Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions doc/api/dgram.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,44 @@ socket.bind({
});
```

### `socket.bindSync([options])`

<!-- YAML
added: REPLACEME
-->

* `options` {Object}
* `port` {integer} If omitted or `0`, the operating system will assign an
arbitrary unused port. **Default:** `0`.
* `address` {string} A numeric IP address to bind to. Unlike
[`socket.bind()`][], no DNS resolution is performed, so a host name is not
accepted. If omitted, the operating system binds to all addresses
(`'0.0.0.0'` for `udp4` sockets, `'::'` for `udp6`).
* Returns: {Object} The bound address as returned by [`socket.address()`][].

The synchronous counterpart of [`socket.bind()`][]. `bind(2)` is a local,
non-blocking system call, so the bind is performed inline and the resolved
address is returned immediately, including the operating-system-assigned
ephemeral port when `port` is `0`:

```js
const dgram = require('node:dgram');

const socket = dgram.createSocket('udp4');
const address = socket.bindSync({ address: '0.0.0.0', port: 0 });
console.log(address); // e.g. { address: '0.0.0.0', family: 'IPv4', port: 53124 }
```

A bind failure such as `EADDRINUSE` is thrown synchronously rather than emitted
as an `'error'` event. After `bindSync()` returns, [`socket.address()`][] is
valid synchronously and the `'listening'` event is emitted on the next tick.

`address` must be a numeric IP literal; `bindSync()` never performs DNS
resolution (asynchronous name resolution being the only genuinely blocking part
of binding). Incoming datagrams continue to be delivered asynchronously via the
[`'message'`][] event. `bindSync()` always binds the socket's own handle and
does not participate in [`cluster`][] handle sharing.

### `socket.close([callback])`

<!-- YAML
Expand Down Expand Up @@ -1015,6 +1053,7 @@ and `udp6` sockets). The bound address and port can be retrieved using
[IPv6 Zone Indexes]: https://en.wikipedia.org/wiki/IPv6_address#Scoped_literal_IPv6_addresses
[RFC 4007]: https://tools.ietf.org/html/rfc4007
[`'close'`]: #event-close
[`'message'`]: #event-message
[`ERR_SOCKET_BAD_PORT`]: errors.md#err_socket_bad_port
[`ERR_SOCKET_BUFFER_SIZE`]: errors.md#err_socket_buffer_size
[`ERR_SOCKET_DGRAM_IS_CONNECTED`]: errors.md#err_socket_dgram_is_connected
Expand All @@ -1028,6 +1067,7 @@ and `udp6` sockets). The bound address and port can be retrieved using
[`dns.lookup()`]: dns.md#dnslookuphostname-options-callback
[`socket.address().address`]: #socketaddress
[`socket.address().port`]: #socketaddress
[`socket.address()`]: #socketaddress
[`socket.bind()`]: #socketbindport-address-callback
[`socket.close()`]: #socketclosecallback
[byte length]: buffer.md#static-method-bufferbytelengthstring-encoding
67 changes: 65 additions & 2 deletions lib/dgram.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const {
codes: {
ERR_BUFFER_OUT_OF_BOUNDS,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_FD_TYPE,
ERR_IP_BLOCKED,
ERR_MISSING_ARGS,
Expand All @@ -58,13 +59,14 @@ const { isIP } = require('internal/net');
const {
isInt32,
validateAbortSignal,
validateObject,
validateString,
validateNumber,
validatePort,
validateUint32,
} = require('internal/validators');
const { Buffer } = require('buffer');
const { guessHandleType, promisify } = require('internal/util');
const { guessHandleType, kEmptyObject, promisify } = require('internal/util');
const { isArrayBufferView } = require('internal/util/types');
const EventEmitter = require('events');
const { addAbortListener } = require('internal/events/abort_listener');
Expand Down Expand Up @@ -192,7 +194,7 @@ function createSocket(type, listener) {
}


function startListening(socket) {
function startReceiving(socket) {
const state = socket[kStateSymbol];

state.handle.onmessage = onMessage;
Expand All @@ -206,10 +208,19 @@ function startListening(socket) {

if (state.sendBufferSize)
bufferSize(socket, state.sendBufferSize, SEND_BUFFER);
}

function startListening(socket) {
startReceiving(socket);
socket.emit('listening');
}

function emitListeningNT(socket) {
// Ensure the socket was not closed before the next tick.
if (socket[kStateSymbol].handle)
socket.emit('listening');
}

function replaceHandle(self, newHandle) {
const state = self[kStateSymbol];
const oldHandle = state.handle;
Expand Down Expand Up @@ -409,6 +420,58 @@ Socket.prototype.bind = function(port_, address_ /* , callback */) {
return this;
};

// Synchronous counterpart of bind(). bind(2) is a local, non-blocking system
// call, so this binds inline and returns the resolved address (including the
// OS-assigned ephemeral port when port is 0), throwing synchronously on bind
// errors such as EADDRINUSE. The address must be a numeric IP literal:
// asynchronous name resolution is the only genuinely blocking part of bind(),
// so callers resolve names separately. Message delivery stays asynchronous
// ('message' events flow as usual); the 'listening' event is emitted on the
// next tick.
Socket.prototype.bindSync = function(options = kEmptyObject) {
healthCheck(this);
validateObject(options, 'options');
const state = this[kStateSymbol];

if (state.bindState !== BIND_STATE_UNBOUND)
throw new ERR_SOCKET_ALREADY_BOUND();

// Validate arguments before mutating state so a bad argument leaves the
// socket unbound and reusable.
const port = validatePort(options.port ?? 0, 'options.port');
let { address } = options;
if (!address) {
address = this.type === 'udp4' ? '0.0.0.0' : '::';
} else {
validateString(address, 'options.address');
if (isIP(address) === 0) {
throw new ERR_INVALID_ARG_VALUE(
'options.address', address,
'must be a numeric IP address; bindSync does not perform DNS resolution');
}
}

state.bindState = BIND_STATE_BINDING;

let flags = 0;
if (state.reuseAddr)
flags |= UV_UDP_REUSEADDR;
if (state.ipv6Only)
flags |= UV_UDP_IPV6ONLY;
if (state.reusePort)
flags |= UV_UDP_REUSEPORT;

const err = state.handle.bind(address, port, flags);
if (err) {
state.bindState = BIND_STATE_UNBOUND;
throw new ExceptionWithHostPort(err, 'bind', address, port);
}

startReceiving(this);
process.nextTick(emitListeningNT, this);
return this.address();
};

Socket.prototype.connect = function(port, address, callback) {
port = validatePort(port, 'Port', false);
if (typeof address === 'function') {
Expand Down
172 changes: 172 additions & 0 deletions test/parallel/test-dgram-bind-sync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const dgram = require('dgram');

// bindSync() binds synchronously and returns the resolved address, including
// the OS-assigned ephemeral port when port is 0.
{
const sock = dgram.createSocket('udp4');
const addr = sock.bindSync({ address: '127.0.0.1', port: 0 });

assert.strictEqual(addr.address, '127.0.0.1');
assert.strictEqual(addr.family, 'IPv4');
assert.strictEqual(typeof addr.port, 'number');
assert.ok(addr.port > 0);

// address() is valid synchronously and matches the returned address.
assert.deepStrictEqual(sock.address(), addr);

// The 'listening' event still fires on the next tick.
sock.on('listening', common.mustCall(() => sock.close()));
}

// Closing synchronously after bindSync() suppresses the deferred 'listening'.
{
const sock = dgram.createSocket('udp4');
sock.bindSync({ port: 0 });
sock.on('listening', common.mustNotCall());
sock.close();
}

// Defaults the address to the udp4 wildcard when omitted.
{
const sock = dgram.createSocket('udp4');
const addr = sock.bindSync();
assert.strictEqual(addr.address, '0.0.0.0');
assert.ok(addr.port > 0);
sock.close();
}

// 'message' events still flow asynchronously after a synchronous bind.
{
const receiver = dgram.createSocket('udp4');
const addr = receiver.bindSync({ address: '127.0.0.1', port: 0 });

receiver.on('message', common.mustCall((msg) => {
assert.strictEqual(msg.toString(), 'hello');
receiver.close();
}));

const sender = dgram.createSocket('udp4');
sender.send('hello', addr.port, '127.0.0.1', common.mustCall(() => {
sender.close();
}));
}

// Throws synchronously on EADDRINUSE.
{
const first = dgram.createSocket('udp4');
const addr = first.bindSync({ address: '127.0.0.1', port: 0 });

const second = dgram.createSocket('udp4');
assert.throws(() => {
second.bindSync({ address: '127.0.0.1', port: addr.port });
}, {
code: 'EADDRINUSE',
syscall: 'bind',
});

first.close();
second.close();
}

// Throws synchronously on a non-numeric address (no DNS resolution).
{
const sock = dgram.createSocket('udp4');
assert.throws(() => {
sock.bindSync({ address: 'localhost', port: 0 });
}, {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
sock.close();
}

// Rejects a non-string address.
{
const sock = dgram.createSocket('udp4');
assert.throws(() => sock.bindSync({ address: 12345 }), {
code: 'ERR_INVALID_ARG_TYPE',
});
sock.close();
}

// A rejected argument leaves the socket unbound and reusable.
{
const sock = dgram.createSocket('udp4');
assert.throws(() => sock.bindSync({ port: -1 }), {
code: 'ERR_SOCKET_BAD_PORT',
});
const addr = sock.bindSync({ port: 0 });
assert.ok(addr.port > 0);
sock.close();
}

// Throws when already bound.
{
const sock = dgram.createSocket('udp4');
sock.bindSync({ port: 0 });
assert.throws(() => sock.bindSync({ port: 0 }), {
code: 'ERR_SOCKET_ALREADY_BOUND',
});
sock.close();
}

// Rejects a non-object options argument.
{
const sock = dgram.createSocket('udp4');
assert.throws(() => sock.bindSync(0), { code: 'ERR_INVALID_ARG_TYPE' });
sock.close();
}

// udp6 wildcard default.
if (common.hasIPv6) {
const sock = dgram.createSocket('udp6');
const addr = sock.bindSync();
assert.strictEqual(addr.address, '::');
assert.strictEqual(addr.family, 'IPv6');
assert.ok(addr.port > 0);
sock.close();
}

// udp6 loopback with an OS-assigned ephemeral port, and async 'message' flow.
if (common.hasIPv6) {
const receiver = dgram.createSocket('udp6');
const addr = receiver.bindSync({ address: '::1', port: 0 });

assert.strictEqual(addr.address, '::1');
assert.strictEqual(addr.family, 'IPv6');
assert.ok(addr.port > 0);
assert.deepStrictEqual(receiver.address(), addr);

receiver.on('message', common.mustCall((msg) => {
assert.strictEqual(msg.toString(), 'hello');
receiver.close();
}));

const sender = dgram.createSocket('udp6');
sender.send('hello', addr.port, '::1', common.mustCall(() => {
sender.close();
}));
}

// A zone-indexed (scoped) IPv6 literal is accepted as a numeric IP; no DNS
// resolution occurs. Interface names are platform-specific, so this binds the
// scoped loopback only where the interface name is known (Linux: 'lo').
if (common.hasIPv6 && process.platform === 'linux') {
const sock = dgram.createSocket('udp6');
const addr = sock.bindSync({ address: '::1%lo', port: 0 });
assert.strictEqual(addr.address, '::1');
assert.strictEqual(addr.family, 'IPv6');
assert.ok(addr.port > 0);
sock.close();
}

// The ipv6Only flag is honored by the synchronous bind.
if (common.hasIPv6) {
const sock = dgram.createSocket({ type: 'udp6', ipv6Only: true });
const addr = sock.bindSync({ address: '::', port: 0 });
assert.strictEqual(addr.family, 'IPv6');
sock.close();
}
Loading