Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions doc/api/net.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,38 @@ added:
* `value` {any} Any JS value
* Returns `true` if the `value` is a `net.BlockList`.

### `blockList.fromJSON(value)`

> Stability: 1 - Experimental

<!-- YAML
added: REPLACEME
-->

```js
const blockList = new net.BlockList();
const data = [
'Subnet: IPv4 192.168.1.0/24',
'Address: IPv4 10.0.0.5',
'Range: IPv4 192.168.2.1-192.168.2.10',
'Range: IPv4 10.0.0.1-10.0.0.10',
];
blockList.fromJSON(data);
blockList.fromJSON(JSON.stringify(data));
```

* `value` Blocklist.rules

### `blockList.toJSON()`

> Stability: 1 - Experimental

<!-- YAML
added: REPLACEME
-->

* Returns Blocklist.rules

## Class: `net.SocketAddress`

<!-- YAML
Expand Down
126 changes: 125 additions & 1 deletion lib/internal/blocklist.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
'use strict';

const {
ArrayIsArray,
Boolean,
JSONParse,
NumberParseInt,
ObjectSetPrototypeOf,
Symbol,
} = primordials;
Expand Down Expand Up @@ -32,6 +35,7 @@ const { owner_symbol } = internalBinding('symbols');

const {
ERR_INVALID_ARG_VALUE,
ERR_INVALID_ARG_TYPE,
} = require('internal/errors').codes;

const { validateInt32, validateString } = require('internal/validators');
Expand Down Expand Up @@ -139,10 +143,130 @@ class BlockList {
return Boolean(this[kHandle].check(address[kSocketAddressHandle]));
}

/*
* @param {string[]} data
* @example
* const data = [
* // IPv4 examples
* 'Subnet: IPv4 192.168.1.0/24',
* 'Address: IPv4 10.0.0.5',
* 'Range: IPv4 192.168.2.1-192.168.2.10',
* 'Range: IPv4 10.0.0.1-10.0.0.10',
*
* // IPv6 examples
* 'Subnet: IPv6 2001:0db8:85a3:0000:0000:8a2e:0370:7334/64',
* 'Address: IPv6 2001:0db8:85a3:0000:0000:8a2e:0370:7334',
* 'Range: IPv6 2001:0db8:85a3:0000:0000:8a2e:0370:7334-2001:0db8:85a3:0000:0000:8a2e:0370:7335',
* 'Subnet: IPv6 2001:db8:1234::/48',
* 'Address: IPv6 2001:db8:1234::1',
* 'Range: IPv6 2001:db8:1234::1-2001:db8:1234::10'
* ];
*/
#parseIPInfo(data) {
for (const item of data) {
if (item.includes('IPv4')) {
const subnetMatch = item.match(
/Subnet: IPv4 (\d{1,3}(?:\.\d{1,3}){3})\/(\d{1,2})/,
);
if (subnetMatch) {
const { 1: network, 2: prefix } = subnetMatch;
this.addSubnet(network, NumberParseInt(prefix));
continue;
}
const addressMatch = item.match(/Address: IPv4 (\d{1,3}(?:\.\d{1,3}){3})/);
if (addressMatch) {
const { 1: address } = addressMatch;
this.addAddress(address);
continue;
}

const rangeMatch = item.match(
/Range: IPv4 (\d{1,3}(?:\.\d{1,3}){3})-(\d{1,3}(?:\.\d{1,3}){3})/,
);
if (rangeMatch) {
const { 1: start, 2: end } = rangeMatch;
this.addRange(start, end);
continue;
}
}
// IPv6 parsing with support for compressed addresses
if (item.includes('IPv6')) {
// IPv6 subnet pattern: supports both full and compressed formats
// Examples:
// - 2001:0db8:85a3:0000:0000:8a2e:0370:7334/64 (full)
// - 2001:db8:85a3::8a2e:370:7334/64 (compressed)
// - 2001:db8:85a3::192.0.2.128/64 (mixed)
const ipv6SubnetMatch = item.match(
/Subnet: IPv6 ([0-9a-fA-F:]{1,39})\/([0-9]{1,3})/i,
);
if (ipv6SubnetMatch) {
const { 1: network, 2: prefix } = ipv6SubnetMatch;
this.addSubnet(network, NumberParseInt(prefix), 'ipv6');
continue;
}

// IPv6 address pattern: supports both full and compressed formats
// Examples:
// - 2001:0db8:85a3:0000:0000:8a2e:0370:7334 (full)
// - 2001:db8:85a3::8a2e:370:7334 (compressed)
// - 2001:db8:85a3::192.0.2.128 (mixed)
const ipv6AddressMatch = item.match(/Address: IPv6 ([0-9a-fA-F:]{1,39})/i);
if (ipv6AddressMatch) {
const { 1: address } = ipv6AddressMatch;
this.addAddress(address, 'ipv6');
continue;
}

// IPv6 range pattern: supports both full and compressed formats
// Examples:
// - 2001:0db8:85a3:0000:0000:8a2e:0370:7334-2001:0db8:85a3:0000:0000:8a2e:0370:7335 (full)
// - 2001:db8:85a3::8a2e:370:7334-2001:db8:85a3::8a2e:370:7335 (compressed)
// - 2001:db8:85a3::192.0.2.128-2001:db8:85a3::192.0.2.129 (mixed)
const ipv6RangeMatch = item.match(/Range: IPv6 ([0-9a-fA-F:]{1,39})-([0-9a-fA-F:]{1,39})/i);
if (ipv6RangeMatch) {
const { 1: start, 2: end } = ipv6RangeMatch;
this.addRange(start, end, 'ipv6');
continue;
}
}
}
}


toJSON() {
return this.rules;
}

fromJSON(data) {
// The data argument must be a string, or an array of strings that
// is JSON parseable.
if (ArrayIsArray(data)) {
for (const n of data) {
if (typeof n !== 'string') {
throw new ERR_INVALID_ARG_TYPE('data', ['string', 'string[]'], data);
}
}
} else if (typeof data !== 'string') {
throw new ERR_INVALID_ARG_TYPE('data', ['string', 'string[]'], data);
} else {
data = JSONParse(data);
if (!ArrayIsArray(data)) {
throw new ERR_INVALID_ARG_TYPE('data', ['string', 'string[]'], data);
}
for (const n of data) {
if (typeof n !== 'string') {
throw new ERR_INVALID_ARG_TYPE('data', ['string', 'string[]'], data);
}
}
}

this.#parseIPInfo(data);
}


get rules() {
return this[kHandle].getRules();
}

[kClone]() {
const handle = this[kHandle];
return {
Expand Down
72 changes: 72 additions & 0 deletions test/parallel/test-blocklist.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,75 @@ const util = require('util');
assert(BlockList.isBlockList(new BlockList()));
assert(!BlockList.isBlockList({}));
}

// Test exporting and importing the rule list to/from JSON
{
const ruleList = [
'Address: IPv4 10.0.0.5',
'Address: IPv6 ::',
'Subnet: IPv4 192.168.1.0/24',
'Subnet: IPv6 8592:757c:efae:4e45::/64',
];

const test2 = new BlockList();
const test3 = new BlockList();
const test4 = new BlockList();
const test5 = new BlockList();

const bl = new BlockList();
bl.addAddress('10.0.0.5');
bl.addAddress('::', 'ipv6');
bl.addSubnet('192.168.1.0', 24);
bl.addSubnet('8592:757c:efae:4e45::', 64, 'ipv6');

// Test invalid inputs (input to fromJSON must be an array of
// string rules or a serialized json string of an array of
// string rules.
[
1, null, Symbol(), [1, 2, 3], '123', [Symbol()], new Map(),
].forEach((i) => {
assert.throws(() => test2.fromJSON(i), {
code: 'ERR_INVALID_ARG_TYPE',
});
});

// Invalid rules are ignored.
test2.fromJSON(['1', '2', '3']);
assert.deepStrictEqual(test2.rules, []);

// Direct output from toJSON method works
test2.fromJSON(bl.toJSON());
assert.deepStrictEqual(test2.rules.sort(), ruleList);

// JSON stringified output works
test3.fromJSON(JSON.stringify(bl));
assert.deepStrictEqual(test3.rules.sort(), ruleList);

// A raw array works
test4.fromJSON(ruleList);
assert.deepStrictEqual(test4.rules.sort(), ruleList);

// Individual rules work
ruleList.forEach((item) => {
test5.fromJSON([item]);
});
assert.deepStrictEqual(test5.rules.sort(), ruleList);

// Each of the created blocklists should handle the checks identically.
[
['10.0.0.5', 'ipv4', true],
['10.0.0.6', 'ipv4', false],
['::', 'ipv6', true],
['::1', 'ipv6', false],
['192.168.1.0', 'ipv4', true],
['193.168.1.0', 'ipv4', false],
['8592:757c:efae:4e45::', 'ipv6', true],
['1111:1111:1111:1111::', 'ipv6', false],
].forEach((i) => {
assert.strictEqual(bl.check(i[0], i[1]), i[2]);
assert.strictEqual(test2.check(i[0], i[1]), i[2]);
assert.strictEqual(test3.check(i[0], i[1]), i[2]);
assert.strictEqual(test4.check(i[0], i[1]), i[2]);
assert.strictEqual(test5.check(i[0], i[1]), i[2]);
});
}
1 change: 0 additions & 1 deletion test/parallel/test-net-blocklist.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
const common = require('../common');
const net = require('net');
const assert = require('assert');

const blockList = new net.BlockList();
blockList.addAddress('127.0.0.1');
blockList.addAddress('127.0.0.2');
Expand Down
Loading