Skip to content

Commit

Permalink
net,dgram: add ipv6Only option for net and dgram
Browse files Browse the repository at this point in the history
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: #17664

PR-URL: #23798
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: James M Snell <jasnell@gmail.com>
  • Loading branch information
oyyd committed Nov 22, 2018
1 parent 91748dd commit 33a25b2
Show file tree
Hide file tree
Showing 14 changed files with 347 additions and 22 deletions.
6 changes: 6 additions & 0 deletions doc/api/dgram.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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()`][].
Expand Down
7 changes: 7 additions & 0 deletions doc/api/net.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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}
Expand Down
9 changes: 8 additions & 1 deletion lib/dgram.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
};
Expand Down Expand Up @@ -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, {
Expand Down
14 changes: 10 additions & 4 deletions lib/internal/cluster/round_robin_handle.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand All @@ -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', () => {
Expand Down
2 changes: 1 addition & 1 deletion lib/internal/cluster/shared_handle.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
35 changes: 20 additions & 15 deletions lib/net.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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.

Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand All @@ -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.
Expand All @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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');
Expand All @@ -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;
}

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

Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1434,15 +1439,15 @@ 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) {
self.emit('error', err);
} else {
addressType = ip ? addressType : 4;
listenInCluster(self, ip, port, addressType,
backlog, undefined, exclusive);
backlog, undefined, exclusive, flags);
}
});
}
Expand Down
5 changes: 4 additions & 1 deletion src/tcp_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}
Expand Down
6 changes: 6 additions & 0 deletions src/udp_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}


Expand Down
51 changes: 51 additions & 0 deletions test/parallel/test-cluster-dgram-ipv6only.js
Original file line number Diff line number Diff line change
@@ -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);
});
}));
}));
}
Loading

0 comments on commit 33a25b2

Please sign in to comment.