Skip to content

Commit a1254a3

Browse files
oyydrvagg
authored andcommitted
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: #17664 PR-URL: #23798 Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 8fcf3b3 commit a1254a3

14 files changed

+347
-22
lines changed

doc/api/dgram.md

+6
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,9 @@ changes:
601601
pr-url: https://github.com/nodejs/node/pull/13623
602602
description: The `recvBufferSize` and `sendBufferSize` options are
603603
supported now.
604+
- version: REPLACEME
605+
pr-url: https://github.com/nodejs/node/pull/23798
606+
description: The `ipv6Only` option is supported.
604607
-->
605608

606609
* `options` {Object} Available options are:
@@ -609,6 +612,9 @@ changes:
609612
* `reuseAddr` {boolean} When `true` [`socket.bind()`][] will reuse the
610613
address, even if another process has already bound a socket on it.
611614
**Default:** `false`.
615+
* `ipv6Only` {boolean} Setting `ipv6Only` to `true` will
616+
disable dual-stack support, i.e., binding to address `::` won't make
617+
`0.0.0.0` be bound. **Default:** `false`.
612618
* `recvBufferSize` {number} - Sets the `SO_RCVBUF` socket value.
613619
* `sendBufferSize` {number} - Sets the `SO_SNDBUF` socket value.
614620
* `lookup` {Function} Custom lookup function. **Default:** [`dns.lookup()`][].

doc/api/net.md

+7
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@ Listening on a file descriptor is not supported on Windows.
252252
#### server.listen(options[, callback])
253253
<!-- YAML
254254
added: v0.11.14
255+
changes:
256+
- version: REPLACEME
257+
pr-url: https://github.com/nodejs/node/pull/23798
258+
description: The `ipv6Only` option is supported.
255259
-->
256260

257261
* `options` {Object} Required. Supports the following properties:
@@ -266,6 +270,9 @@ added: v0.11.14
266270
for all users. **Default:** `false`
267271
* `writableAll` {boolean} For IPC servers makes the pipe writable
268272
for all users. **Default:** `false`
273+
* `ipv6Only` {boolean} For TCP servers, setting `ipv6Only` to `true` will
274+
disable dual-stack support, i.e., binding to host `::` won't make
275+
`0.0.0.0` be bound. **Default:** `false`.
269276
* `callback` {Function} Common parameter of [`server.listen()`][]
270277
functions.
271278
* Returns: {net.Server}

lib/dgram.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ const {
5454
} = require('internal/async_hooks');
5555
const { UV_UDP_REUSEADDR } = internalBinding('constants').os;
5656

57-
const { UDP, SendWrap } = internalBinding('udp_wrap');
57+
const {
58+
constants: { UV_UDP_IPV6ONLY },
59+
UDP,
60+
SendWrap
61+
} = internalBinding('udp_wrap');
5862

5963
const BIND_STATE_UNBOUND = 0;
6064
const BIND_STATE_BINDING = 1;
@@ -99,6 +103,7 @@ function Socket(type, listener) {
99103
bindState: BIND_STATE_UNBOUND,
100104
queue: undefined,
101105
reuseAddr: options && options.reuseAddr, // Use UV_UDP_REUSEADDR if true.
106+
ipv6Only: options && options.ipv6Only,
102107
recvBufferSize,
103108
sendBufferSize
104109
};
@@ -270,6 +275,8 @@ Socket.prototype.bind = function(port_, address_ /* , callback */) {
270275
var flags = 0;
271276
if (state.reuseAddr)
272277
flags |= UV_UDP_REUSEADDR;
278+
if (state.ipv6Only)
279+
flags |= UV_UDP_IPV6ONLY;
273280

274281
if (cluster.isWorker && !exclusive) {
275282
bindServerHandle(this, {

lib/internal/cluster/round_robin_handle.js

+10-4
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ const assert = require('assert');
33
const net = require('net');
44
const { sendHelper } = require('internal/cluster/utils');
55
const uv = internalBinding('uv');
6+
const { constants } = internalBinding('tcp_wrap');
67

78
module.exports = RoundRobinHandle;
89

9-
function RoundRobinHandle(key, address, port, addressType, fd) {
10+
function RoundRobinHandle(key, address, port, addressType, fd, flags) {
1011
this.key = key;
1112
this.all = new Map();
1213
this.free = [];
@@ -16,9 +17,14 @@ function RoundRobinHandle(key, address, port, addressType, fd) {
1617

1718
if (fd >= 0)
1819
this.server.listen({ fd });
19-
else if (port >= 0)
20-
this.server.listen(port, address);
21-
else
20+
else if (port >= 0) {
21+
this.server.listen({
22+
port,
23+
host: address,
24+
// Currently, net module only supports `ipv6Only` option in `flags`.
25+
ipv6Only: Boolean(flags & constants.UV_TCP_IPV6ONLY),
26+
});
27+
} else
2228
this.server.listen(address); // UNIX socket path.
2329

2430
this.server.once('listening', () => {

lib/internal/cluster/shared_handle.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ function SharedHandle(key, address, port, addressType, fd, flags) {
1515
if (addressType === 'udp4' || addressType === 'udp6')
1616
rval = dgram._createSocketHandle(address, port, addressType, fd, flags);
1717
else
18-
rval = net._createServerHandle(address, port, addressType, fd);
18+
rval = net._createServerHandle(address, port, addressType, fd, flags);
1919

2020
if (typeof rval === 'number')
2121
this.errno = rval;

lib/net.js

+20-15
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ const {
9797

9898
function noop() {}
9999

100+
function getFlags(ipv6Only) {
101+
return ipv6Only === true ? TCPConstants.UV_TCP_IPV6ONLY : 0;
102+
}
103+
100104
function createHandle(fd, is_server) {
101105
validateInt32(fd, 'fd', 0);
102106
const type = TTYWrap.guessHandleType(fd);
@@ -798,7 +802,7 @@ function checkBindError(err, port, handle) {
798802

799803

800804
function internalConnect(
801-
self, address, port, addressType, localAddress, localPort) {
805+
self, address, port, addressType, localAddress, localPort, flags) {
802806
// TODO return promise from Socket.prototype.connect which
803807
// wraps _connectReq.
804808

@@ -812,7 +816,7 @@ function internalConnect(
812816
err = self._handle.bind(localAddress, localPort);
813817
} else { // addressType === 6
814818
localAddress = localAddress || '::';
815-
err = self._handle.bind6(localAddress, localPort);
819+
err = self._handle.bind6(localAddress, localPort, flags);
816820
}
817821
debug('binding to localAddress: %s and localPort: %d (addressType: %d)',
818822
localAddress, localPort, addressType);
@@ -1148,7 +1152,7 @@ util.inherits(Server, EventEmitter);
11481152
function toNumber(x) { return (x = Number(x)) >= 0 ? x : false; }
11491153

11501154
// Returns handle if it can be created, or error code if it can't
1151-
function createServerHandle(address, port, addressType, fd) {
1155+
function createServerHandle(address, port, addressType, fd, flags) {
11521156
var err = 0;
11531157
// assign handle in listen, and clean up if bind or listen fails
11541158
var handle;
@@ -1187,14 +1191,14 @@ function createServerHandle(address, port, addressType, fd) {
11871191
debug('bind to', address || 'any');
11881192
if (!address) {
11891193
// Try binding to ipv6 first
1190-
err = handle.bind6('::', port);
1194+
err = handle.bind6('::', port, flags);
11911195
if (err) {
11921196
handle.close();
11931197
// Fallback to ipv4
11941198
return createServerHandle('0.0.0.0', port);
11951199
}
11961200
} else if (addressType === 6) {
1197-
err = handle.bind6(address, port);
1201+
err = handle.bind6(address, port, flags);
11981202
} else {
11991203
err = handle.bind(address, port);
12001204
}
@@ -1208,7 +1212,7 @@ function createServerHandle(address, port, addressType, fd) {
12081212
return handle;
12091213
}
12101214

1211-
function setupListenHandle(address, port, addressType, backlog, fd) {
1215+
function setupListenHandle(address, port, addressType, backlog, fd, flags) {
12121216
debug('setupListenHandle', address, port, addressType, backlog, fd);
12131217

12141218
// 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) {
12221226

12231227
// Try to bind to the unspecified IPv6 address, see if IPv6 is available
12241228
if (!address && typeof fd !== 'number') {
1225-
rval = createServerHandle('::', port, 6, fd);
1229+
rval = createServerHandle('::', port, 6, fd, flags);
12261230

12271231
if (typeof rval === 'number') {
12281232
rval = null;
@@ -1235,7 +1239,7 @@ function setupListenHandle(address, port, addressType, backlog, fd) {
12351239
}
12361240

12371241
if (rval === null)
1238-
rval = createServerHandle(address, port, addressType, fd);
1242+
rval = createServerHandle(address, port, addressType, fd, flags);
12391243

12401244
if (typeof rval === 'number') {
12411245
var error = uvExceptionWithHostPort(rval, 'listen', address, port);
@@ -1294,7 +1298,7 @@ function emitListeningNT(self) {
12941298

12951299

12961300
function listenInCluster(server, address, port, addressType,
1297-
backlog, fd, exclusive) {
1301+
backlog, fd, exclusive, flags) {
12981302
exclusive = !!exclusive;
12991303

13001304
if (cluster === undefined) cluster = require('cluster');
@@ -1303,7 +1307,7 @@ function listenInCluster(server, address, port, addressType,
13031307
// Will create a new handle
13041308
// _listen2 sets up the listened handle, it is still named like this
13051309
// to avoid breaking code that wraps this method
1306-
server._listen2(address, port, addressType, backlog, fd);
1310+
server._listen2(address, port, addressType, backlog, fd, flags);
13071311
return;
13081312
}
13091313

@@ -1312,7 +1316,7 @@ function listenInCluster(server, address, port, addressType,
13121316
port: port,
13131317
addressType: addressType,
13141318
fd: fd,
1315-
flags: 0
1319+
flags,
13161320
};
13171321

13181322
// Get the master's server handle, and listen on it
@@ -1330,7 +1334,7 @@ function listenInCluster(server, address, port, addressType,
13301334
server._handle = handle;
13311335
// _listen2 sets up the listened handle, it is still named like this
13321336
// to avoid breaking code that wraps this method
1333-
server._listen2(address, port, addressType, backlog, fd);
1337+
server._listen2(address, port, addressType, backlog, fd, flags);
13341338
}
13351339
}
13361340

@@ -1353,6 +1357,7 @@ Server.prototype.listen = function(...args) {
13531357
toNumber(args.length > 2 && args[2]); // (port, host, backlog)
13541358

13551359
options = options._handle || options.handle || options;
1360+
const flags = getFlags(options.ipv6Only);
13561361
// (handle[, backlog][, cb]) where handle is an object with a handle
13571362
if (options instanceof TCP) {
13581363
this._handle = options;
@@ -1387,7 +1392,7 @@ Server.prototype.listen = function(...args) {
13871392
// start TCP server listening on host:port
13881393
if (options.host) {
13891394
lookupAndListen(this, options.port | 0, options.host, backlog,
1390-
options.exclusive);
1395+
options.exclusive, flags);
13911396
} else { // Undefined host, listens on unspecified address
13921397
// Default addressType 4 will be used to search for master server
13931398
listenInCluster(this, null, options.port | 0, 4,
@@ -1434,15 +1439,15 @@ Server.prototype.listen = function(...args) {
14341439
throw new ERR_INVALID_OPT_VALUE('options', util.inspect(options));
14351440
};
14361441

1437-
function lookupAndListen(self, port, address, backlog, exclusive) {
1442+
function lookupAndListen(self, port, address, backlog, exclusive, flags) {
14381443
if (dns === undefined) dns = require('dns');
14391444
dns.lookup(address, function doListen(err, ip, addressType) {
14401445
if (err) {
14411446
self.emit('error', err);
14421447
} else {
14431448
addressType = ip ? addressType : 4;
14441449
listenInCluster(self, ip, port, addressType,
1445-
backlog, undefined, exclusive);
1450+
backlog, undefined, exclusive, flags);
14461451
}
14471452
});
14481453
}

src/tcp_wrap.cc

+4-1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ void TCPWrap::Initialize(Local<Object> target,
127127
Local<Object> constants = Object::New(env->isolate());
128128
NODE_DEFINE_CONSTANT(constants, SOCKET);
129129
NODE_DEFINE_CONSTANT(constants, SERVER);
130+
NODE_DEFINE_CONSTANT(constants, UV_TCP_IPV6ONLY);
130131
target->Set(context,
131132
env->constants_string(),
132133
constants).FromJust();
@@ -252,13 +253,15 @@ void TCPWrap::Bind6(const FunctionCallbackInfo<Value>& args) {
252253
Environment* env = wrap->env();
253254
node::Utf8Value ip6_address(env->isolate(), args[0]);
254255
int port;
256+
unsigned int flags;
255257
if (!args[1]->Int32Value(env->context()).To(&port)) return;
258+
if (!args[2]->Uint32Value(env->context()).To(&flags)) return;
256259
sockaddr_in6 addr;
257260
int err = uv_ip6_addr(*ip6_address, port, &addr);
258261
if (err == 0) {
259262
err = uv_tcp_bind(&wrap->handle_,
260263
reinterpret_cast<const sockaddr*>(&addr),
261-
0);
264+
flags);
262265
}
263266
args.GetReturnValue().Set(err);
264267
}

src/udp_wrap.cc

+6
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ void UDPWrap::Initialize(Local<Object> target,
149149
target->Set(env->context(),
150150
sendWrapString,
151151
swt->GetFunction(env->context()).ToLocalChecked()).FromJust();
152+
153+
Local<Object> constants = Object::New(env->isolate());
154+
NODE_DEFINE_CONSTANT(constants, UV_UDP_IPV6ONLY);
155+
target->Set(context,
156+
env->constants_string(),
157+
constants).FromJust();
152158
}
153159

154160

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasIPv6)
5+
common.skip('no IPv6 support');
6+
if (common.isWindows)
7+
common.skip('dgram clustering is currently not supported on windows.');
8+
9+
const assert = require('assert');
10+
const cluster = require('cluster');
11+
const dgram = require('dgram');
12+
13+
// This test ensures that the `ipv6Only` option in `dgram.createSock()`
14+
// works as expected.
15+
if (cluster.isMaster) {
16+
cluster.fork().on('exit', common.mustCall((code) => {
17+
assert.strictEqual(code, 0);
18+
}));
19+
} else {
20+
let waiting = 2;
21+
function close() {
22+
if (--waiting === 0)
23+
cluster.worker.disconnect();
24+
}
25+
26+
const socket1 = dgram.createSocket({
27+
type: 'udp6',
28+
ipv6Only: true
29+
});
30+
const socket2 = dgram.createSocket({
31+
type: 'udp4',
32+
});
33+
socket1.on('error', common.mustNotCall());
34+
socket2.on('error', common.mustNotCall());
35+
36+
socket1.bind({
37+
port: 0,
38+
address: '::',
39+
}, common.mustCall(() => {
40+
const { port } = socket1.address();
41+
socket2.bind({
42+
port,
43+
address: '0.0.0.0',
44+
}, common.mustCall(() => {
45+
process.nextTick(() => {
46+
socket1.close(close);
47+
socket2.close(close);
48+
});
49+
}));
50+
}));
51+
}

0 commit comments

Comments
 (0)