From 573bf3c16082c453d5d99edd2f2b4df5050192d3 Mon Sep 17 00:00:00 2001 From: Satya Rohith Date: Tue, 18 Jun 2024 16:16:13 +0530 Subject: [PATCH] feat(ext/node): add BlockList & SocketAddress classes (#24229) Closes https://github.com/denoland/deno/issues/24059 --- Cargo.lock | 10 + ext/node/Cargo.toml | 1 + ext/node/lib.rs | 10 + ext/node/ops/blocklist.rs | 290 +++++++++++++++++ ext/node/ops/mod.rs | 1 + ext/node/polyfills/internal/blocklist.mjs | 227 ++++++++++++++ ext/node/polyfills/internal/errors.ts | 14 +- ext/node/polyfills/net.ts | 6 +- tests/node_compat/config.jsonc | 2 + tests/node_compat/runner/TODO.md | 1 - .../test/parallel/test-blocklist.js | 291 ++++++++++++++++++ tests/unit_node/net_test.ts | 6 + 12 files changed, 845 insertions(+), 14 deletions(-) create mode 100644 ext/node/ops/blocklist.rs create mode 100644 ext/node/polyfills/internal/blocklist.mjs create mode 100644 tests/node_compat/test/parallel/test-blocklist.js diff --git a/Cargo.lock b/Cargo.lock index 9ba6698b3c..fd5fa22c30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1701,6 +1701,7 @@ dependencies = [ "http 1.1.0", "idna 0.3.0", "indexmap", + "ipnetwork", "k256", "lazy-regex", "libc", @@ -3571,6 +3572,15 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + [[package]] name = "is-docker" version = "0.2.0" diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml index 83ce490606..ecb618e484 100644 --- a/ext/node/Cargo.toml +++ b/ext/node/Cargo.toml @@ -41,6 +41,7 @@ home = "0.5.9" http.workspace = true idna = "0.3.0" indexmap.workspace = true +ipnetwork = "0.20.0" k256 = "0.13.1" lazy-regex.workspace = true libc.workspace = true diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 586439557f..3b9b8aa815 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -230,6 +230,15 @@ deno_core::extension!(deno_node, deps = [ deno_io, deno_fs ], parameters = [P: NodePermissions], ops = [ + ops::blocklist::op_socket_address_parse, + ops::blocklist::op_socket_address_get_serialization, + + ops::blocklist::op_blocklist_new, + ops::blocklist::op_blocklist_add_address, + ops::blocklist::op_blocklist_add_range, + ops::blocklist::op_blocklist_add_subnet, + ops::blocklist::op_blocklist_check, + ops::buffer::op_is_ascii, ops::buffer::op_is_utf8, ops::crypto::op_node_create_decipheriv, @@ -489,6 +498,7 @@ deno_core::extension!(deno_node, "internal_binding/uv.ts", "internal/assert.mjs", "internal/async_hooks.ts", + "internal/blocklist.mjs", "internal/buffer.mjs", "internal/child_process.ts", "internal/cli_table.ts", diff --git a/ext/node/ops/blocklist.rs b/ext/node/ops/blocklist.rs new file mode 100644 index 0000000000..ce32c14ba0 --- /dev/null +++ b/ext/node/ops/blocklist.rs @@ -0,0 +1,290 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::cell::RefCell; +use std::collections::HashSet; +use std::net::IpAddr; +use std::net::Ipv4Addr; +use std::net::Ipv6Addr; +use std::net::SocketAddr; + +use deno_core::anyhow::anyhow; +use deno_core::anyhow::bail; +use deno_core::error::AnyError; +use deno_core::op2; +use deno_core::OpState; + +use ipnetwork::IpNetwork; +use ipnetwork::Ipv4Network; +use ipnetwork::Ipv6Network; +use serde::Serialize; + +pub struct BlockListResource { + blocklist: RefCell, +} + +#[derive(Serialize)] +struct SocketAddressSerialization(String, String); + +#[op2(fast)] +pub fn op_socket_address_parse( + state: &mut OpState, + #[string] addr: &str, + #[smi] port: u16, + #[string] family: &str, +) -> Result { + let ip = addr.parse::()?; + let parsed: SocketAddr = SocketAddr::new(ip, port); + let parsed_ip_str = parsed.ip().to_string(); + let family_correct = family.eq_ignore_ascii_case("ipv4") && parsed.is_ipv4() + || family.eq_ignore_ascii_case("ipv6") && parsed.is_ipv6(); + + if family_correct { + let family_is_lowercase = family[..3].chars().all(char::is_lowercase); + if family_is_lowercase && parsed_ip_str == addr { + Ok(true) + } else { + state.put::(SocketAddressSerialization( + parsed_ip_str, + family.to_lowercase(), + )); + Ok(false) + } + } else { + Err(anyhow!("Invalid address")) + } +} + +#[op2] +#[serde] +pub fn op_socket_address_get_serialization( + state: &mut OpState, +) -> Result { + Ok(state.take::()) +} + +#[op2] +#[cppgc] +pub fn op_blocklist_new() -> BlockListResource { + let blocklist = BlockList::new(); + BlockListResource { + blocklist: RefCell::new(blocklist), + } +} + +#[op2(fast)] +pub fn op_blocklist_add_address( + #[cppgc] wrap: &BlockListResource, + #[string] addr: &str, +) -> Result<(), AnyError> { + wrap.blocklist.borrow_mut().add_address(addr) +} + +#[op2(fast)] +pub fn op_blocklist_add_range( + #[cppgc] wrap: &BlockListResource, + #[string] start: &str, + #[string] end: &str, +) -> Result { + wrap.blocklist.borrow_mut().add_range(start, end) +} + +#[op2(fast)] +pub fn op_blocklist_add_subnet( + #[cppgc] wrap: &BlockListResource, + #[string] addr: &str, + #[smi] prefix: u8, +) -> Result<(), AnyError> { + wrap.blocklist.borrow_mut().add_subnet(addr, prefix) +} + +#[op2(fast)] +pub fn op_blocklist_check( + #[cppgc] wrap: &BlockListResource, + #[string] addr: &str, + #[string] r#type: &str, +) -> Result { + wrap.blocklist.borrow().check(addr, r#type) +} + +struct BlockList { + rules: HashSet, +} + +impl BlockList { + pub fn new() -> Self { + BlockList { + rules: HashSet::new(), + } + } + + fn map_addr_add_network(&mut self, addr: IpAddr, prefix: Option) { + match addr { + IpAddr::V4(addr) => { + self.rules.insert(IpNetwork::V4( + Ipv4Network::new(addr, prefix.unwrap_or(32)).unwrap(), + )); + self.rules.insert(IpNetwork::V6( + Ipv6Network::new(addr.to_ipv6_mapped(), prefix.unwrap_or(128)) + .unwrap(), + )); + } + IpAddr::V6(addr) => { + if let Some(ipv4_mapped) = addr.to_ipv4_mapped() { + self.rules.insert(IpNetwork::V4( + Ipv4Network::new(ipv4_mapped, prefix.unwrap_or(32)).unwrap(), + )); + } + self.rules.insert(IpNetwork::V6( + Ipv6Network::new(addr, prefix.unwrap_or(128)).unwrap(), + )); + } + }; + } + + pub fn add_address(&mut self, address: &str) -> Result<(), AnyError> { + let ip: IpAddr = address.parse()?; + self.map_addr_add_network(ip, None); + Ok(()) + } + + pub fn add_range( + &mut self, + start: &str, + end: &str, + ) -> Result { + let start_ip: IpAddr = start.parse()?; + let end_ip: IpAddr = end.parse()?; + + match (start_ip, end_ip) { + (IpAddr::V4(start), IpAddr::V4(end)) => { + let start_u32: u32 = start.into(); + let end_u32: u32 = end.into(); + if end_u32 < start_u32 { + // Indicates invalid range. + return Ok(false); + } + for ip in start_u32..=end_u32 { + let addr: Ipv4Addr = ip.into(); + self.map_addr_add_network(IpAddr::V4(addr), None); + } + } + (IpAddr::V6(start), IpAddr::V6(end)) => { + let start_u128: u128 = start.into(); + let end_u128: u128 = end.into(); + if end_u128 < start_u128 { + // Indicates invalid range. + return Ok(false); + } + for ip in start_u128..=end_u128 { + let addr: Ipv6Addr = ip.into(); + self.map_addr_add_network(IpAddr::V6(addr), None); + } + } + _ => bail!("IP version mismatch between start and end addresses"), + } + Ok(true) + } + + pub fn add_subnet(&mut self, addr: &str, prefix: u8) -> Result<(), AnyError> { + let ip: IpAddr = addr.parse()?; + self.map_addr_add_network(ip, Some(prefix)); + Ok(()) + } + + pub fn check(&self, addr: &str, r#type: &str) -> Result { + let addr: IpAddr = addr.parse()?; + let family = r#type.to_lowercase(); + if family == "ipv4" && addr.is_ipv4() || family == "ipv6" && addr.is_ipv6() + { + Ok(self.rules.iter().any(|net| net.contains(addr))) + } else { + Err(anyhow!("Invalid address")) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_add_address() { + // Single IPv4 address + let mut block_list = BlockList::new(); + block_list.add_address("192.168.0.1").unwrap(); + assert!(block_list.check("192.168.0.1", "ipv4").unwrap()); + assert!(block_list.check("::ffff:c0a8:1", "ipv6").unwrap()); + + // Single IPv6 address + let mut block_list = BlockList::new(); + block_list.add_address("2001:db8::1").unwrap(); + assert!(block_list.check("2001:db8::1", "ipv6").unwrap()); + assert!(!block_list.check("192.168.0.1", "ipv4").unwrap()); + } + + #[test] + fn test_add_range() { + // IPv4 range + let mut block_list = BlockList::new(); + block_list.add_range("192.168.0.1", "192.168.0.3").unwrap(); + assert!(block_list.check("192.168.0.1", "ipv4").unwrap()); + assert!(block_list.check("192.168.0.2", "ipv4").unwrap()); + assert!(block_list.check("192.168.0.3", "ipv4").unwrap()); + assert!(block_list.check("::ffff:c0a8:1", "ipv6").unwrap()); + + // IPv6 range + let mut block_list = BlockList::new(); + block_list.add_range("2001:db8::1", "2001:db8::3").unwrap(); + assert!(block_list.check("2001:db8::1", "ipv6").unwrap()); + assert!(block_list.check("2001:db8::2", "ipv6").unwrap()); + assert!(block_list.check("2001:db8::3", "ipv6").unwrap()); + assert!(!block_list.check("192.168.0.1", "ipv4").unwrap()); + } + + #[test] + fn test_add_subnet() { + // IPv4 subnet + let mut block_list = BlockList::new(); + block_list.add_subnet("192.168.0.0", 24).unwrap(); + assert!(block_list.check("192.168.0.1", "ipv4").unwrap()); + assert!(block_list.check("192.168.0.255", "ipv4").unwrap()); + assert!(block_list.check("::ffff:c0a8:0", "ipv6").unwrap()); + + // IPv6 subnet + let mut block_list = BlockList::new(); + block_list.add_subnet("2001:db8::", 64).unwrap(); + assert!(block_list.check("2001:db8::1", "ipv6").unwrap()); + assert!(block_list.check("2001:db8::ffff", "ipv6").unwrap()); + assert!(!block_list.check("192.168.0.1", "ipv4").unwrap()); + } + + #[test] + fn test_check() { + // Check IPv4 presence + let mut block_list = BlockList::new(); + block_list.add_address("192.168.0.1").unwrap(); + assert!(block_list.check("192.168.0.1", "ipv4").unwrap()); + + // Check IPv6 presence + let mut block_list = BlockList::new(); + block_list.add_address("2001:db8::1").unwrap(); + assert!(block_list.check("2001:db8::1", "ipv6").unwrap()); + + // Check IPv4 not present + let block_list = BlockList::new(); + assert!(!block_list.check("192.168.0.1", "ipv4").unwrap()); + + // Check IPv6 not present + let block_list = BlockList::new(); + assert!(!block_list.check("2001:db8::1", "ipv6").unwrap()); + + // Check invalid IP version + let block_list = BlockList::new(); + assert!(block_list.check("192.168.0.1", "ipv6").is_err()); + + // Check invalid type + let mut block_list = BlockList::new(); + block_list.add_address("192.168.0.1").unwrap(); + assert!(block_list.check("192.168.0.1", "invalid_type").is_err()); + } +} diff --git a/ext/node/ops/mod.rs b/ext/node/ops/mod.rs index ae703e3f36..b51e23ac8a 100644 --- a/ext/node/ops/mod.rs +++ b/ext/node/ops/mod.rs @@ -1,5 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +pub mod blocklist; pub mod buffer; pub mod crypto; pub mod fs; diff --git a/ext/node/polyfills/internal/blocklist.mjs b/ext/node/polyfills/internal/blocklist.mjs new file mode 100644 index 0000000000..a9aba03b6f --- /dev/null +++ b/ext/node/polyfills/internal/blocklist.mjs @@ -0,0 +1,227 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright Joyent and Node contributors. All rights reserved. MIT license. + +import { primordials } from "ext:core/mod.js"; +import { + op_blocklist_add_address, + op_blocklist_add_range, + op_blocklist_add_subnet, + op_blocklist_check, + op_blocklist_new, + op_socket_address_get_serialization, + op_socket_address_parse, +} from "ext:core/ops"; + +import { + validateInt32, + validateObject, + validatePort, + validateString, + validateUint32, +} from "ext:deno_node/internal/validators.mjs"; +import { ERR_INVALID_ARG_VALUE } from "ext:deno_node/internal/errors.ts"; +import { customInspectSymbol } from "ext:deno_node/internal/util.mjs"; +import { inspect } from "ext:deno_node/internal/util/inspect.mjs"; + +const { Symbol } = primordials; + +const internalBlockList = Symbol("blocklist"); + +class BlockList { + constructor() { + this[internalBlockList] = op_blocklist_new(); + } + + [customInspectSymbol](depth, options) { + if (depth < 0) { + return this; + } + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `BlockList ${ + inspect({ + rules: [], // TODO(satyarohith): provide the actual rules + }, opts) + }`; + } + + addAddress(address, family = "ipv4") { + if (!SocketAddress.isSocketAddress(address)) { + validateString(address, "address"); + validateString(family, "family"); + new SocketAddress({ + address, + family, + }); + } else { + address = address.address; + } + op_blocklist_add_address(this[internalBlockList], address); + } + + addRange(start, end, family = "ipv4") { + if (!SocketAddress.isSocketAddress(start)) { + validateString(start, "start"); + validateString(family, "family"); + new SocketAddress({ + address: start, + family, + }); + } else { + start = start.address; + } + if (!SocketAddress.isSocketAddress(end)) { + validateString(end, "end"); + validateString(family, "family"); + new SocketAddress({ + address: end, + family, + }); + } else { + end = end.address; + } + const ret = op_blocklist_add_range(this[internalBlockList], start, end); + if (ret === false) { + throw new ERR_INVALID_ARG_VALUE("start", start, "must come before end"); + } + } + + addSubnet(network, prefix, family = "ipv4") { + if (!SocketAddress.isSocketAddress(network)) { + validateString(network, "network"); + validateString(family, "family"); + new SocketAddress({ + address: network, + family, + }); + } else { + network = network.address; + family = network.family; + } + switch (family) { + case "ipv4": + validateInt32(prefix, "prefix", 0, 32); + break; + case "ipv6": + validateInt32(prefix, "prefix", 0, 128); + break; + } + op_blocklist_add_subnet(this[internalBlockList], network, prefix); + } + + check(address, family = "ipv4") { + if (!SocketAddress.isSocketAddress(address)) { + validateString(address, "address"); + validateString(family, "family"); + try { + new SocketAddress({ + address, + family, + }); + } catch { + // Ignore the error. If it's not a valid address, return false. + return false; + } + } else { + family = address.family; + address = address.address; + } + try { + return op_blocklist_check(this[internalBlockList], address, family); + } catch (_) { + // Node API expects false as return value if the address is invalid. + // Example: `blocklist.check("1.1.1.1", "ipv6")` should return false. + return false; + } + } + + get rules() { + // TODO(satyarohith): return the actual rules + return []; + } +} + +const kDetail = Symbol("kDetail"); + +class SocketAddress { + static isSocketAddress(value) { + return value?.[kDetail] !== undefined; + } + + constructor(options = kEmptyObject) { + validateObject(options, "options"); + let { family = "ipv4" } = options; + const { + address = (family === "ipv4" ? "127.0.0.1" : "::"), + port = 0, + flowlabel = 0, + } = options; + + if (typeof family?.toLowerCase === "function") { + // deno-lint-ignore prefer-primordials + family = family.toLowerCase(); + } + switch (family) { + case "ipv4": + break; + case "ipv6": + break; + default: + throw new ERR_INVALID_ARG_VALUE("options.family", options.family); + } + + validateString(address, "options.address"); + validatePort(port, "options.port"); + validateUint32(flowlabel, "options.flowlabel", false); + + this[kDetail] = { + address, + port, + family, + flowlabel, + }; + const useInput = op_socket_address_parse( + address, + port, + family, + ); + if (!useInput) { + const { 0: address_, 1: family_ } = op_socket_address_get_serialization(); + this[kDetail].address = address_; + this[kDetail].family = family_; + } + } + + get address() { + return this[kDetail].address; + } + + get port() { + return this[kDetail].port; + } + + get family() { + return this[kDetail].family; + } + + get flowlabel() { + // TODO(satyarohith): Implement this in Rust. + // The flow label can be changed internally. + return this[kDetail].flowlabel; + } + + toJSON() { + return { + address: this.address, + port: this.port, + family: this.family, + flowlabel: this.flowlabel, + }; + } +} + +export { BlockList, SocketAddress }; diff --git a/ext/node/polyfills/internal/errors.ts b/ext/node/polyfills/internal/errors.ts index cb4119411a..6529e98947 100644 --- a/ext/node/polyfills/internal/errors.ts +++ b/ext/node/polyfills/internal/errors.ts @@ -667,9 +667,7 @@ function invalidArgTypeHelper(input: any) { return ` Received type ${typeof input} (${inspected})`; } -export class ERR_OUT_OF_RANGE extends RangeError { - code = "ERR_OUT_OF_RANGE"; - +export class ERR_OUT_OF_RANGE extends NodeRangeError { constructor( str: string, range: string, @@ -694,15 +692,7 @@ export class ERR_OUT_OF_RANGE extends RangeError { } msg += ` It must be ${range}. Received ${received}`; - super(msg); - - const { name } = this; - // Add the error code to the name to include it in the stack trace. - this.name = `${name} [${this.code}]`; - // Access the stack to generate the error message including the error code from the name. - this.stack; - // Reset the name to the actual name. - this.name = name; + super("ERR_OUT_OF_RANGE", msg); } } diff --git a/ext/node/polyfills/net.ts b/ext/node/polyfills/net.ts index 66b7735d95..6625ce7b50 100644 --- a/ext/node/polyfills/net.ts +++ b/ext/node/polyfills/net.ts @@ -24,6 +24,8 @@ // deno-lint-ignore-file prefer-primordials import { notImplemented } from "ext:deno_node/_utils.ts"; +import { BlockList, SocketAddress } from "ext:deno_node/internal/blocklist.mjs"; + import { EventEmitter } from "node:events"; import { isIP, @@ -2472,7 +2474,7 @@ export function createServer( return new Server(options, connectionListener); } -export { isIP, isIPv4, isIPv6 }; +export { BlockList, isIP, isIPv4, isIPv6, SocketAddress }; export default { _createServerHandle, @@ -2480,6 +2482,8 @@ export default { isIP, isIPv4, isIPv6, + BlockList, + SocketAddress, connect, createConnection, createServer, diff --git a/tests/node_compat/config.jsonc b/tests/node_compat/config.jsonc index 27cf6afb82..6a61c4e631 100644 --- a/tests/node_compat/config.jsonc +++ b/tests/node_compat/config.jsonc @@ -19,6 +19,7 @@ ], "parallel": [ "test-assert.js", + "test-blocklist.js", "test-buffer-alloc.js", "test-buffer-arraybuffer.js", "test-buffer-from.js", @@ -162,6 +163,7 @@ "test-assert-strict-exists.js", "test-assert.js", "test-bad-unicode.js", + "test-blocklist.js", "test-btoa-atob.js", "test-buffer-alloc.js", "test-buffer-arraybuffer.js", diff --git a/tests/node_compat/runner/TODO.md b/tests/node_compat/runner/TODO.md index e24c82b75c..4f397cc21e 100644 --- a/tests/node_compat/runner/TODO.md +++ b/tests/node_compat/runner/TODO.md @@ -222,7 +222,6 @@ NOTE: This file should not be manually edited. Please edit `tests/node_compat/co - [parallel/test-blob-file-backed.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-blob-file-backed.js) - [parallel/test-blob.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-blob.js) - [parallel/test-blocklist-clone.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-blocklist-clone.js) -- [parallel/test-blocklist.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-blocklist.js) - [parallel/test-bootstrap-modules.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-bootstrap-modules.js) - [parallel/test-broadcastchannel-custom-inspect.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-broadcastchannel-custom-inspect.js) - [parallel/test-buffer-backing-arraybuffer.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-buffer-backing-arraybuffer.js) diff --git a/tests/node_compat/test/parallel/test-blocklist.js b/tests/node_compat/test/parallel/test-blocklist.js new file mode 100644 index 0000000000..fd63f51ff0 --- /dev/null +++ b/tests/node_compat/test/parallel/test-blocklist.js @@ -0,0 +1,291 @@ +// deno-fmt-ignore-file +// deno-lint-ignore-file + +// Copyright Joyent and Node contributors. All rights reserved. MIT license. +// Taken from Node 18.12.1 +// This file is automatically generated by `tests/node_compat/runner/setup.ts`. Do not modify this file manually. + +'use strict'; + +require('../common'); + +const { + BlockList, + SocketAddress, +} = require('net'); +const assert = require('assert'); +const util = require('util'); + +{ + 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('::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(); + const sa1 = new SocketAddress({ address: '1.1.1.1' }); + const sa2 = new SocketAddress({ + address: '8592:757c:efae:4e45:fb5d:d62a:0d00:8e17', + family: 'ipv6' + }); + const sa3 = new SocketAddress({ address: '1.1.1.2' }); + + blockList.addAddress(sa1); + blockList.addAddress(sa2); + blockList.addAddress('::ffff:1.1.1.2', 'ipv6'); + + assert(blockList.check('1.1.1.1')); + assert(blockList.check(sa1)); + 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(sa2)); + + assert(blockList.check('::ffff:1.1.1.1', 'ipv6')); + assert(blockList.check('::ffff:1.1.1.1', 'IPV6')); + + assert(blockList.check('1.1.1.2')); + assert(blockList.check(sa3)); + + 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(); + const sa1 = new SocketAddress({ address: '1.1.1.1' }); + const sa2 = new SocketAddress({ address: '1.1.1.10' }); + const sa3 = new SocketAddress({ address: '::1', family: 'ipv6' }); + const sa4 = new SocketAddress({ address: '::f', family: 'ipv6' }); + + blockList.addRange(sa1, sa2); + blockList.addRange(sa3, sa4); + + 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(); + const sa1 = new SocketAddress({ address: '1.1.1.0' }); + const sa2 = new SocketAddress({ address: '1.1.1.1' }); + blockList.addSubnet(sa1, 16); + blockList.addSubnet('8592:757c:efae:4e45::', 64, 'ipv6'); + + assert(blockList.check('1.1.0.1')); + assert(blockList.check(sa2)); + 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'); // Case insensitive + + // 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); + + 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')); +} + +{ + // Regression test for https://github.com/nodejs/node/issues/39074 + const blockList = new BlockList(); + + blockList.addRange('10.0.0.2', '10.0.0.10'); + + // IPv4 checks against IPv4 range. + assert(blockList.check('10.0.0.2')); + assert(blockList.check('10.0.0.10')); + assert(!blockList.check('192.168.0.3')); + assert(!blockList.check('2.2.2.2')); + assert(!blockList.check('255.255.255.255')); + + // IPv6 checks against IPv4 range. + assert(blockList.check('::ffff:0a00:0002', 'ipv6')); + assert(blockList.check('::ffff:0a00:000a', 'ipv6')); + assert(!blockList.check('::ffff:c0a8:0003', 'ipv6')); + assert(!blockList.check('::ffff:0202:0202', 'ipv6')); + assert(!blockList.check('::ffff:ffff:ffff', 'ipv6')); +} + +{ + const blockList = new BlockList(); + assert.throws(() => blockList.addRange('1.1.1.2', '1.1.1.1'), /ERR_INVALID_ARG_VALUE/); +} + +{ + const blockList = new BlockList(); + assert.throws(() => blockList.addSubnet(1), /ERR_INVALID_ARG_TYPE/); + assert.throws(() => blockList.addSubnet('1.1.1.1', ''), + /ERR_INVALID_ARG_TYPE/); + assert.throws(() => blockList.addSubnet('1.1.1.1', NaN), /ERR_OUT_OF_RANGE/); + assert.throws(() => blockList.addSubnet('', 1, 1), /ERR_INVALID_ARG_TYPE/); + assert.throws(() => blockList.addSubnet('', 1, ''), /ERR_INVALID_ARG_VALUE/); + + assert.throws(() => blockList.addSubnet('1.1.1.1', -1, 'ipv4'), + /ERR_OUT_OF_RANGE/); + assert.throws(() => blockList.addSubnet('1.1.1.1', 33, 'ipv4'), + /ERR_OUT_OF_RANGE/); + + assert.throws(() => blockList.addSubnet('::', -1, 'ipv6'), + /ERR_OUT_OF_RANGE/); + assert.throws(() => blockList.addSubnet('::', 129, 'ipv6'), + /ERR_OUT_OF_RANGE/); +} + +{ + const blockList = new BlockList(); + assert.throws(() => blockList.check(1), /ERR_INVALID_ARG_TYPE/); + assert.throws(() => blockList.check('', 1), /ERR_INVALID_ARG_TYPE/); +} + +{ + const blockList = new BlockList(); + const ret = util.inspect(blockList, { depth: -1 }); + assert.strictEqual(ret, '[BlockList]'); +} + +{ + const blockList = new BlockList(); + const ret = util.inspect(blockList, { depth: null }); + assert(ret.includes('rules: []')); +} + +{ + // Test for https://github.com/nodejs/node/issues/43360 + const blocklist = new BlockList(); + blocklist.addSubnet('1.1.1.1', 32, 'ipv4'); + + assert(blocklist.check('1.1.1.1')); + assert(!blocklist.check('1.1.1.2')); + assert(!blocklist.check('2.3.4.5')); +} diff --git a/tests/unit_node/net_test.ts b/tests/unit_node/net_test.ts index e08b24c028..89a9fb6bab 100644 --- a/tests/unit_node/net_test.ts +++ b/tests/unit_node/net_test.ts @@ -200,3 +200,9 @@ Deno.test("[node/net] multiple Sockets should get correct server data", async () assertEquals(sockets[i].events, [`${i}`.repeat(3), `${i}`.repeat(3)]); } }); + +Deno.test("[node/net] BlockList doesn't leak resources", () => { + const blockList = new net.BlockList(); + blockList.addAddress("1.1.1.1"); + assert(blockList.check("1.1.1.1")); +});