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
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()`][].

View File

@ -252,6 +252,10 @@ Listening on a file descriptor is not supported on Windows.
#### server.listen(options[, callback])
<!-- YAML
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:
@ -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}

View File

@ -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, {

View File

@ -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', () => {

View File

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

View File

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

View File

@ -127,6 +127,7 @@ void TCPWrap::Initialize(Local<Object> target,
Local<Object> 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<Value>& 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<const sockaddr*>(&addr),
0);
flags);
}
args.GetReturnValue().Set(err);
}

View File

@ -149,6 +149,12 @@ void UDPWrap::Initialize(Local<Object> target,
target->Set(env->context(),
sendWrapString,
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();
}));
}));