diff --git a/doc/api/errors.md b/doc/api/errors.md index ea22c9306fdb47..f50a5a10a52e78 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -2153,6 +2153,12 @@ An attempt was made to open an IPC communication channel with a synchronously forked Node.js process. See the documentation for the [`child_process`][] module for more information. + + +### `ERR_IP_BLOCKED` + +IP is blocked by `net.BlockList`. + ### `ERR_LOADER_CHAIN_INCOMPLETE` diff --git a/doc/api/net.md b/doc/api/net.md index de1f5b095ea40e..aedfd9b4a4d3e5 100644 --- a/doc/api/net.md +++ b/doc/api/net.md @@ -1089,6 +1089,8 @@ For TCP connections, available `options` are: * `noDelay` {boolean} If set to `true`, it disables the use of Nagle's algorithm immediately after the socket is established. **Default:** `false`. * `port` {number} Required. Port the socket should connect to. +* `blockList` {net.BlockList} `blockList` can be used for disabling outbound + access to specific IP addresses, IP ranges, or IP subnets. For [IPC][] connections, available `options` are: diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 214bed12e62d19..d990f8d5a106aa 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1551,6 +1551,9 @@ E('ERR_IPC_CHANNEL_CLOSED', 'Channel closed', Error); E('ERR_IPC_DISCONNECTED', 'IPC channel is already disconnected', Error); E('ERR_IPC_ONE_PIPE', 'Child process can have only one IPC pipe', Error); E('ERR_IPC_SYNC_FORK', 'IPC cannot be used with synchronous forks', Error); +E('ERR_IP_BLOCKED', function(ip) { + return `IP(${ip}) is blocked by net.BlockList`; +}, Error); E( 'ERR_LOADER_CHAIN_INCOMPLETE', '"%s" did not call the next hook in its chain and did not' + diff --git a/lib/net.js b/lib/net.js index ae4d536cd9840b..b8c0057bfd1a6c 100644 --- a/lib/net.js +++ b/lib/net.js @@ -105,6 +105,7 @@ const { ERR_INVALID_FD_TYPE, ERR_INVALID_HANDLE_TYPE, ERR_INVALID_IP_ADDRESS, + ERR_IP_BLOCKED, ERR_MISSING_ARGS, ERR_SERVER_ALREADY_LISTEN, ERR_SERVER_NOT_RUNNING, @@ -510,6 +511,12 @@ function Socket(options) { // Used after `.destroy()` this[kBytesRead] = 0; this[kBytesWritten] = 0; + if (options.blockList) { + if (!module.exports.BlockList.isBlockList(options.blockList)) { + throw new ERR_INVALID_ARG_TYPE('options.blockList', 'net.BlockList', options.blockList); + } + this.blockList = options.blockList; + } } ObjectSetPrototypeOf(Socket.prototype, stream.Duplex.prototype); ObjectSetPrototypeOf(Socket, stream.Duplex); @@ -1073,6 +1080,10 @@ function internalConnect( self.emit('connectionAttempt', address, port, addressType); if (addressType === 6 || addressType === 4) { + if (self.blockList?.check(address, `ipv${addressType}`)) { + self.destroy(new ERR_IP_BLOCKED(address)); + return; + } const req = new TCPConnectWrap(); req.oncomplete = afterConnect; req.address = address; @@ -1162,6 +1173,14 @@ function internalConnectMultiple(context, canceled) { } } + if (self.blockList?.check(address, `ipv${addressType}`)) { + const ex = new ERR_IP_BLOCKED(address); + ArrayPrototypePush(context.errors, ex); + self.emit('connectionAttemptFailed', address, port, addressType, ex); + internalConnectMultiple(context); + return; + } + debug('connect/multiple: attempting to connect to %s:%d (addressType: %d)', address, port, addressType); self.emit('connectionAttempt', address, port, addressType); @@ -1792,8 +1811,7 @@ function Server(options, connectionListener) { this.keepAliveInitialDelay = ~~(options.keepAliveInitialDelay / 1000); this.highWaterMark = options.highWaterMark ?? getDefaultHighWaterMark(); if (options.blockList) { - // TODO: use BlockList.isBlockList (https://github.com/nodejs/node/pull/56078) - if (!(options.blockList instanceof module.exports.BlockList)) { + if (!module.exports.BlockList.isBlockList(options.blockList)) { throw new ERR_INVALID_ARG_TYPE('options.blockList', 'net.BlockList', options.blockList); } this.blockList = options.blockList; diff --git a/test/parallel/test-net-blocklist.js b/test/parallel/test-net-blocklist.js new file mode 100644 index 00000000000000..901b9a4dfb7b02 --- /dev/null +++ b/test/parallel/test-net-blocklist.js @@ -0,0 +1,68 @@ +'use strict'; + +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'); + +function check(err) { + assert.ok(err.code === 'ERR_IP_BLOCKED', err); +} + +// Connect without calling dns.lookup +{ + const socket = net.connect({ + port: 9999, + host: '127.0.0.1', + blockList, + }); + socket.on('error', common.mustCall(check)); +} + +// Connect with single IP returned by dns.lookup +{ + const socket = net.connect({ + port: 9999, + host: 'localhost', + blockList, + lookup: function(_, __, cb) { + cb(null, '127.0.0.1', 4); + }, + autoSelectFamily: false, + }); + + socket.on('error', common.mustCall(check)); +} + +// Connect with autoSelectFamily and single IP +{ + const socket = net.connect({ + port: 9999, + host: 'localhost', + blockList, + lookup: function(_, __, cb) { + cb(null, [{ address: '127.0.0.1', family: 4 }]); + }, + autoSelectFamily: true, + }); + + socket.on('error', common.mustCall(check)); +} + +// Connect with autoSelectFamily and multiple IPs +{ + const socket = net.connect({ + port: 9999, + host: 'localhost', + blockList, + lookup: function(_, __, cb) { + cb(null, [{ address: '127.0.0.1', family: 4 }, { address: '127.0.0.2', family: 4 }]); + }, + autoSelectFamily: true, + }); + + socket.on('error', common.mustCall(check)); +}