Repository: nodejs/node
Pull request: dgram: support blocklist in udp



- [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/dgram.md
patch: @@ -959,6 +959,13 @@ changes:
   * `sendBufferSize` {number} Sets the `SO_SNDBUF` socket value.
   * `lookup` {Function} Custom lookup function. **Default:** [`dns.lookup()`][].
   * `signal` {AbortSignal} An AbortSignal that may be used to close a socket.
+  * `receiveBlockList` {net.BlockList} `receiveBlockList` can be used for discarding
+    inbound datagram to specific IP addresses, IP ranges, or IP subnets. This does not
+    work if the server is behind a reverse proxy, NAT, etc. because the address
+    checked against the blocklist is the address of the proxy, or the one
+    specified by the NAT.
+  * `sendBlockList` {net.BlockList} `sendBlockList` can be used for disabling outbound
+    access to specific IP addresses, IP ranges, or IP subnets.
 * `callback` {Function} Attached as a listener for `'message'` events. Optional.
 * Returns: {dgram.Socket}
 

filename: lib/dgram.js
patch: @@ -41,6 +41,7 @@ const {
     ERR_BUFFER_OUT_OF_BOUNDS,
     ERR_INVALID_ARG_TYPE,
     ERR_INVALID_FD_TYPE,
+    ERR_IP_BLOCKED,
     ERR_MISSING_ARGS,
     ERR_SOCKET_ALREADY_BOUND,
     ERR_SOCKET_BAD_BUFFER_SIZE,
@@ -55,6 +56,7 @@ const {
   _createSocketHandle,
   newHandle,
 } = require('internal/dgram');
+const { isIP } = require('internal/net');
 const {
   isInt32,
   validateAbortSignal,
@@ -99,12 +101,18 @@ let _cluster = null;
 function lazyLoadCluster() {
   return _cluster ??= require('cluster');
 }
+let _blockList = null;
+function lazyLoadBlockList() {
+  return _blockList ??= require('internal/blocklist').BlockList;
+}
 
 function Socket(type, listener) {
   FunctionPrototypeCall(EventEmitter, this);
   let lookup;
   let recvBufferSize;
   let sendBufferSize;
+  let receiveBlockList;
+  let sendBlockList;
 
   let options;
   if (type !== null && typeof type === 'object') {
@@ -119,6 +127,18 @@ function Socket(type, listener) {
     }
     recvBufferSize = options.recvBufferSize;
     sendBufferSize = options.sendBufferSize;
+    if (options.receiveBlockList) {
+      if (!lazyLoadBlockList().isBlockList(options.receiveBlockList)) {
+        throw new ERR_INVALID_ARG_TYPE('options.receiveBlockList', 'net.BlockList', options.receiveBlockList);
+      }
+      receiveBlockList = options.receiveBlockList;
+    }
+    if (options.sendBlockList) {
+      if (!lazyLoadBlockList().isBlockList(options.sendBlockList)) {
+        throw new ERR_INVALID_ARG_TYPE('options.sendBlockList', 'net.BlockList', options.sendBlockList);
+      }
+      sendBlockList = options.sendBlockList;
+    }
   }
 
   const handle = newHandle(type, lookup);
@@ -141,6 +161,8 @@ function Socket(type, listener) {
     ipv6Only: options?.ipv6Only,
     recvBufferSize,
     sendBufferSize,
+    receiveBlockList,
+    sendBlockList,
   };
 
   if (options?.signal !== undefined) {
@@ -439,7 +461,9 @@ function doConnect(ex, self, ip, address, port, callback) {
   const state = self[kStateSymbol];
   if (!state.handle)
     return;
-
+  if (!ex && state.sendBlockList?.check(ip, `ipv${isIP(ip)}`)) {
+    ex = new ERR_IP_BLOCKED(ip);
+  }
   if (!ex) {
     const err = state.handle.connect(ip, port);
     if (err) {
@@ -703,6 +727,13 @@ function doSend(ex, self, ip, list, address, port, callback) {
     return;
   }
 
+  if (ip && state.sendBlockList?.check(ip, `ipv${isIP(ip)}`)) {
+    if (callback) {
+      process.nextTick(callback, new ERR_IP_BLOCKED(ip));
+    }
+    return;
+  }
+
   const req = new SendWrap();
   req.list = list;  // Keep reference alive.
   req.address = address;
@@ -951,6 +982,10 @@ function onMessage(nread, handle, buf, rinfo) {
   if (nread < 0) {
     return self.emit('error', new ErrnoException(nread, 'recvmsg'));
   }
+  if (self[kStateSymbol]?.receiveBlockList?.check(rinfo.address,
+                                                  rinfo.family?.toLocaleLowerCase())) {
+    return;
+  }
   rinfo.size = buf.length; // compatibility
   self.emit('message', buf, rinfo);
 }

filename: test/parallel/test-dgram-blocklist.js
patch: @@ -0,0 +1,49 @@
+'use strict';
+const common = require('../common');
+const assert = require('assert');
+const dgram = require('dgram');
+const net = require('net');
+
+{
+  const blockList = new net.BlockList();
+  blockList.addAddress(common.localhostIPv4);
+
+  const connectSocket = dgram.createSocket({ type: 'udp4', sendBlockList: blockList });
+  connectSocket.connect(9999, common.localhostIPv4, common.mustCall((err) => {
+    assert.ok(err.code === 'ERR_IP_BLOCKED', err);
+    connectSocket.close();
+  }));
+}
+
+{
+  const blockList = new net.BlockList();
+  blockList.addAddress(common.localhostIPv4);
+  const sendSocket = dgram.createSocket({ type: 'udp4', sendBlockList: blockList });
+  sendSocket.send('hello', 9999, common.localhostIPv4, common.mustCall((err) => {
+    assert.ok(err.code === 'ERR_IP_BLOCKED', err);
+    sendSocket.close();
+  }));
+}
+
+{
+  const blockList = new net.BlockList();
+  blockList.addAddress(common.localhostIPv4);
+  const receiveSocket = dgram.createSocket({ type: 'udp4', receiveBlockList: blockList });
+  // Hack to close the socket
+  const check = blockList.check;
+  blockList.check = function() {
+    process.nextTick(() => {
+      receiveSocket.close();
+    });
+    return check.apply(this, arguments);
+  };
+  receiveSocket.on('message', common.mustNotCall());
+  receiveSocket.bind(0, common.localhostIPv4, common.mustCall(() => {
+    const addressInfo = receiveSocket.address();
+    const client = dgram.createSocket('udp4');
+    client.send('hello', addressInfo.port, addressInfo.address, common.mustCall((err) => {
+      assert.ok(!err);
+      client.close();
+    }));
+  }));
+}