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 <info@bnoordhuis.nl>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Ouyang Yadong 2018-10-21 15:59:38 +08:00
parent 91748dd89c
commit 33a25b29a4
14 changed files with 347 additions and 22 deletions

View File

@ -601,6 +601,9 @@ changes:
pr-url: https://github.com/nodejs/node/pull/13623 pr-url: https://github.com/nodejs/node/pull/13623
description: The `recvBufferSize` and `sendBufferSize` options are description: The `recvBufferSize` and `sendBufferSize` options are
supported now. supported now.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/23798
description: The `ipv6Only` option is supported.
--> -->
* `options` {Object} Available options are: * `options` {Object} Available options are:
@ -609,6 +612,9 @@ changes:
* `reuseAddr` {boolean} When `true` [`socket.bind()`][] will reuse the * `reuseAddr` {boolean} When `true` [`socket.bind()`][] will reuse the
address, even if another process has already bound a socket on it. address, even if another process has already bound a socket on it.
**Default:** `false`. **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. * `recvBufferSize` {number} - Sets the `SO_RCVBUF` socket value.
* `sendBufferSize` {number} - Sets the `SO_SNDBUF` socket value. * `sendBufferSize` {number} - Sets the `SO_SNDBUF` socket value.
* `lookup` {Function} Custom lookup function. **Default:** [`dns.lookup()`][]. * `lookup` {Function} Custom lookup function. **Default:** [`dns.lookup()`][].

View File

@ -252,6 +252,10 @@ Listening on a file descriptor is not supported on Windows.
#### server.listen(options[, callback]) #### server.listen(options[, callback])
<!-- YAML <!-- YAML
added: v0.11.14 added: v0.11.14
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/23798
description: The `ipv6Only` option is supported.
--> -->
* `options` {Object} Required. Supports the following properties: * `options` {Object} Required. Supports the following properties:
@ -266,6 +270,9 @@ added: v0.11.14
for all users. **Default:** `false` for all users. **Default:** `false`
* `writableAll` {boolean} For IPC servers makes the pipe writable * `writableAll` {boolean} For IPC servers makes the pipe writable
for all users. **Default:** `false` 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()`][] * `callback` {Function} Common parameter of [`server.listen()`][]
functions. functions.
* Returns: {net.Server} * Returns: {net.Server}

View File

@ -54,7 +54,11 @@ const {
} = require('internal/async_hooks'); } = require('internal/async_hooks');
const { UV_UDP_REUSEADDR } = internalBinding('constants').os; 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_UNBOUND = 0;
const BIND_STATE_BINDING = 1; const BIND_STATE_BINDING = 1;
@ -99,6 +103,7 @@ function Socket(type, listener) {
bindState: BIND_STATE_UNBOUND, bindState: BIND_STATE_UNBOUND,
queue: undefined, queue: undefined,
reuseAddr: options && options.reuseAddr, // Use UV_UDP_REUSEADDR if true. reuseAddr: options && options.reuseAddr, // Use UV_UDP_REUSEADDR if true.
ipv6Only: options && options.ipv6Only,
recvBufferSize, recvBufferSize,
sendBufferSize sendBufferSize
}; };
@ -270,6 +275,8 @@ Socket.prototype.bind = function(port_, address_ /* , callback */) {
var flags = 0; var flags = 0;
if (state.reuseAddr) if (state.reuseAddr)
flags |= UV_UDP_REUSEADDR; flags |= UV_UDP_REUSEADDR;
if (state.ipv6Only)
flags |= UV_UDP_IPV6ONLY;
if (cluster.isWorker && !exclusive) { if (cluster.isWorker && !exclusive) {
bindServerHandle(this, { bindServerHandle(this, {

View File

@ -3,10 +3,11 @@ const assert = require('assert');
const net = require('net'); const net = require('net');
const { sendHelper } = require('internal/cluster/utils'); const { sendHelper } = require('internal/cluster/utils');
const uv = internalBinding('uv'); const uv = internalBinding('uv');
const { constants } = internalBinding('tcp_wrap');
module.exports = RoundRobinHandle; module.exports = RoundRobinHandle;
function RoundRobinHandle(key, address, port, addressType, fd) { function RoundRobinHandle(key, address, port, addressType, fd, flags) {
this.key = key; this.key = key;
this.all = new Map(); this.all = new Map();
this.free = []; this.free = [];
@ -16,9 +17,14 @@ function RoundRobinHandle(key, address, port, addressType, fd) {
if (fd >= 0) if (fd >= 0)
this.server.listen({ fd }); this.server.listen({ fd });
else if (port >= 0) else if (port >= 0) {
this.server.listen(port, address); this.server.listen({
else 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.listen(address); // UNIX socket path.
this.server.once('listening', () => { this.server.once('listening', () => {

View File

@ -15,7 +15,7 @@ function SharedHandle(key, address, port, addressType, fd, flags) {
if (addressType === 'udp4' || addressType === 'udp6') if (addressType === 'udp4' || addressType === 'udp6')
rval = dgram._createSocketHandle(address, port, addressType, fd, flags); rval = dgram._createSocketHandle(address, port, addressType, fd, flags);
else else
rval = net._createServerHandle(address, port, addressType, fd); rval = net._createServerHandle(address, port, addressType, fd, flags);
if (typeof rval === 'number') if (typeof rval === 'number')
this.errno = rval; this.errno = rval;

View File

@ -97,6 +97,10 @@ const {
function noop() {} function noop() {}
function getFlags(ipv6Only) {
return ipv6Only === true ? TCPConstants.UV_TCP_IPV6ONLY : 0;
}
function createHandle(fd, is_server) { function createHandle(fd, is_server) {
validateInt32(fd, 'fd', 0); validateInt32(fd, 'fd', 0);
const type = TTYWrap.guessHandleType(fd); const type = TTYWrap.guessHandleType(fd);
@ -798,7 +802,7 @@ function checkBindError(err, port, handle) {
function internalConnect( function internalConnect(
self, address, port, addressType, localAddress, localPort) { self, address, port, addressType, localAddress, localPort, flags) {
// TODO return promise from Socket.prototype.connect which // TODO return promise from Socket.prototype.connect which
// wraps _connectReq. // wraps _connectReq.
@ -812,7 +816,7 @@ function internalConnect(
err = self._handle.bind(localAddress, localPort); err = self._handle.bind(localAddress, localPort);
} else { // addressType === 6 } else { // addressType === 6
localAddress = localAddress || '::'; 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)', debug('binding to localAddress: %s and localPort: %d (addressType: %d)',
localAddress, localPort, addressType); localAddress, localPort, addressType);
@ -1148,7 +1152,7 @@ util.inherits(Server, EventEmitter);
function toNumber(x) { return (x = Number(x)) >= 0 ? x : false; } function toNumber(x) { return (x = Number(x)) >= 0 ? x : false; }
// Returns handle if it can be created, or error code if it can't // 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; var err = 0;
// assign handle in listen, and clean up if bind or listen fails // assign handle in listen, and clean up if bind or listen fails
var handle; var handle;
@ -1187,14 +1191,14 @@ function createServerHandle(address, port, addressType, fd) {
debug('bind to', address || 'any'); debug('bind to', address || 'any');
if (!address) { if (!address) {
// Try binding to ipv6 first // Try binding to ipv6 first
err = handle.bind6('::', port); err = handle.bind6('::', port, flags);
if (err) { if (err) {
handle.close(); handle.close();
// Fallback to ipv4 // Fallback to ipv4
return createServerHandle('0.0.0.0', port); return createServerHandle('0.0.0.0', port);
} }
} else if (addressType === 6) { } else if (addressType === 6) {
err = handle.bind6(address, port); err = handle.bind6(address, port, flags);
} else { } else {
err = handle.bind(address, port); err = handle.bind(address, port);
} }
@ -1208,7 +1212,7 @@ function createServerHandle(address, port, addressType, fd) {
return handle; return handle;
} }
function setupListenHandle(address, port, addressType, backlog, fd) { function setupListenHandle(address, port, addressType, backlog, fd, flags) {
debug('setupListenHandle', address, port, addressType, backlog, fd); debug('setupListenHandle', address, port, addressType, backlog, fd);
// If there is not yet a handle, we need to create one and bind. // 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 // Try to bind to the unspecified IPv6 address, see if IPv6 is available
if (!address && typeof fd !== 'number') { if (!address && typeof fd !== 'number') {
rval = createServerHandle('::', port, 6, fd); rval = createServerHandle('::', port, 6, fd, flags);
if (typeof rval === 'number') { if (typeof rval === 'number') {
rval = null; rval = null;
@ -1235,7 +1239,7 @@ function setupListenHandle(address, port, addressType, backlog, fd) {
} }
if (rval === null) if (rval === null)
rval = createServerHandle(address, port, addressType, fd); rval = createServerHandle(address, port, addressType, fd, flags);
if (typeof rval === 'number') { if (typeof rval === 'number') {
var error = uvExceptionWithHostPort(rval, 'listen', address, port); var error = uvExceptionWithHostPort(rval, 'listen', address, port);
@ -1294,7 +1298,7 @@ function emitListeningNT(self) {
function listenInCluster(server, address, port, addressType, function listenInCluster(server, address, port, addressType,
backlog, fd, exclusive) { backlog, fd, exclusive, flags) {
exclusive = !!exclusive; exclusive = !!exclusive;
if (cluster === undefined) cluster = require('cluster'); if (cluster === undefined) cluster = require('cluster');
@ -1303,7 +1307,7 @@ function listenInCluster(server, address, port, addressType,
// Will create a new handle // Will create a new handle
// _listen2 sets up the listened handle, it is still named like this // _listen2 sets up the listened handle, it is still named like this
// to avoid breaking code that wraps this method // to avoid breaking code that wraps this method
server._listen2(address, port, addressType, backlog, fd); server._listen2(address, port, addressType, backlog, fd, flags);
return; return;
} }
@ -1312,7 +1316,7 @@ function listenInCluster(server, address, port, addressType,
port: port, port: port,
addressType: addressType, addressType: addressType,
fd: fd, fd: fd,
flags: 0 flags,
}; };
// Get the master's server handle, and listen on it // Get the master's server handle, and listen on it
@ -1330,7 +1334,7 @@ function listenInCluster(server, address, port, addressType,
server._handle = handle; server._handle = handle;
// _listen2 sets up the listened handle, it is still named like this // _listen2 sets up the listened handle, it is still named like this
// to avoid breaking code that wraps this method // 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) toNumber(args.length > 2 && args[2]); // (port, host, backlog)
options = options._handle || options.handle || options; options = options._handle || options.handle || options;
const flags = getFlags(options.ipv6Only);
// (handle[, backlog][, cb]) where handle is an object with a handle // (handle[, backlog][, cb]) where handle is an object with a handle
if (options instanceof TCP) { if (options instanceof TCP) {
this._handle = options; this._handle = options;
@ -1387,7 +1392,7 @@ Server.prototype.listen = function(...args) {
// start TCP server listening on host:port // start TCP server listening on host:port
if (options.host) { if (options.host) {
lookupAndListen(this, options.port | 0, options.host, backlog, lookupAndListen(this, options.port | 0, options.host, backlog,
options.exclusive); options.exclusive, flags);
} else { // Undefined host, listens on unspecified address } else { // Undefined host, listens on unspecified address
// Default addressType 4 will be used to search for master server // Default addressType 4 will be used to search for master server
listenInCluster(this, null, options.port | 0, 4, 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)); 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'); if (dns === undefined) dns = require('dns');
dns.lookup(address, function doListen(err, ip, addressType) { dns.lookup(address, function doListen(err, ip, addressType) {
if (err) { if (err) {
@ -1442,7 +1447,7 @@ function lookupAndListen(self, port, address, backlog, exclusive) {
} else { } else {
addressType = ip ? addressType : 4; addressType = ip ? addressType : 4;
listenInCluster(self, ip, port, addressType, listenInCluster(self, ip, port, addressType,
backlog, undefined, exclusive); backlog, undefined, exclusive, flags);
} }
}); });
} }

View File

@ -127,6 +127,7 @@ void TCPWrap::Initialize(Local<Object> target,
Local<Object> constants = Object::New(env->isolate()); Local<Object> constants = Object::New(env->isolate());
NODE_DEFINE_CONSTANT(constants, SOCKET); NODE_DEFINE_CONSTANT(constants, SOCKET);
NODE_DEFINE_CONSTANT(constants, SERVER); NODE_DEFINE_CONSTANT(constants, SERVER);
NODE_DEFINE_CONSTANT(constants, UV_TCP_IPV6ONLY);
target->Set(context, target->Set(context,
env->constants_string(), env->constants_string(),
constants).FromJust(); constants).FromJust();
@ -252,13 +253,15 @@ void TCPWrap::Bind6(const FunctionCallbackInfo<Value>& args) {
Environment* env = wrap->env(); Environment* env = wrap->env();
node::Utf8Value ip6_address(env->isolate(), args[0]); node::Utf8Value ip6_address(env->isolate(), args[0]);
int port; int port;
unsigned int flags;
if (!args[1]->Int32Value(env->context()).To(&port)) return; if (!args[1]->Int32Value(env->context()).To(&port)) return;
if (!args[2]->Uint32Value(env->context()).To(&flags)) return;
sockaddr_in6 addr; sockaddr_in6 addr;
int err = uv_ip6_addr(*ip6_address, port, &addr); int err = uv_ip6_addr(*ip6_address, port, &addr);
if (err == 0) { if (err == 0) {
err = uv_tcp_bind(&wrap->handle_, err = uv_tcp_bind(&wrap->handle_,
reinterpret_cast<const sockaddr*>(&addr), reinterpret_cast<const sockaddr*>(&addr),
0); flags);
} }
args.GetReturnValue().Set(err); args.GetReturnValue().Set(err);
} }

View File

@ -149,6 +149,12 @@ void UDPWrap::Initialize(Local<Object> target,
target->Set(env->context(), target->Set(env->context(),
sendWrapString, sendWrapString,
swt->GetFunction(env->context()).ToLocalChecked()).FromJust(); swt->GetFunction(env->context()).ToLocalChecked()).FromJust();
Local<Object> constants = Object::New(env->isolate());
NODE_DEFINE_CONSTANT(constants, UV_UDP_IPV6ONLY);
target->Set(context,
env->constants_string(),
constants).FromJust();
} }

View File

@ -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);
});
}));
}));
}

View File

@ -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());
}

View File

@ -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());
}

View File

@ -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());
}

View File

@ -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());
}));

View File

@ -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();
}));
}));