diff options
-rw-r--r-- | doc/api/net.md | 80 | ||||
-rw-r--r-- | lib/internal/blocklist.js | 115 | ||||
-rw-r--r-- | lib/net.js | 6 | ||||
-rw-r--r-- | node.gyp | 1 | ||||
-rw-r--r-- | src/node_binding.cc | 1 | ||||
-rw-r--r-- | src/node_sockaddr-inl.h | 28 | ||||
-rw-r--r-- | src/node_sockaddr.cc | 592 | ||||
-rw-r--r-- | src/node_sockaddr.h | 146 | ||||
-rw-r--r-- | test/cctest/test_sockaddr.cc | 86 | ||||
-rw-r--r-- | test/parallel/test-blocklist.js | 136 |
10 files changed, 1189 insertions, 2 deletions
diff --git a/doc/api/net.md b/doc/api/net.md index 3bf9a68a8ab..72324760f88 100644 --- a/doc/api/net.md +++ b/doc/api/net.md @@ -55,6 +55,86 @@ net.createServer().listen( path.join('\\\\?\\pipe', process.cwd(), 'myctl')); ``` +## Class: `net.BlockList` +<!-- YAML +added: REPLACEME +--> + +The `BlockList` object can be used with some network APIs to specify rules for +disabling inbound or outbound access to specific IP addresses, IP ranges, or +IP subnets. + +### `blockList.addAddress(address[, type])` +<!-- YAML +added: REPLACEME +--> + +* `address` {string} An IPv4 or IPv6 address. +* `type` {string} Either `'ipv4'` or `'ipv6'`. **Default**: `'ipv4'`. + +Adds a rule to block the given IP address. + +### `blockList.addRange(start, end[, type])` +<!-- YAML +added: REPLACEME +--> + +* `start` {string} The starting IPv4 or IPv6 address in the range. +* `end` {string} The ending IPv4 or IPv6 address in the range. +* `type` {string} Either `'ipv4'` or `'ipv6'`. **Default**: `'ipv4'`. + +Adds a rule to block a range of IP addresses from `start` (inclusive) to +`end` (inclusive). + +### `blockList.addSubnet(net, prefix[, type])` +<!-- YAML +added: REPLACEME +--> + +* `net` {string} The network IPv4 or IPv6 address. +* `prefix` {number} The number of CIDR prefix bits. For IPv4, this + must be a value between `0` and `32`. For IPv6, this must be between + `0` and `128`. +* `type` {string} Either `'ipv4'` or `'ipv6'`. **Default**: `'ipv4'`. + +Adds a rule to block a range of IP addresses specified as a subnet mask. + +### `blockList.check(address[, type])` +<!-- YAML +added: REPLACEME +--> + +* `address` {string} The IP address to check +* `type` {string} Either `'ipv4'` or `'ipv6'`. **Default**: `'ipv4'`. +* Returns: {boolean} + +Returns `true` if the given IP address matches any of the rules added to the +`BlockList`. + +```js +const blockList = new net.BlockList(); +blockList.addAddress('123.123.123.123'); +blockList.addRange('10.0.0.1', '10.0.0.10'); +blockList.addSubnet('8592:757c:efae:4e45::', 64, 'ipv6'); + +console.log(blockList.check('123.123.123.123')); // Prints: true +console.log(blockList.check('10.0.0.3')); // Prints: true +console.log(blockList.check('222.111.111.222')); // Prints: false + +// IPv6 notation for IPv4 addresses works: +console.log(blockList.check('::ffff:7b7b:7b7b', 'ipv6')); // Prints: true +console.log(blockList.check('::ffff:123.123.123.123', 'ipv6')); // Prints: true +``` + +### `blockList.rules` +<!-- YAML +added: REPLACEME +--> + +* Type: {string[]} + +The list of rules added to the blocklist. + ## Class: `net.Server` <!-- YAML added: v0.1.90 diff --git a/lib/internal/blocklist.js b/lib/internal/blocklist.js new file mode 100644 index 00000000000..d4074ab41c2 --- /dev/null +++ b/lib/internal/blocklist.js @@ -0,0 +1,115 @@ +'use strict'; + +const { + Boolean, + Symbol +} = primordials; + +const { + BlockList: BlockListHandle, + AF_INET, + AF_INET6, +} = internalBinding('block_list'); + +const { + customInspectSymbol: kInspect, +} = require('internal/util'); +const { inspect } = require('internal/util/inspect'); + +const kHandle = Symbol('kHandle'); +const { owner_symbol } = internalBinding('symbols'); + +const { + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + ERR_OUT_OF_RANGE, +} = require('internal/errors').codes; + +class BlockList { + constructor() { + this[kHandle] = new BlockListHandle(); + this[kHandle][owner_symbol] = this; + } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1 + }; + + return `BlockList ${inspect({ + rules: this.rules + }, opts)}`; + } + + addAddress(address, family = 'ipv4') { + if (typeof address !== 'string') + throw new ERR_INVALID_ARG_TYPE('address', 'string', address); + if (typeof family !== 'string') + throw new ERR_INVALID_ARG_TYPE('family', 'string', family); + if (family !== 'ipv4' && family !== 'ipv6') + throw new ERR_INVALID_ARG_VALUE('family', family); + const type = family === 'ipv4' ? AF_INET : AF_INET6; + this[kHandle].addAddress(address, type); + } + + addRange(start, end, family = 'ipv4') { + if (typeof start !== 'string') + throw new ERR_INVALID_ARG_TYPE('start', 'string', start); + if (typeof end !== 'string') + throw new ERR_INVALID_ARG_TYPE('end', 'string', end); + if (typeof family !== 'string') + throw new ERR_INVALID_ARG_TYPE('family', 'string', family); + if (family !== 'ipv4' && family !== 'ipv6') + throw new ERR_INVALID_ARG_VALUE('family', family); + const type = family === 'ipv4' ? AF_INET : AF_INET6; + const ret = this[kHandle].addRange(start, end, type); + if (ret === false) + throw new ERR_INVALID_ARG_VALUE('start', start, 'must come before end'); + } + + addSubnet(network, prefix, family = 'ipv4') { + if (typeof network !== 'string') + throw new ERR_INVALID_ARG_TYPE('network', 'string', network); + if (typeof prefix !== 'number') + throw new ERR_INVALID_ARG_TYPE('prefix', 'number', prefix); + if (typeof family !== 'string') + throw new ERR_INVALID_ARG_TYPE('family', 'string', family); + let type; + switch (family) { + case 'ipv4': + type = AF_INET; + if (prefix < 0 || prefix > 32) + throw new ERR_OUT_OF_RANGE(prefix, '>= 0 and <= 32', prefix); + break; + case 'ipv6': + type = AF_INET6; + if (prefix < 0 || prefix > 128) + throw new ERR_OUT_OF_RANGE(prefix, '>= 0 and <= 128', prefix); + break; + default: + throw new ERR_INVALID_ARG_VALUE('family', family); + } + this[kHandle].addSubnet(network, type, prefix); + } + + check(address, family = 'ipv4') { + if (typeof address !== 'string') + throw new ERR_INVALID_ARG_TYPE('address', 'string', address); + if (typeof family !== 'string') + throw new ERR_INVALID_ARG_TYPE('family', 'string', family); + if (family !== 'ipv4' && family !== 'ipv6') + throw new ERR_INVALID_ARG_VALUE('family', family); + const type = family === 'ipv4' ? AF_INET : AF_INET6; + return Boolean(this[kHandle].check(address, type)); + } + + get rules() { + return this[kHandle].getRules(); + } +} + +module.exports = BlockList; diff --git a/lib/net.js b/lib/net.js index 54958d5912e..21b28c8978d 100644 --- a/lib/net.js +++ b/lib/net.js @@ -117,6 +117,7 @@ const { // Lazy loaded to improve startup performance. let cluster; let dns; +let BlockList; const { clearTimeout } = require('timers'); const { kTimeout } = require('internal/timers'); @@ -1724,6 +1725,11 @@ module.exports = { _createServerHandle: createServerHandle, _normalizeArgs: normalizeArgs, _setSimultaneousAccepts, + get BlockList() { + if (BlockList === undefined) + BlockList = require('internal/blocklist'); + return BlockList; + }, connect, createConnection: connect, createServer, @@ -106,6 +106,7 @@ 'lib/internal/assert/assertion_error.js', 'lib/internal/assert/calltracker.js', 'lib/internal/async_hooks.js', + 'lib/internal/blocklist.js', 'lib/internal/buffer.js', 'lib/internal/cli_table.js', 'lib/internal/child_process.js', diff --git a/src/node_binding.cc b/src/node_binding.cc index 4dbf56b99a3..9890f9e7be9 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -44,6 +44,7 @@ // __attribute__((constructor)) like mechanism in GCC. #define NODE_BUILTIN_STANDARD_MODULES(V) \ V(async_wrap) \ + V(block_list) \ V(buffer) \ V(cares_wrap) \ V(config) \ diff --git a/src/node_sockaddr-inl.h b/src/node_sockaddr-inl.h index c8b985aedda..e5d8985771e 100644 --- a/src/node_sockaddr-inl.h +++ b/src/node_sockaddr-inl.h @@ -4,6 +4,7 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #include "node.h" +#include "env-inl.h" #include "node_internals.h" #include "node_sockaddr.h" #include "util-inl.h" @@ -88,11 +89,11 @@ SocketAddress& SocketAddress::operator=(const SocketAddress& addr) { } const sockaddr& SocketAddress::operator*() const { - return *this->data(); + return *data(); } const sockaddr* SocketAddress::operator->() const { - return this->data(); + return data(); } size_t SocketAddress::length() const { @@ -166,6 +167,24 @@ bool SocketAddress::operator!=(const SocketAddress& other) const { return !(*this == other); } +bool SocketAddress::operator<(const SocketAddress& other) const { + return compare(other) == CompareResult::LESS_THAN; +} + +bool SocketAddress::operator>(const SocketAddress& other) const { + return compare(other) == CompareResult::GREATER_THAN; +} + +bool SocketAddress::operator<=(const SocketAddress& other) const { + CompareResult c = compare(other); + return c == CompareResult::NOT_COMPARABLE ? false : + c <= CompareResult::SAME; +} + +bool SocketAddress::operator>=(const SocketAddress& other) const { + return compare(other) >= CompareResult::SAME; +} + template <typename T> SocketAddressLRU<T>::SocketAddressLRU( size_t max_size) @@ -231,6 +250,11 @@ typename T::Type* SocketAddressLRU<T>::Upsert( return &map_[address]->second; } +v8::MaybeLocal<v8::Value> SocketAddressBlockList::Rule::ToV8String( + Environment* env) { + std::string str = ToString(); + return ToV8Value(env->context(), str); +} } // namespace node #endif // NODE_WANT_INTERNALS diff --git a/src/node_sockaddr.cc b/src/node_sockaddr.cc index 74fe123529a..8ba82ff6853 100644 --- a/src/node_sockaddr.cc +++ b/src/node_sockaddr.cc @@ -1,8 +1,25 @@ #include "node_sockaddr-inl.h" // NOLINT(build/include) +#include "env-inl.h" +#include "base_object-inl.h" +#include "memory_tracker-inl.h" #include "uv.h" +#include <memory> +#include <string> +#include <vector> + namespace node { +using v8::Array; +using v8::Context; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::Local; +using v8::MaybeLocal; +using v8::Object; +using v8::String; +using v8::Value; + namespace { template <typename T, typename F> SocketAddress FromUVHandle(F fn, const T& handle) { @@ -92,4 +109,579 @@ SocketAddress SocketAddress::FromPeerName(const uv_udp_t& handle) { return FromUVHandle(uv_udp_getpeername, handle); } +namespace { +constexpr uint8_t mask[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff }; + +bool is_match_ipv4( + const SocketAddress& one, + const SocketAddress& two) { + const sockaddr_in* one_in = + reinterpret_cast<const sockaddr_in*>(one.data()); + const sockaddr_in* two_in = + reinterpret_cast<const sockaddr_in*>(two.data()); + return memcmp(&one_in->sin_addr, &two_in->sin_addr, sizeof(uint32_t)) == 0; +} + +bool is_match_ipv6( + const SocketAddress& one, + const SocketAddress& two) { + const sockaddr_in6* one_in = + reinterpret_cast<const sockaddr_in6*>(one.data()); + const sockaddr_in6* two_in = + reinterpret_cast<const sockaddr_in6*>(two.data()); + return memcmp(&one_in->sin6_addr, &two_in->sin6_addr, 16) == 0; +} + +bool is_match_ipv4_ipv6( + const SocketAddress& ipv4, + const SocketAddress& ipv6) { + const sockaddr_in* check_ipv4 = + reinterpret_cast<const sockaddr_in*>(ipv4.data()); + const sockaddr_in6* check_ipv6 = + reinterpret_cast<const sockaddr_in6*>(ipv6.data()); + + const uint8_t* ptr = + reinterpret_cast<const uint8_t*>(&check_ipv6->sin6_addr); + + return memcmp(ptr, mask, sizeof(mask)) == 0 && + memcmp(ptr + sizeof(mask), + &check_ipv4->sin_addr, + sizeof(uint32_t)) == 0; +} + +SocketAddress::CompareResult compare_ipv4( + const SocketAddress& one, + const SocketAddress& two) { + const sockaddr_in* one_in = + reinterpret_cast<const sockaddr_in*>(one.data()); + const sockaddr_in* two_in = + reinterpret_cast<const sockaddr_in*>(two.data()); + + if (one_in->sin_addr.s_addr < two_in->sin_addr.s_addr) + return SocketAddress::CompareResult::LESS_THAN; + else if (one_in->sin_addr.s_addr == two_in->sin_addr.s_addr) + return SocketAddress::CompareResult::SAME; + else + return SocketAddress::CompareResult::GREATER_THAN; +} + +SocketAddress::CompareResult compare_ipv6( + const SocketAddress& one, + const SocketAddress& two) { + const sockaddr_in6* one_in = + reinterpret_cast<const sockaddr_in6*>(one.data()); + const sockaddr_in6* two_in = + reinterpret_cast<const sockaddr_in6*>(two.data()); + int ret = memcmp(&one_in->sin6_addr, &two_in->sin6_addr, 16); + if (ret < 0) + return SocketAddress::CompareResult::LESS_THAN; + else if (ret > 0) + return SocketAddress::CompareResult::GREATER_THAN; + return SocketAddress::CompareResult::SAME; +} + +SocketAddress::CompareResult compare_ipv4_ipv6( + const SocketAddress& ipv4, + const SocketAddress& ipv6) { + const sockaddr_in* ipv4_in = + reinterpret_cast<const sockaddr_in*>(ipv4.data()); + const sockaddr_in6 * ipv6_in = + reinterpret_cast<const sockaddr_in6*>(ipv6.data()); + + const uint8_t* ptr = + reinterpret_cast<const uint8_t*>(&ipv6_in->sin6_addr); + + if (memcmp(ptr, mask, sizeof(mask)) != 0) + return SocketAddress::CompareResult::NOT_COMPARABLE; + + int ret = memcmp( + &ipv4_in->sin_addr, + ptr + sizeof(mask), + sizeof(uint32_t)); + + if (ret < 0) + return SocketAddress::CompareResult::LESS_THAN; + else if (ret > 0) + return SocketAddress::CompareResult::GREATER_THAN; + return SocketAddress::CompareResult::SAME; +} + +bool in_network_ipv4( + const SocketAddress& ip, + const SocketAddress& net, + int prefix) { + uint32_t mask = ((1 << prefix) - 1) << (32 - prefix); + + const sockaddr_in* ip_in = + reinterpret_cast<const sockaddr_in*>(ip.data()); + const sockaddr_in* net_in = + reinterpret_cast<const sockaddr_in*>(net.data()); + + return (htonl(ip_in->sin_addr.s_addr) & mask) == + (htonl(net_in->sin_addr.s_addr) & mask); +} + +bool in_network_ipv6( + const SocketAddress& ip, + const SocketAddress& net, + int prefix) { + // Special case, if prefix == 128, then just do a + // straight comparison. + if (prefix == 128) + return compare_ipv6(ip, net) == SocketAddress::CompareResult::SAME; + + uint8_t r = prefix % 8; + int len = (prefix - r) / 8; + uint8_t mask = ((1 << r) - 1) << (8 - r); + + const sockaddr_in6* ip_in = + reinterpret_cast<const sockaddr_in6*>(ip.data()); + const sockaddr_in6* net_in = + reinterpret_cast<const sockaddr_in6*>(net.data()); + + if (memcmp(&ip_in->sin6_addr, &net_in->sin6_addr, len) != 0) + return false; + + const uint8_t* p1 = reinterpret_cast<const uint8_t*>( + ip_in->sin6_addr.s6_addr); + const uint8_t* p2 = reinterpret_cast<const uint8_t*>( + net_in->sin6_addr.s6_addr); + + return (p1[len] & mask) == (p2[len] & mask); +} + +bool in_network_ipv4_ipv6( + const SocketAddress& ip, + const SocketAddress& net, + int prefix) { + + if (prefix == 128) + return compare_ipv4_ipv6(ip, net) == SocketAddress::CompareResult::SAME; + + uint8_t r = prefix % 8; + int len = (prefix - r) / 8; + uint8_t mask = ((1 << r) - 1) << (8 - r); + + const sockaddr_in* ip_in = + reinterpret_cast<const sockaddr_in*>(ip.data()); + const sockaddr_in6* net_in = + reinterpret_cast<const sockaddr_in6*>(net.data()); + + uint8_t ip_mask[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0, 0, 0, 0}; + uint8_t* ptr = ip_mask; + memcpy(ptr + 12, &ip_in->sin_addr, 4); + + if (memcmp(ptr, &net_in->sin6_addr, len) != 0) + return false; + + ptr += len; + const uint8_t* p2 = reinterpret_cast<const uint8_t*>( + net_in->sin6_addr.s6_addr); + + return (ptr[0] & mask) == (p2[len] & mask); +} + +bool in_network_ipv6_ipv4( + const SocketAddress& ip, + const SocketAddress& net, + int prefix) { + if (prefix == 32) + return compare_ipv4_ipv6(net, ip) == SocketAddress::CompareResult::SAME; + + uint32_t m = ((1 << prefix) - 1) << (32 - prefix); + + const sockaddr_in6* ip_in = + reinterpret_cast<const sockaddr_in6*>(ip.data()); + const sockaddr_in* net_in = + reinterpret_cast<const sockaddr_in*>(net.data()); + + const uint8_t* ptr = + reinterpret_cast<const uint8_t*>(&ip_in->sin6_addr); + + if (memcmp(ptr, mask, sizeof(mask)) != 0) + return false; + + ptr += sizeof(mask); + uint32_t check = ptr[0] << 24 | ptr[1] << 16 | ptr[2] << 8 | ptr[3]; + + return (check & m) == (htonl(net_in->sin_addr.s_addr) & m); +} +} // namespace + +// TODO(@jasnell): The implementations of is_match, compare, and +// is_in_network have not been performance optimized and could +// likely benefit from work on more performant approaches. + +bool SocketAddress::is_match(const SocketAddress& other) const { + switch (family()) { + case AF_INET: + switch (other.family()) { + case AF_INET: return is_match_ipv4(*this, other); + case AF_INET6: return is_match_ipv4_ipv6(*this, other); + } + break; + case AF_INET6: + switch (other.family()) { + case AF_INET: return is_match_ipv4_ipv6(other, *this); + case AF_INET6: return is_match_ipv6(*this, other); + } + break; + } + return false; +} + +SocketAddress::CompareResult SocketAddress::compare( + const SocketAddress& other) const { + switch (family()) { + case AF_INET: + switch (other.family()) { + case AF_INET: return compare_ipv4(*this, other); + case AF_INET6: return compare_ipv4_ipv6(*this, other); + } + break; + case AF_INET6: + switch (other.family()) { + case AF_INET: { + CompareResult c = compare_ipv4_ipv6(other, *this); + switch (c) { + case SocketAddress::CompareResult::NOT_COMPARABLE: + // Fall through + case SocketAddress::CompareResult::SAME: + return c; + case SocketAddress::CompareResult::GREATER_THAN: + return SocketAddress::CompareResult::LESS_THAN; + case SocketAddress::CompareResult::LESS_THAN: + return SocketAddress::CompareResult::GREATER_THAN; + } + break; + } + case AF_INET6: return compare_ipv6(*this, other); + } + break; + } + return SocketAddress::CompareResult::NOT_COMPARABLE; +} + +bool SocketAddress::is_in_network( + const SocketAddress& other, + int prefix) const { + + switch (family()) { + case AF_INET: + switch (other.family()) { + case AF_INET: return in_network_ipv4(*this, other, prefix); + case AF_INET6: return in_network_ipv4_ipv6(*this, other, prefix); + } + break; + case AF_INET6: + switch (other.family()) { + case AF_INET: return in_network_ipv6_ipv4(*this, other, prefix); + case AF_INET6: return in_network_ipv6(*this, other, prefix); + } + break; + } + + return false; +} + +SocketAddressBlockList::SocketAddressBlockList( + std::shared_ptr<SocketAddressBlockList> parent) + : parent_(parent) {} + +void SocketAddressBlockList::AddSocketAddress( + const SocketAddress& address) { + std::unique_ptr<Rule> rule = + std::make_unique<SocketAddressRule>(address); + rules_.emplace_front(std::move(rule)); + address_rules_[address] = rules_.begin(); +} + +void SocketAddressBlockList::RemoveSocketAddress( + const SocketAddress& address) { + auto it = address_rules_.find(address); + if (it != std::end(address_rules_)) { + rules_.erase(it->second); + address_rules_.erase(it); + } +} + +void SocketAddressBlockList::AddSocketAddressRange( + const SocketAddress& start, + const SocketAddress& end) { + std::unique_ptr<Rule> rule = + std::make_unique<SocketAddressRangeRule>(start, end); + rules_.emplace_front(std::move(rule)); +} + +void SocketAddressBlockList::AddSocketAddressMask( + const SocketAddress& network, + int prefix) { + std::unique_ptr<Rule> rule = + std::make_unique<SocketAddressMaskRule>(network, prefix); + rules_.emplace_front(std::move(rule)); +} + +bool SocketAddressBlockList::Apply(const SocketAddress& address) { + for (const auto& rule : rules_) { + if (rule->Apply(address)) + return true; + } + return parent_ ? parent_->Apply(address) : false; +} + +SocketAddressBlockList::SocketAddressRule::SocketAddressRule( + const SocketAddress& address_) + : address(address_) {} + +SocketAddressBlockList::SocketAddressRangeRule::SocketAddressRangeRule( + const SocketAddress& start_, + const SocketAddress& end_) + : start(start_), + end(end_) {} + +SocketAddressBlockList::SocketAddressMaskRule::SocketAddressMaskRule( + const SocketAddress& network_, + int prefix_) + : network(network_), + prefix(prefix_) {} + +bool SocketAddressBlockList::SocketAddressRule::Apply( + const SocketAddress& address) { + return this->address.is_match(address); +} + +std::string SocketAddressBlockList::SocketAddressRule::ToString() { + std::string ret = "Address: "; + ret += address.family() == AF_INET ? "IPv4" : "IPv6"; + ret += " "; + ret += address.address(); + return ret; +} + +bool SocketAddressBlockList::SocketAddressRangeRule::Apply( + const SocketAddress& address) { + return address >= start && address <= end; +} + +std::string SocketAddressBlockList::SocketAddressRangeRule::ToString() { + std::string ret = "Range: "; + ret += start.family() == AF_INET ? "IPv4" : "IPv6"; + ret += " "; + ret += start.address(); + ret += "-"; + ret += end.address(); + return ret; +} + +bool SocketAddressBlockList::SocketAddressMaskRule::Apply( + const SocketAddress& address) { + return address.is_in_network(network, prefix); +} + +std::string SocketAddressBlockList::SocketAddressMaskRule::ToString() { + std::string ret = "Subnet: "; + ret += network.family() == AF_INET ? "IPv4" : "IPv6"; + ret += " "; + ret += network.address(); + ret += "/" + std::to_string(prefix); + return ret; +} + +MaybeLocal<Array> SocketAddressBlockList::ListRules(Environment* env) { + std::vector<Local<Value>> rules; + for (const auto& rule : rules_) { + Local<Value> str; + if (!rule->ToV8String(env).ToLocal(&str)) + return MaybeLocal<Array>(); + rules.push_back(str); + } + return Array::New(env->isolate(), rules.data(), rules.size()); +} + +void SocketAddressBlockList::MemoryInfo(node::MemoryTracker* tracker) const { + tracker->TrackField("rules", rules_); +} + +void SocketAddressBlockList::SocketAddressRule::MemoryInfo( + node::MemoryTracker* tracker) const { + tracker->TrackField("address", address); +} + +void SocketAddressBlockList::SocketAddressRangeRule::MemoryInfo( + node::MemoryTracker* tracker) const { + tracker->TrackField("start", start); + tracker->TrackField("end", end); +} + +void SocketAddressBlockList::SocketAddressMaskRule::MemoryInfo( + node::MemoryTracker* tracker) const { + tracker->TrackField("network", network); +} + +SocketAddressBlockListWrap::SocketAddressBlockListWrap( + Environment* env, Local<Object> wrap) + : BaseObject(env, wrap) { + MakeWeak(); +} + +void SocketAddressBlockListWrap::New( + const FunctionCallbackInfo<Value>& args) { + CHECK(args.IsConstructCall()); + Environment* env = Environment::GetCurrent(args); + new SocketAddressBlockListWrap(env, args.This()); +} + +void SocketAddressBlockListWrap::AddAddress( + const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + SocketAddressBlockListWrap* wrap; + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + + CHECK(args[0]->IsString()); + CHECK(args[1]->IsInt32()); + + sockaddr_storage address; + Utf8Value value(args.GetIsolate(), args[0]); + int32_t family; + if (!args[1]->Int32Value(env->context()).To(&family)) + return; + + if (!SocketAddress::ToSockAddr(family, *value, 0, &address)) + return; + + wrap->AddSocketAddress( + SocketAddress(reinterpret_cast<const sockaddr*>(&address))); + + args.GetReturnValue().Set(true); +} + +void SocketAddressBlockListWrap::AddRange( + const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + SocketAddressBlockListWrap* wrap; + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + + CHECK(args[0]->IsString()); + CHECK(args[1]->IsString()); + CHECK(args[2]->IsInt32()); + + sockaddr_storage address[2]; + Utf8Value start(args.GetIsolate(), args[0]); + Utf8Value end(args.GetIsolate(), args[1]); + int32_t family; + if (!args[2]->Int32Value(env->context()).To(&family)) + return; + + if (!SocketAddress::ToSockAddr(family, *start, 0, &address[0]) || + !SocketAddress::ToSockAddr(family, *end, 0, &address[1])) { + return; + } + + SocketAddress start_addr(reinterpret_cast<const sockaddr*>(&address[0])); + SocketAddress end_addr(reinterpret_cast<const sockaddr*>(&address[1])); + + // Starting address must come before the end address + if (start_addr > end_addr) + return args.GetReturnValue().Set(false); + + wrap->AddSocketAddressRange(start_addr, end_addr); + + args.GetReturnValue().Set(true); +} + +void SocketAddressBlockListWrap::AddSubnet( + const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + SocketAddressBlockListWrap* wrap; + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + + CHECK(args[0]->IsString()); + CHECK(args[1]->IsInt32()); + CHECK(args[2]->IsInt32()); + + sockaddr_storage address; + Utf8Value network(args.GetIsolate(), args[0]); + int32_t family; + int32_t prefix; + if (!args[1]->Int32Value(env->context()).To(&family) || + !args[2]->Int32Value(env->context()).To(&prefix)) { + return; + } + + if (!SocketAddress::ToSockAddr(family, *network, 0, &address)) + return; + + CHECK_IMPLIES(family == AF_INET, prefix <= 32); + CHECK_IMPLIES(family == AF_INET6, prefix <= 128); + CHECK_GE(prefix, 0); + + wrap->AddSocketAddressMask( + SocketAddress(reinterpret_cast<const sockaddr*>(&address)), + prefix); + + args.GetReturnValue().Set(true); +} + +void SocketAddressBlockListWrap::Check( + const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + SocketAddressBlockListWrap* wrap; + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + + CHECK(args[0]->IsString()); + CHECK(args[1]->IsInt32()); + + sockaddr_storage address; + Utf8Value value(args.GetIsolate(), args[0]); + int32_t family; + if (!args[1]->Int32Value(env->context()).To(&family)) + return; + + if (!SocketAddress::ToSockAddr(family, *value, 0, &address)) + return; + + args.GetReturnValue().Set( + wrap->Apply(SocketAddress(reinterpret_cast<const sockaddr*>(&address)))); +} + +void SocketAddressBlockListWrap::GetRules( + const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + SocketAddressBlockListWrap* wrap; + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + Local<Array> rules; + if (wrap->ListRules(env).ToLocal(&rules)) + args.GetReturnValue().Set(rules); +} + +void SocketAddressBlockListWrap::Initialize( + Local<Object> target, + Local<Value> unused, + Local<Context> context, + void* priv) { + Environment* env = Environment::GetCurrent(context); + + Local<String> name = FIXED_ONE_BYTE_STRING(env->isolate(), "BlockList"); + Local<FunctionTemplate> t = + env->NewFunctionTemplate(SocketAddressBlockListWrap::New); + t->InstanceTemplate()->SetInternalFieldCount(BaseObject::kInternalFieldCount); + t->SetClassName(name); + + env->SetProtoMethod(t, "addAddress", SocketAddressBlockListWrap::AddAddress); + env->SetProtoMethod(t, "addRange", SocketAddressBlockListWrap::AddRange); + env->SetProtoMethod(t, "addSubnet", SocketAddressBlockListWrap::AddSubnet); + env->SetProtoMethod(t, "check", SocketAddressBlockListWrap::Check); + env->SetProtoMethod(t, "getRules", SocketAddressBlockListWrap::GetRules); + + target->Set(env->context(), name, + t->GetFunction(env->context()).ToLocalChecked()).FromJust(); + + NODE_DEFINE_CONSTANT(target, AF_INET); + NODE_DEFINE_CONSTANT(target, AF_INET6); +} + } // namespace node + +NODE_MODULE_CONTEXT_AWARE_INTERNAL( + block_list, + node::SocketAddressBlockListWrap::Initialize) diff --git a/src/node_sockaddr.h b/src/node_sockaddr.h index 5d20487f93d..f539cf6555f 100644 --- a/src/node_sockaddr.h +++ b/src/node_sockaddr.h @@ -3,11 +3,14 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#include "env.h" #include "memory_tracker.h" +#include "base_object.h" #include "node.h" #include "uv.h" #include "v8.h" +#include <memory> #include <string> #include <list> #include <unordered_map> @@ -18,6 +21,13 @@ class Environment; class SocketAddress : public MemoryRetainer { public: + enum class CompareResult { + NOT_COMPARABLE = -2, + LESS_THAN, + SAME, + GREATER_THAN + }; + struct Hash { size_t operator()(const SocketAddress& addr) const; }; @@ -25,6 +35,11 @@ class SocketAddress : public MemoryRetainer { inline bool operator==(const SocketAddress& other) const; inline bool operator!=(const SocketAddress& other) const; + inline bool operator<(const SocketAddress& other) const; + inline bool operator>(const SocketAddress& other) const; + inline bool operator<=(const SocketAddress& other) const; + inline bool operator>=(const SocketAddress& other) const; + inline static bool is_numeric_host(const char* hostname); inline static bool is_numeric_host(const char* hostname, int family); @@ -78,6 +93,20 @@ class SocketAddress : public MemoryRetainer { inline std::string address() const; inline int port() const; + // Returns true if the given other SocketAddress is a match + // for this one. The addresses are a match if: + // 1. They are the same family and match identically + // 2. They are different family but match semantically ( + // for instance, an IPv4 addres in IPv6 notation) + bool is_match(const SocketAddress& other) const; + + // Compares this SocketAddress to the given other SocketAddress. + CompareResult compare(const SocketAddress& other) const; + + // Returns true if this SocketAddress is within the subnet + // identified by the given network address and CIDR prefix. + bool is_in_network(const SocketAddress& network, int prefix) const; + // If the SocketAddress is an IPv6 address, returns the // current value of the IPv6 flow label, if set. Otherwise // returns 0. @@ -152,6 +181,123 @@ class SocketAddressLRU : public MemoryRetainer { size_t max_size_; }; +// A BlockList is used to evaluate whether a given +// SocketAddress should be accepted for inbound or +// outbound network activity. +class SocketAddressBlockList : public MemoryRetainer { + public: + explicit SocketAddressBlockList( + std::shared_ptr<SocketAddressBlockList> parent = {}); + ~SocketAddressBlockList() = default; + + void AddSocketAddress( + const SocketAddress& address); + + void RemoveSocketAddress( + const SocketAddress& address); + + void AddSocketAddressRange( + const SocketAddress& start, + const SocketAddress& end); + + void AddSocketAddressMask( + const SocketAddress& address, + int prefix); + + bool Apply(const SocketAddress& address); + + size_t size() const { return rules_.size(); } + + v8::MaybeLocal<v8::Array> ListRules(Environment* env); + + struct Rule : public MemoryRetainer { + virtual bool Apply(const SocketAddress& address) = 0; + inline v8::MaybeLocal<v8::Value> ToV8String(Environment* env); + virtual std::string ToString() = 0; + }; + + struct SocketAddressRule final : Rule { + SocketAddress address; + + explicit SocketAddressRule(const SocketAddress& address); + + bool Apply(const SocketAddress& address) override; + std::string ToString() override; + + void MemoryInfo(node::MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(SocketAddressRule) + SET_SELF_SIZE(SocketAddressRule) + }; + + struct SocketAddressRangeRule final : Rule { + SocketAddress start; + SocketAddress end; + + SocketAddressRangeRule( + const SocketAddress& start, + const SocketAddress& end); + + bool Apply(const SocketAddress& address) override; + std::string ToString() override; + + void MemoryInfo(node::MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(SocketAddressRangeRule) + SET_SELF_SIZE(SocketAddressRangeRule) + }; + + struct SocketAddressMaskRule final : Rule { + SocketAddress network; + int prefix; + + SocketAddressMaskRule( + const SocketAddress& address, + int prefix); + + bool Apply(const SocketAddress& address) override; + std::string ToString() override; + + void MemoryInfo(node::MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(SocketAddressMaskRule) + SET_SELF_SIZE(SocketAddressMaskRule) + }; + + void MemoryInfo(node::MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(SocketAddressBlockList) + SET_SELF_SIZE(SocketAddressBlockList) + + private: + std::shared_ptr<SocketAddressBlockList> parent_; + std::list<std::unique_ptr<Rule>> rules_; + SocketAddress::Map<std::list<std::unique_ptr<Rule>>::iterator> address_rules_; +}; + +class SocketAddressBlockListWrap : + public BaseObject, + public SocketAddressBlockList { + public: + static void Initialize(v8::Local<v8::Object> target, + v8::Local<v8::Value> unused, + v8::Local<v8::Context> context, + void* priv); + + static void New(const v8::FunctionCallbackInfo<v8::Value>& args); + static void AddAddress(const v8::FunctionCallbackInfo<v8::Value>& args); + static void AddRange(const v8::FunctionCallbackInfo<v8::Value>& args); + static void AddSubnet(const v8::FunctionCallbackInfo<v8::Value>& args); + static void Check(const v8::FunctionCallbackInfo<v8::Value>& args); + static void GetRules(const v8::FunctionCallbackInfo<v8::Value>& args); + + SocketAddressBlockListWrap( + Environment* env, + v8::Local<v8::Object> wrap); + + void MemoryInfo(node::MemoryTracker* tracker) const override { + SocketAddressBlockList::MemoryInfo(tracker); + } + SET_MEMORY_INFO_NAME(SocketAddressBlockListWrap) + SET_SELF_SIZE(SocketAddressBlockListWrap) +}; + } // namespace node #endif // NOE_WANT_INTERNALS diff --git a/test/cctest/test_sockaddr.cc b/test/cctest/test_sockaddr.cc index 9abcd8ba819..036dfae78a5 100644 --- a/test/cctest/test_sockaddr.cc +++ b/test/cctest/test_sockaddr.cc @@ -2,6 +2,7 @@ #include "gtest/gtest.h" using node::SocketAddress; +using node::SocketAddressBlockList; using node::SocketAddressLRU; TEST(SocketAddress, SocketAddress) { @@ -84,6 +85,7 @@ TEST(SocketAddressLRU, SocketAddressLRU) { SocketAddress::ToSockAddr(AF_INET, "123.123.123.125", 443, &storage[2]); SocketAddress::ToSockAddr(AF_INET, "123.123.123.123", 443, &storage[3]); + SocketAddress addr1(reinterpret_cast<const sockaddr*>(&storage[0])); SocketAddress addr2(reinterpret_cast<const sockaddr*>(&storage[1])); SocketAddress addr3(reinterpret_cast<const sockaddr*>(&storage[2])); @@ -125,3 +127,87 @@ TEST(SocketAddressLRU, SocketAddressLRU) { CHECK_NULL(lru.Peek(addr1)); CHECK_NULL(lru.Peek(addr2)); } + +TEST(SocketAddress, Comparison) { + sockaddr_storage storage[6]; + + SocketAddress::ToSockAddr(AF_INET, "10.0.0.1", 0, &storage[0]); + SocketAddress::ToSockAddr(AF_INET, "10.0.0.2", 0, &storage[1]); + SocketAddress::ToSockAddr(AF_INET6, "::1", 0, &storage[2]); + SocketAddress::ToSockAddr(AF_INET6, "::2", 0, &storage[3]); + SocketAddress::ToSockAddr(AF_INET6, "::ffff:10.0.0.1", 0, &storage[4]); + SocketAddress::ToSockAddr(AF_INET6, "::ffff:10.0.0.2", 0, &storage[5]); + + SocketAddress addr1(reinterpret_cast<const sockaddr*>(&storage[0])); + SocketAddress addr2(reinterpret_cast<const sockaddr*>(&storage[1])); + SocketAddress addr3(reinterpret_cast<const sockaddr*>(&storage[2])); + SocketAddress addr4(reinterpret_cast<const sockaddr*>(&storage[3])); + SocketAddress addr5(reinterpret_cast<const sockaddr*>(&storage[4])); + SocketAddress addr6(reinterpret_cast<const sockaddr*>(&storage[5])); + + CHECK_EQ(addr1.compare(addr1), SocketAddress::CompareResult::SAME); + CHECK_EQ(addr1.compare(addr2), SocketAddress::CompareResult::LESS_THAN); + CHECK_EQ(addr2.compare(addr1), SocketAddress::CompareResult::GREATER_THAN); + CHECK(addr1 <= addr1); + CHECK(addr1 < addr2); + CHECK(addr1 <= addr2); + CHECK(addr2 >= addr2); + CHECK(addr2 > addr1); + CHECK(addr2 >= addr1); + + CHECK_EQ(addr3.compare(addr3), SocketAddress::CompareResult::SAME); + CHECK_EQ(addr3.compare(addr4), SocketAddress::CompareResult::LESS_THAN); + CHECK_EQ(addr4.compare(addr3), SocketAddress::CompareResult::GREATER_THAN); + CHECK(addr3 <= addr3); + CHECK(addr3 < addr4); + CHECK(addr3 <= addr4); + CHECK(addr4 >= addr4); + CHECK(addr4 > addr3); + CHECK(addr4 >= addr3); + + // Not comparable + CHECK_EQ(addr1.compare(addr3), SocketAddress::CompareResult::NOT_COMPARABLE); + CHECK_EQ(addr3.compare(addr1), SocketAddress::CompareResult::NOT_COMPARABLE); + CHECK(!(addr1 < addr3)); + CHECK(!(addr1 > addr3)); + CHECK(!(addr1 >= addr3)); + CHECK(!(addr1 <= addr3)); + CHECK(!(addr3 < addr1)); + CHECK(!(addr3 > addr1)); + CHECK(!(addr3 >= addr1)); + CHECK(!(addr3 <= addr1)); + + // Comparable + CHECK_EQ(addr1.compare(addr5), SocketAddress::CompareResult::SAME); + CHECK_EQ(addr2.compare(addr6), SocketAddress::CompareResult::SAME); + CHECK_EQ(addr1.compare(addr6), SocketAddress::CompareResult::LESS_THAN); + CHECK_EQ(addr6.compare(addr1), SocketAddress::CompareResult::GREATER_THAN); + CHECK(addr1 <= addr5); + CHECK(addr1 <= addr6); + CHECK(addr1 < addr6); + CHECK(addr6 > addr1); + CHECK(addr6 >= addr1); + CHECK(addr2 >= addr6); + CHECK(addr2 >= addr5); +} + +TEST(SocketAddressBlockList, Simple) { + SocketAddressBlockList bl; + + sockaddr_storage storage[2]; + SocketAddress::ToSockAddr(AF_INET, "10.0.0.1", 0, &storage[0]); + SocketAddress::ToSockAddr(AF_INET, "10.0.0.2", 0, &storage[1]); + SocketAddress addr1(reinterpret_cast<const sockaddr*>(&storage[0])); + SocketAddress addr2(reinterpret_cast<const sockaddr*>(&storage[1])); + + bl.AddSocketAddress(addr1); + bl.AddSocketAddress(addr2); + + CHECK(bl.Apply(addr1)); + CHECK(bl.Apply(addr2)); + + bl.RemoveSocketAddress(addr1); + + CHECK(!bl.Apply(addr1)); + CHECK(bl.Apply(addr2)); +} diff --git a/test/parallel/test-blocklist.js b/test/parallel/test-blocklist.js new file mode 100644 index 00000000000..cef90dc9a10 --- /dev/null +++ b/test/parallel/test-blocklist.js @@ -0,0 +1,136 @@ +'use strict'; + +require('../common'); + +const { BlockList } = require('net'); +const assert = require('assert'); + +{ + const blockList = new BlockList(); + + [1, [], {}, null, 1n, undefined, null].forEach((i) => { + assert.throws(() => blockList.addAddress(i), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + [1, [], {}, null, 1n, null].forEach((i) => { + assert.throws(() => blockList.addAddress('1.1.1.1', i), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + assert.throws(() => blockList.addAddress('1.1.1.1', 'foo'), { + code: 'ERR_INVALID_ARG_VALUE' + }); + + [1, [], {}, null, 1n, undefined, null].forEach((i) => { + assert.throws(() => blockList.addRange(i), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => blockList.addRange('1.1.1.1', i), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + [1, [], {}, null, 1n, null].forEach((i) => { + assert.throws(() => blockList.addRange('1.1.1.1', '1.1.1.2', i), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + assert.throws(() => blockList.addRange('1.1.1.1', '1.1.1.2', 'foo'), { + code: 'ERR_INVALID_ARG_VALUE' + }); +} + +{ + const blockList = new BlockList(); + blockList.addAddress('1.1.1.1'); + blockList.addAddress('8592:757c:efae:4e45:fb5d:d62a:0d00:8e17', 'ipv6'); + blockList.addAddress('::ffff:1.1.1.2', 'ipv6'); + + assert(blockList.check('1.1.1.1')); + assert(!blockList.check('1.1.1.1', 'ipv6')); + assert(!blockList.check('8592:757c:efae:4e45:fb5d:d62a:0d00:8e17')); + assert(blockList.check('8592:757c:efae:4e45:fb5d:d62a:0d00:8e17', 'ipv6')); + + assert(blockList.check('::ffff:1.1.1.1', 'ipv6')); + + assert(blockList.check('1.1.1.2')); + + assert(!blockList.check('1.2.3.4')); + assert(!blockList.check('::1', 'ipv6')); +} + +{ + const blockList = new BlockList(); + blockList.addRange('1.1.1.1', '1.1.1.10'); + blockList.addRange('::1', '::f', 'ipv6'); + + assert(!blockList.check('1.1.1.0')); + for (let n = 1; n <= 10; n++) + assert(blockList.check(`1.1.1.${n}`)); + assert(!blockList.check('1.1.1.11')); + + assert(!blockList.check('::0', 'ipv6')); + for (let n = 0x1; n <= 0xf; n++) { + assert(blockList.check(`::${n.toString(16)}`, 'ipv6'), + `::${n.toString(16)} check failed`); + } + assert(!blockList.check('::10', 'ipv6')); +} + +{ + const blockList = new BlockList(); + blockList.addSubnet('1.1.1.0', 16); + blockList.addSubnet('8592:757c:efae:4e45::', 64, 'ipv6'); + + assert(blockList.check('1.1.0.1')); + assert(blockList.check('1.1.1.1')); + assert(!blockList.check('1.2.0.1')); + assert(blockList.check('::ffff:1.1.0.1', 'ipv6')); + + assert(blockList.check('8592:757c:efae:4e45:f::', 'ipv6')); + assert(blockList.check('8592:757c:efae:4e45::f', 'ipv6')); + assert(!blockList.check('8592:757c:efae:4f45::f', 'ipv6')); +} + +{ + const blockList = new BlockList(); + blockList.addAddress('1.1.1.1'); + blockList.addRange('10.0.0.1', '10.0.0.10'); + blockList.addSubnet('8592:757c:efae:4e45::', 64, 'ipv6'); + + const rulesCheck = [ + 'Subnet: IPv6 8592:757c:efae:4e45::/64', + 'Range: IPv4 10.0.0.1-10.0.0.10', + 'Address: IPv4 1.1.1.1' + ]; + assert.deepStrictEqual(blockList.rules, rulesCheck); + console.log(blockList); + + assert(blockList.check('1.1.1.1')); + assert(blockList.check('10.0.0.5')); + assert(blockList.check('::ffff:10.0.0.5', 'ipv6')); + assert(blockList.check('8592:757c:efae:4e45::f', 'ipv6')); + + assert(!blockList.check('123.123.123.123')); + assert(!blockList.check('8592:757c:efaf:4e45:fb5d:d62a:0d00:8e17', 'ipv6')); + assert(!blockList.check('::ffff:123.123.123.123', 'ipv6')); +} + +{ + // This test validates boundaries of non-aligned CIDR bit prefixes + const blockList = new BlockList(); + blockList.addSubnet('10.0.0.0', 27); + blockList.addSubnet('8592:757c:efaf::', 51, 'ipv6'); + + for (let n = 0; n <= 31; n++) + assert(blockList.check(`10.0.0.${n}`)); + assert(!blockList.check('10.0.0.32')); + + assert(blockList.check('8592:757c:efaf:0:0:0:0:0', 'ipv6')); + assert(blockList.check('8592:757c:efaf:1fff:ffff:ffff:ffff:ffff', 'ipv6')); + assert(!blockList.check('8592:757c:efaf:2fff:ffff:ffff:ffff:ffff', 'ipv6')); +} |