From 33a25b29a4d654f5c2a5c74725862bccb2fcccfb Mon Sep 17 00:00:00 2001 From: Ouyang Yadong Date: Sun, 21 Oct 2018 15:59:38 +0800 Subject: [PATCH] net,dgram: add ipv6Only option for net and dgram For TCP servers, the dual-stack support is enable by default, i.e. binding host "::" will also make "0.0.0.0" bound. This commit add ipv6Only option in `net.Server.listen()` and `dgram.createSocket()` methods which allows to disable dual-stack support. Support for cluster module is also provided in this commit. Fixes: https://github.com/nodejs/node/issues/17664 PR-URL: https://github.com/nodejs/node/pull/23798 Reviewed-By: Ben Noordhuis Reviewed-By: James M Snell --- doc/api/dgram.md | 6 ++ doc/api/net.md | 7 +++ lib/dgram.js | 9 ++- lib/internal/cluster/round_robin_handle.js | 14 +++-- lib/internal/cluster/shared_handle.js | 2 +- lib/net.js | 35 ++++++----- src/tcp_wrap.cc | 5 +- src/udp_wrap.cc | 6 ++ test/parallel/test-cluster-dgram-ipv6only.js | 51 ++++++++++++++++ .../test-cluster-net-listen-ipv6only-false.js | 55 ++++++++++++++++++ .../test-cluster-net-listen-ipv6only-none.js | 58 +++++++++++++++++++ .../test-cluster-net-listen-ipv6only-rr.js | 58 +++++++++++++++++++ test/parallel/test-dgram-ipv6only.js | 33 +++++++++++ test/parallel/test-net-listen-ipv6only.js | 30 ++++++++++ 14 files changed, 347 insertions(+), 22 deletions(-) create mode 100644 test/parallel/test-cluster-dgram-ipv6only.js create mode 100644 test/parallel/test-cluster-net-listen-ipv6only-false.js create mode 100644 test/parallel/test-cluster-net-listen-ipv6only-none.js create mode 100644 test/parallel/test-cluster-net-listen-ipv6only-rr.js create mode 100644 test/parallel/test-dgram-ipv6only.js create mode 100644 test/parallel/test-net-listen-ipv6only.js diff --git a/doc/api/dgram.md b/doc/api/dgram.md index e263627723a879..31abaf450c595b 100644 --- a/doc/api/dgram.md +++ b/doc/api/dgram.md @@ -601,6 +601,9 @@ changes: pr-url: https://github.com/nodejs/node/pull/13623 description: The `recvBufferSize` and `sendBufferSize` options are supported now. + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/23798 + description: The `ipv6Only` option is supported. --> * `options` {Object} Available options are: @@ -609,6 +612,9 @@ changes: * `reuseAddr` {boolean} When `true` [`socket.bind()`][] will reuse the address, even if another process has already bound a socket on it. **Default:** `false`. + * `ipv6Only` {boolean} Setting `ipv6Only` to `true` will + disable dual-stack support, i.e., binding to address `::` won't make + `0.0.0.0` be bound. **Default:** `false`. * `recvBufferSize` {number} - Sets the `SO_RCVBUF` socket value. * `sendBufferSize` {number} - Sets the `SO_SNDBUF` socket value. * `lookup` {Function} Custom lookup function. **Default:** [`dns.lookup()`][]. diff --git a/doc/api/net.md b/doc/api/net.md index b925245e7a338b..35fb0a6171382e 100644 --- a/doc/api/net.md +++ b/doc/api/net.md @@ -252,6 +252,10 @@ Listening on a file descriptor is not supported on Windows. #### server.listen(options[, callback]) * `options` {Object} Required. Supports the following properties: @@ -266,6 +270,9 @@ added: v0.11.14 for all users. **Default:** `false` * `writableAll` {boolean} For IPC servers makes the pipe writable for all users. **Default:** `false` + * `ipv6Only` {boolean} For TCP servers, setting `ipv6Only` to `true` will + disable dual-stack support, i.e., binding to host `::` won't make + `0.0.0.0` be bound. **Default:** `false`. * `callback` {Function} Common parameter of [`server.listen()`][] functions. * Returns: {net.Server} diff --git a/lib/dgram.js b/lib/dgram.js index 549b6dd738d0fe..55662313d640cd 100644 --- a/lib/dgram.js +++ b/lib/dgram.js @@ -54,7 +54,11 @@ const { } = require('internal/async_hooks'); const { UV_UDP_REUSEADDR } = internalBinding('constants').os; -const { UDP, SendWrap } = internalBinding('udp_wrap'); +const { + constants: { UV_UDP_IPV6ONLY }, + UDP, + SendWrap +} = internalBinding('udp_wrap'); const BIND_STATE_UNBOUND = 0; const BIND_STATE_BINDING = 1; @@ -99,6 +103,7 @@ function Socket(type, listener) { bindState: BIND_STATE_UNBOUND, queue: undefined, reuseAddr: options && options.reuseAddr, // Use UV_UDP_REUSEADDR if true. + ipv6Only: options && options.ipv6Only, recvBufferSize, sendBufferSize }; @@ -270,6 +275,8 @@ Socket.prototype.bind = function(port_, address_ /* , callback */) { var flags = 0; if (state.reuseAddr) flags |= UV_UDP_REUSEADDR; + if (state.ipv6Only) + flags |= UV_UDP_IPV6ONLY; if (cluster.isWorker && !exclusive) { bindServerHandle(this, { diff --git a/lib/internal/cluster/round_robin_handle.js b/lib/internal/cluster/round_robin_handle.js index 6b2bc372545cd9..95f33ad1d09907 100644 --- a/lib/internal/cluster/round_robin_handle.js +++ b/lib/internal/cluster/round_robin_handle.js @@ -3,10 +3,11 @@ const assert = require('assert'); const net = require('net'); const { sendHelper } = require('internal/cluster/utils'); const uv = internalBinding('uv'); +const { constants } = internalBinding('tcp_wrap'); module.exports = RoundRobinHandle; -function RoundRobinHandle(key, address, port, addressType, fd) { +function RoundRobinHandle(key, address, port, addressType, fd, flags) { this.key = key; this.all = new Map(); this.free = []; @@ -16,9 +17,14 @@ function RoundRobinHandle(key, address, port, addressType, fd) { if (fd >= 0) this.server.listen({ fd }); - else if (port >= 0) - this.server.listen(port, address); - else + else if (port >= 0) { + this.server.listen({ + port, + host: address, + // Currently, net module only supports `ipv6Only` option in `flags`. + ipv6Only: Boolean(flags & constants.UV_TCP_IPV6ONLY), + }); + } else this.server.listen(address); // UNIX socket path. this.server.once('listening', () => { diff --git a/lib/internal/cluster/shared_handle.js b/lib/internal/cluster/shared_handle.js index 0bb8c44f5d630b..0b5f1531931b6c 100644 --- a/lib/internal/cluster/shared_handle.js +++ b/lib/internal/cluster/shared_handle.js @@ -15,7 +15,7 @@ function SharedHandle(key, address, port, addressType, fd, flags) { if (addressType === 'udp4' || addressType === 'udp6') rval = dgram._createSocketHandle(address, port, addressType, fd, flags); else - rval = net._createServerHandle(address, port, addressType, fd); + rval = net._createServerHandle(address, port, addressType, fd, flags); if (typeof rval === 'number') this.errno = rval; diff --git a/lib/net.js b/lib/net.js index 01cfed98e872a4..25767b25741644 100644 --- a/lib/net.js +++ b/lib/net.js @@ -97,6 +97,10 @@ const { function noop() {} +function getFlags(ipv6Only) { + return ipv6Only === true ? TCPConstants.UV_TCP_IPV6ONLY : 0; +} + function createHandle(fd, is_server) { validateInt32(fd, 'fd', 0); const type = TTYWrap.guessHandleType(fd); @@ -798,7 +802,7 @@ function checkBindError(err, port, handle) { function internalConnect( - self, address, port, addressType, localAddress, localPort) { + self, address, port, addressType, localAddress, localPort, flags) { // TODO return promise from Socket.prototype.connect which // wraps _connectReq. @@ -812,7 +816,7 @@ function internalConnect( err = self._handle.bind(localAddress, localPort); } else { // addressType === 6 localAddress = localAddress || '::'; - err = self._handle.bind6(localAddress, localPort); + err = self._handle.bind6(localAddress, localPort, flags); } debug('binding to localAddress: %s and localPort: %d (addressType: %d)', localAddress, localPort, addressType); @@ -1148,7 +1152,7 @@ util.inherits(Server, EventEmitter); function toNumber(x) { return (x = Number(x)) >= 0 ? x : false; } // Returns handle if it can be created, or error code if it can't -function createServerHandle(address, port, addressType, fd) { +function createServerHandle(address, port, addressType, fd, flags) { var err = 0; // assign handle in listen, and clean up if bind or listen fails var handle; @@ -1187,14 +1191,14 @@ function createServerHandle(address, port, addressType, fd) { debug('bind to', address || 'any'); if (!address) { // Try binding to ipv6 first - err = handle.bind6('::', port); + err = handle.bind6('::', port, flags); if (err) { handle.close(); // Fallback to ipv4 return createServerHandle('0.0.0.0', port); } } else if (addressType === 6) { - err = handle.bind6(address, port); + err = handle.bind6(address, port, flags); } else { err = handle.bind(address, port); } @@ -1208,7 +1212,7 @@ function createServerHandle(address, port, addressType, fd) { return handle; } -function setupListenHandle(address, port, addressType, backlog, fd) { +function setupListenHandle(address, port, addressType, backlog, fd, flags) { debug('setupListenHandle', address, port, addressType, backlog, fd); // If there is not yet a handle, we need to create one and bind. @@ -1222,7 +1226,7 @@ function setupListenHandle(address, port, addressType, backlog, fd) { // Try to bind to the unspecified IPv6 address, see if IPv6 is available if (!address && typeof fd !== 'number') { - rval = createServerHandle('::', port, 6, fd); + rval = createServerHandle('::', port, 6, fd, flags); if (typeof rval === 'number') { rval = null; @@ -1235,7 +1239,7 @@ function setupListenHandle(address, port, addressType, backlog, fd) { } if (rval === null) - rval = createServerHandle(address, port, addressType, fd); + rval = createServerHandle(address, port, addressType, fd, flags); if (typeof rval === 'number') { var error = uvExceptionWithHostPort(rval, 'listen', address, port); @@ -1294,7 +1298,7 @@ function emitListeningNT(self) { function listenInCluster(server, address, port, addressType, - backlog, fd, exclusive) { + backlog, fd, exclusive, flags) { exclusive = !!exclusive; if (cluster === undefined) cluster = require('cluster'); @@ -1303,7 +1307,7 @@ function listenInCluster(server, address, port, addressType, // Will create a new handle // _listen2 sets up the listened handle, it is still named like this // to avoid breaking code that wraps this method - server._listen2(address, port, addressType, backlog, fd); + server._listen2(address, port, addressType, backlog, fd, flags); return; } @@ -1312,7 +1316,7 @@ function listenInCluster(server, address, port, addressType, port: port, addressType: addressType, fd: fd, - flags: 0 + flags, }; // Get the master's server handle, and listen on it @@ -1330,7 +1334,7 @@ function listenInCluster(server, address, port, addressType, server._handle = handle; // _listen2 sets up the listened handle, it is still named like this // to avoid breaking code that wraps this method - server._listen2(address, port, addressType, backlog, fd); + server._listen2(address, port, addressType, backlog, fd, flags); } } @@ -1353,6 +1357,7 @@ Server.prototype.listen = function(...args) { toNumber(args.length > 2 && args[2]); // (port, host, backlog) options = options._handle || options.handle || options; + const flags = getFlags(options.ipv6Only); // (handle[, backlog][, cb]) where handle is an object with a handle if (options instanceof TCP) { this._handle = options; @@ -1387,7 +1392,7 @@ Server.prototype.listen = function(...args) { // start TCP server listening on host:port if (options.host) { lookupAndListen(this, options.port | 0, options.host, backlog, - options.exclusive); + options.exclusive, flags); } else { // Undefined host, listens on unspecified address // Default addressType 4 will be used to search for master server listenInCluster(this, null, options.port | 0, 4, @@ -1434,7 +1439,7 @@ Server.prototype.listen = function(...args) { throw new ERR_INVALID_OPT_VALUE('options', util.inspect(options)); }; -function lookupAndListen(self, port, address, backlog, exclusive) { +function lookupAndListen(self, port, address, backlog, exclusive, flags) { if (dns === undefined) dns = require('dns'); dns.lookup(address, function doListen(err, ip, addressType) { if (err) { @@ -1442,7 +1447,7 @@ function lookupAndListen(self, port, address, backlog, exclusive) { } else { addressType = ip ? addressType : 4; listenInCluster(self, ip, port, addressType, - backlog, undefined, exclusive); + backlog, undefined, exclusive, flags); } }); } diff --git a/src/tcp_wrap.cc b/src/tcp_wrap.cc index e1316b42cd206b..504fda3de6cf95 100644 --- a/src/tcp_wrap.cc +++ b/src/tcp_wrap.cc @@ -127,6 +127,7 @@ void TCPWrap::Initialize(Local target, Local constants = Object::New(env->isolate()); NODE_DEFINE_CONSTANT(constants, SOCKET); NODE_DEFINE_CONSTANT(constants, SERVER); + NODE_DEFINE_CONSTANT(constants, UV_TCP_IPV6ONLY); target->Set(context, env->constants_string(), constants).FromJust(); @@ -252,13 +253,15 @@ void TCPWrap::Bind6(const FunctionCallbackInfo& args) { Environment* env = wrap->env(); node::Utf8Value ip6_address(env->isolate(), args[0]); int port; + unsigned int flags; if (!args[1]->Int32Value(env->context()).To(&port)) return; + if (!args[2]->Uint32Value(env->context()).To(&flags)) return; sockaddr_in6 addr; int err = uv_ip6_addr(*ip6_address, port, &addr); if (err == 0) { err = uv_tcp_bind(&wrap->handle_, reinterpret_cast(&addr), - 0); + flags); } args.GetReturnValue().Set(err); } diff --git a/src/udp_wrap.cc b/src/udp_wrap.cc index 4c7072f2a2b981..b4c859e5947b80 100644 --- a/src/udp_wrap.cc +++ b/src/udp_wrap.cc @@ -149,6 +149,12 @@ void UDPWrap::Initialize(Local target, target->Set(env->context(), sendWrapString, swt->GetFunction(env->context()).ToLocalChecked()).FromJust(); + + Local constants = Object::New(env->isolate()); + NODE_DEFINE_CONSTANT(constants, UV_UDP_IPV6ONLY); + target->Set(context, + env->constants_string(), + constants).FromJust(); } diff --git a/test/parallel/test-cluster-dgram-ipv6only.js b/test/parallel/test-cluster-dgram-ipv6only.js new file mode 100644 index 00000000000000..0906baec929d21 --- /dev/null +++ b/test/parallel/test-cluster-dgram-ipv6only.js @@ -0,0 +1,51 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasIPv6) + common.skip('no IPv6 support'); +if (common.isWindows) + common.skip('dgram clustering is currently not supported on windows.'); + +const assert = require('assert'); +const cluster = require('cluster'); +const dgram = require('dgram'); + +// This test ensures that the `ipv6Only` option in `dgram.createSock()` +// works as expected. +if (cluster.isMaster) { + cluster.fork().on('exit', common.mustCall((code) => { + assert.strictEqual(code, 0); + })); +} else { + let waiting = 2; + function close() { + if (--waiting === 0) + cluster.worker.disconnect(); + } + + const socket1 = dgram.createSocket({ + type: 'udp6', + ipv6Only: true + }); + const socket2 = dgram.createSocket({ + type: 'udp4', + }); + socket1.on('error', common.mustNotCall()); + socket2.on('error', common.mustNotCall()); + + socket1.bind({ + port: 0, + address: '::', + }, common.mustCall(() => { + const { port } = socket1.address(); + socket2.bind({ + port, + address: '0.0.0.0', + }, common.mustCall(() => { + process.nextTick(() => { + socket1.close(close); + socket2.close(close); + }); + })); + })); +} diff --git a/test/parallel/test-cluster-net-listen-ipv6only-false.js b/test/parallel/test-cluster-net-listen-ipv6only-false.js new file mode 100644 index 00000000000000..4d495d8faf6ddc --- /dev/null +++ b/test/parallel/test-cluster-net-listen-ipv6only-false.js @@ -0,0 +1,55 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasIPv6) + common.skip('no IPv6 support'); + +const assert = require('assert'); +const cluster = require('cluster'); +const net = require('net'); +const Countdown = require('../common/countdown'); + +// This test ensures that dual-stack support still works for cluster module +// when `ipv6Only` is not `true`. +const host = '::'; +const WORKER_COUNT = 3; + +if (cluster.isMaster) { + const workers = new Map(); + let address; + + const countdown = new Countdown(WORKER_COUNT, () => { + const socket = net.connect({ + port: address.port, + host: '0.0.0.0', + }, common.mustCall(() => { + socket.destroy(); + workers.forEach((worker) => { + worker.disconnect(); + }); + })); + socket.on('error', common.mustNotCall()); + }); + + for (let i = 0; i < WORKER_COUNT; i += 1) { + const worker = cluster.fork().on('exit', common.mustCall((statusCode) => { + assert.strictEqual(statusCode, 0); + })).on('listening', common.mustCall((workerAddress) => { + if (!address) { + address = workerAddress; + } else { + assert.strictEqual(address.addressType, workerAddress.addressType); + assert.strictEqual(address.host, workerAddress.host); + assert.strictEqual(address.port, workerAddress.port); + } + countdown.dec(); + })); + + workers.set(i, worker); + } +} else { + net.createServer().listen({ + host, + port: 0, + }, common.mustCall()); +} diff --git a/test/parallel/test-cluster-net-listen-ipv6only-none.js b/test/parallel/test-cluster-net-listen-ipv6only-none.js new file mode 100644 index 00000000000000..401afbc035e36a --- /dev/null +++ b/test/parallel/test-cluster-net-listen-ipv6only-none.js @@ -0,0 +1,58 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasIPv6) + common.skip('no IPv6 support'); + +const assert = require('assert'); +const cluster = require('cluster'); +const net = require('net'); +const Countdown = require('../common/countdown'); + +// This test ensures that the `ipv6Only` option in `net.Server.listen()` +// works as expected when we use cluster with `SCHED_NONE` schedulingPolicy. +cluster.schedulingPolicy = cluster.SCHED_NONE; +const host = '::'; +const WORKER_ACCOUNT = 3; + +if (cluster.isMaster) { + const workers = new Map(); + let address; + + const countdown = new Countdown(WORKER_ACCOUNT, () => { + // Make sure the `ipv6Only` option works. + const server = net.createServer().listen({ + host: '0.0.0.0', + port: address.port, + }, common.mustCall(() => { + // Exit. + server.close(); + workers.forEach((worker) => { + worker.disconnect(); + }); + })); + }); + + for (let i = 0; i < WORKER_ACCOUNT; i += 1) { + const worker = cluster.fork().on('exit', common.mustCall((statusCode) => { + assert.strictEqual(statusCode, 0); + })).on('listening', common.mustCall((workerAddress) => { + if (!address) { + address = workerAddress; + } else { + assert.strictEqual(address.addressType, workerAddress.addressType); + assert.strictEqual(address.host, workerAddress.host); + assert.strictEqual(address.port, workerAddress.port); + } + countdown.dec(); + })); + + workers.set(i, worker); + } +} else { + net.createServer().listen({ + host, + port: 0, + ipv6Only: true, + }, common.mustCall()); +} diff --git a/test/parallel/test-cluster-net-listen-ipv6only-rr.js b/test/parallel/test-cluster-net-listen-ipv6only-rr.js new file mode 100644 index 00000000000000..de254a4fe9ecd9 --- /dev/null +++ b/test/parallel/test-cluster-net-listen-ipv6only-rr.js @@ -0,0 +1,58 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasIPv6) + common.skip('no IPv6 support'); + +const assert = require('assert'); +const cluster = require('cluster'); +const net = require('net'); +const Countdown = require('../common/countdown'); + +// This test ensures that the `ipv6Only` option in `net.Server.listen()` +// works as expected when we use cluster with `SCHED_RR` schedulingPolicy. +cluster.schedulingPolicy = cluster.SCHED_RR; +const host = '::'; +const WORKER_ACCOUNT = 3; + +if (cluster.isMaster) { + const workers = new Map(); + let address; + + const countdown = new Countdown(WORKER_ACCOUNT, () => { + // Make sure the `ipv6Only` option works. + const server = net.createServer().listen({ + host: '0.0.0.0', + port: address.port, + }, common.mustCall(() => { + // Exit. + server.close(); + workers.forEach((worker) => { + worker.disconnect(); + }); + })); + }); + + for (let i = 0; i < WORKER_ACCOUNT; i += 1) { + const worker = cluster.fork().on('exit', common.mustCall((statusCode) => { + assert.strictEqual(statusCode, 0); + })).on('listening', common.mustCall((workerAddress) => { + if (!address) { + address = workerAddress; + } else { + assert.strictEqual(address.addressType, workerAddress.addressType); + assert.strictEqual(address.host, workerAddress.host); + assert.strictEqual(address.port, workerAddress.port); + } + countdown.dec(); + })); + + workers.set(i, worker); + } +} else { + net.createServer().listen({ + host, + port: 0, + ipv6Only: true, + }, common.mustCall()); +} diff --git a/test/parallel/test-dgram-ipv6only.js b/test/parallel/test-dgram-ipv6only.js new file mode 100644 index 00000000000000..1187f3084ad6f5 --- /dev/null +++ b/test/parallel/test-dgram-ipv6only.js @@ -0,0 +1,33 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasIPv6) + common.skip('no IPv6 support'); + +const dgram = require('dgram'); + +// This test ensures that dual-stack support is disabled when +// we specify the `ipv6Only` option in `dgram.createSocket()`. +const socket = dgram.createSocket({ + type: 'udp6', + ipv6Only: true, +}); + +socket.bind({ + port: 0, + address: '::', +}, common.mustCall(() => { + const { port } = socket.address(); + const client = dgram.createSocket('udp4'); + + // We can still bind to '0.0.0.0'. + client.bind({ + port, + address: '0.0.0.0', + }, common.mustCall(() => { + client.close(); + socket.close(); + })); + + client.on('error', common.mustNotCall()); +})); diff --git a/test/parallel/test-net-listen-ipv6only.js b/test/parallel/test-net-listen-ipv6only.js new file mode 100644 index 00000000000000..a329011bcc8a23 --- /dev/null +++ b/test/parallel/test-net-listen-ipv6only.js @@ -0,0 +1,30 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasIPv6) + common.skip('no IPv6 support'); + +// This test ensures that dual-stack support is disabled when +// we specify the `ipv6Only` option in `net.Server.listen()`. +const assert = require('assert'); +const net = require('net'); + +const host = '::'; +const server = net.createServer(); +server.listen({ + host, + port: 0, + ipv6Only: true, +}, common.mustCall(() => { + const { port } = server.address(); + const socket = net.connect({ + host: '0.0.0.0', + port, + }); + + socket.on('connect', common.mustNotCall()); + socket.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ECONNREFUSED'); + server.close(); + })); +}));