Repository: nodejs/node
Pull request: net: support blockList in net.connect

The first PR for https://github.com/nodejs/node/issues/55000

- [x] `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes
- [x] tests and/or benchmarks are included
- [x] documentation is changed or added
- [x] commit message follows [commit guidelines](https://github.com/nodejs/node/blob/master/doc/guides/contributing/pull-requests.md#commit-message-guidelines)

Changed files:
filename: doc/api/errors.md
patch: @@ -2155,6 +2155,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.
 
+<a id="ERR_IP_BLOCKED"></a>
+
+### `ERR_IP_BLOCKED`
+
+IP is blocked by `net.BlockList`.
+
 <a id="ERR_LOADER_CHAIN_INCOMPLETE"></a>
 
 ### `ERR_LOADER_CHAIN_INCOMPLETE`

filename: doc/api/net.md
patch: @@ -1093,6 +1093,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:
 

filename: lib/internal/errors.js
patch: @@ -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' +

filename: lib/net.js
patch: @@ -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;

filename: test/parallel/test-net-blocklist.js
patch: @@ -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));
+}