From 1a593eea74d8d953f6fea382d822a8d78e161457 Mon Sep 17 00:00:00 2001 From: theanarkh Date: Sun, 15 Dec 2024 22:19:27 +0800 Subject: [PATCH] dgram: support blocklist in udp PR-URL: https://github.com/nodejs/node/pull/56087 Reviewed-By: Luigi Pinca --- doc/api/dgram.md | 7 ++++ lib/dgram.js | 37 +++++++++++++++++++- test/parallel/test-dgram-blocklist.js | 49 +++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-dgram-blocklist.js diff --git a/doc/api/dgram.md b/doc/api/dgram.md index 2243b6abdea9bc..4d2ef8dea164f9 100644 --- a/doc/api/dgram.md +++ b/doc/api/dgram.md @@ -957,6 +957,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} diff --git a/lib/dgram.js b/lib/dgram.js index 09630b6c901181..b4c5db6439784a 100644 --- a/lib/dgram.js +++ b/lib/dgram.js @@ -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); } diff --git a/test/parallel/test-dgram-blocklist.js b/test/parallel/test-dgram-blocklist.js new file mode 100644 index 00000000000000..8af6522e7bd2d2 --- /dev/null +++ b/test/parallel/test-dgram-blocklist.js @@ -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(); + })); + })); +}