diff --git a/doc/api/dgram.md b/doc/api/dgram.md
index 2d44074df802ca..7ef483bc23ba15 100644
--- a/doc/api/dgram.md
+++ b/doc/api/dgram.md
@@ -49,6 +49,14 @@ added: v0.1.99
The `'close'` event is emitted after a socket is closed with [`close()`][].
Once triggered, no new `'message'` events will be emitted on this socket.
+### Event: 'connect'
+
+
+The `'connect'` event is emitted after a socket is associated to a remote
+address as a result of a successful [`connect()`][] call.
+
### Event: 'error'
+
+* `port` {integer}
+* `address` {string}
+* `callback` {Function} Called when the connection is completed or on error.
+
+Associates the `dgram.Socket` to a remote address and port. Every
+message sent by this handle is automatically sent to that destination. Also,
+the socket will only receive messages from that remote peer.
+Trying to call `connect()` on an already connected socket will result
+in an [`ERR_SOCKET_DGRAM_IS_CONNECTED`][] exception. If `address` is not
+provided, `'127.0.0.1'` (for `udp4` sockets) or `'::1'` (for `udp6` sockets)
+will be used by default. Once the connection is complete, a `'connect'` event
+is emitted and the optional `callback` function is called. In case of failure,
+the `callback` is called or, failing this, an `'error'` event is emitted.
+
+### socket.disconnect()
+
+
+A synchronous function that disassociates a connected `dgram.Socket` from
+its remote address. Trying to call `disconnect()` on an already disconnected
+socket will result in an [`ERR_SOCKET_DGRAM_NOT_CONNECTED`][] exception.
+
### socket.dropMembership(multicastAddress[, multicastInterface])
+
+* Returns: {Object}
+
+Returns an object containing the `address`, `family`, and `port` of the remote
+endpoint. It throws an [`ERR_SOCKET_DGRAM_NOT_CONNECTED`][] exception if the
+socket is not connected.
+
+### socket.send(msg[, offset, length][, port][, address][, callback])
* `msg` {Buffer|Uint8Array|string|Array} Message to be sent.
@@ -310,8 +360,10 @@ changes:
* `address` {string} Destination hostname or IP address.
* `callback` {Function} Called when the message has been sent.
-Broadcasts a datagram on the socket. The destination `port` and `address` must
-be specified.
+Broadcasts a datagram on the socket.
+For connectionless sockets, the destination `port` and `address` must be
+specified. Connected sockets, on the other hand, will use their associated
+remote endpoint, so the `port` and `address` arguments must not be set.
The `msg` argument contains the message to be sent.
Depending on its type, different behavior can apply. If `msg` is a `Buffer`
@@ -375,6 +427,20 @@ application and operating system. It is important to run benchmarks to
determine the optimal strategy on a case-by-case basis. Generally speaking,
however, sending multiple buffers is faster.
+Example of sending a UDP packet using a socket connected to a port on
+`localhost`:
+
+```js
+const dgram = require('dgram');
+const message = Buffer.from('Some bytes');
+const client = dgram.createSocket('udp4');
+client.connect(41234, 'localhost', (err) => {
+ client.send(message, (err) => {
+ client.close();
+ });
+});
+```
+
**A Note about UDP datagram size**
The maximum size of an `IPv4/v6` datagram depends on the `MTU`
@@ -651,10 +717,13 @@ and `udp6` sockets). The bound address and port can be retrieved using
[`'close'`]: #dgram_event_close
[`Error`]: errors.html#errors_class_error
+[`ERR_SOCKET_DGRAM_IS_CONNECTED`]: errors.html#errors_err_socket_dgram_is_connected
+[`ERR_SOCKET_DGRAM_NOT_CONNECTED`]: errors.html#errors_err_socket_dgram_not_connected
[`EventEmitter`]: events.html
[`System Error`]: errors.html#errors_class_systemerror
[`close()`]: #dgram_socket_close_callback
[`cluster`]: cluster.html
+[`connect()`]: #dgram_socket_connect_port_address_callback
[`dgram.Socket#bind()`]: #dgram_socket_bind_options_callback
[`dgram.createSocket()`]: #dgram_dgram_createsocket_options_callback
[`dns.lookup()`]: dns.html#dns_dns_lookup_hostname_options_callback
diff --git a/doc/api/errors.md b/doc/api/errors.md
index 6a47b0e8096e4a..a1fb20e58fcf96 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -1645,6 +1645,17 @@ Data could be sent on a socket.
An attempt was made to operate on an already closed socket.
+
+### ERR_SOCKET_DGRAM_IS_CONNECTED
+
+A [`dgram.connect()`][] call was made on an already connected socket.
+
+
+### ERR_SOCKET_DGRAM_NOT_CONNECTED
+
+A [`dgram.disconnect()`][] or [`dgram.remoteAddress()`][] call was made on a
+disconnected socket.
+
### ERR_SOCKET_DGRAM_NOT_RUNNING
@@ -2288,7 +2299,10 @@ such as `process.stdout.on('data')`.
[`crypto.scrypt()`]: crypto.html#crypto_crypto_scrypt_password_salt_keylen_options_callback
[`crypto.scryptSync()`]: crypto.html#crypto_crypto_scryptsync_password_salt_keylen_options
[`crypto.timingSafeEqual()`]: crypto.html#crypto_crypto_timingsafeequal_a_b
+[`dgram.connect()`]: dgram.html#dgram_socket_connect_port_address_callback
[`dgram.createSocket()`]: dgram.html#dgram_dgram_createsocket_options_callback
+[`dgram.disconnect()`]: dgram.html#dgram_socket_disconnect
+[`dgram.remoteAddress()`]: dgram.html#dgram_socket_remoteaddress
[`errno`(3) man page]: http://man7.org/linux/man-pages/man3/errno.3.html
[`fs.readFileSync`]: fs.html#fs_fs_readfilesync_path_options
[`fs.readdir`]: fs.html#fs_fs_readdir_path_options_callback
diff --git a/lib/dgram.js b/lib/dgram.js
index 24297b34d41a02..54cbc0faed32aa 100644
--- a/lib/dgram.js
+++ b/lib/dgram.js
@@ -28,6 +28,9 @@ const {
newHandle,
guessHandleType,
} = require('internal/dgram');
+const {
+ isLegalPort,
+} = require('internal/net');
const {
ERR_INVALID_ARG_TYPE,
ERR_MISSING_ARGS,
@@ -36,6 +39,8 @@ const {
ERR_SOCKET_BAD_PORT,
ERR_SOCKET_BUFFER_SIZE,
ERR_SOCKET_CANNOT_SEND,
+ ERR_SOCKET_DGRAM_IS_CONNECTED,
+ ERR_SOCKET_DGRAM_NOT_CONNECTED,
ERR_SOCKET_DGRAM_NOT_RUNNING,
ERR_INVALID_FD_TYPE
} = errors.codes;
@@ -64,6 +69,10 @@ const BIND_STATE_UNBOUND = 0;
const BIND_STATE_BINDING = 1;
const BIND_STATE_BOUND = 2;
+const CONNECT_STATE_DISCONNECTED = 0;
+const CONNECT_STATE_CONNECTING = 1;
+const CONNECT_STATE_CONNECTED = 2;
+
const RECV_BUFFER = true;
const SEND_BUFFER = false;
@@ -101,6 +110,7 @@ function Socket(type, listener) {
handle,
receiving: false,
bindState: BIND_STATE_UNBOUND,
+ connectState: CONNECT_STATE_DISCONNECTED,
queue: undefined,
reuseAddr: options && options.reuseAddr, // Use UV_UDP_REUSEADDR if true.
ipv6Only: options && options.ipv6Only,
@@ -148,6 +158,9 @@ function replaceHandle(self, newHandle) {
// Replace the existing handle by the handle we got from master.
oldHandle.close();
state.handle = newHandle;
+ // Check if the udp handle was connected and set the state accordingly
+ if (isConnected(self))
+ state.connectState = CONNECT_STATE_CONNECTED;
}
function bufferSize(self, size, buffer) {
@@ -238,6 +251,10 @@ Socket.prototype.bind = function(port_, address_ /* , callback */) {
if (err)
throw errnoException(err, 'open');
+ // Check if the udp handle was connected and set the state accordingly
+ if (isConnected(this))
+ state.connectState = CONNECT_STATE_CONNECTED;
+
startListening(this);
return this;
}
@@ -313,6 +330,106 @@ Socket.prototype.bind = function(port_, address_ /* , callback */) {
};
+function validatePort(port) {
+ const legal = isLegalPort(port);
+ if (legal)
+ port = port | 0;
+
+ if (!legal || port === 0)
+ throw new ERR_SOCKET_BAD_PORT(port);
+
+ return port;
+}
+
+
+Socket.prototype.connect = function(port, address, callback) {
+ port = validatePort(port);
+ if (typeof address === 'function') {
+ callback = address;
+ address = '';
+ } else if (address === undefined) {
+ address = '';
+ }
+
+ validateString(address, 'address');
+
+ const state = this[kStateSymbol];
+
+ if (state.connectState !== CONNECT_STATE_DISCONNECTED)
+ throw new ERR_SOCKET_DGRAM_IS_CONNECTED();
+
+ state.connectState = CONNECT_STATE_CONNECTING;
+ if (state.bindState === BIND_STATE_UNBOUND)
+ this.bind({ port: 0, exclusive: true }, null);
+
+ if (state.bindState !== BIND_STATE_BOUND) {
+ enqueue(this, _connect.bind(this, port, address, callback));
+ return;
+ }
+
+ _connect.call(this, port, address, callback);
+};
+
+
+function _connect(port, address, callback) {
+ const state = this[kStateSymbol];
+ if (callback)
+ this.once('connect', callback);
+
+ const afterDns = (ex, ip) => {
+ defaultTriggerAsyncIdScope(
+ this[async_id_symbol],
+ doConnect,
+ ex, this, ip, address, port, callback
+ );
+ };
+
+ state.handle.lookup(address, afterDns);
+}
+
+
+function doConnect(ex, self, ip, address, port, callback) {
+ const state = self[kStateSymbol];
+ if (!state.handle)
+ return;
+
+ if (!ex) {
+ const err = state.handle.connect(ip, port);
+ if (err) {
+ ex = exceptionWithHostPort(err, 'connect', address, port);
+ }
+ }
+
+ if (ex) {
+ state.connectState = CONNECT_STATE_DISCONNECTED;
+ return process.nextTick(() => {
+ if (callback) {
+ self.removeListener('connect', callback);
+ callback(ex);
+ } else {
+ self.emit('error', ex);
+ }
+ });
+ }
+
+ state.connectState = CONNECT_STATE_CONNECTED;
+ process.nextTick(() => self.emit('connect'));
+}
+
+
+Socket.prototype.disconnect = function() {
+ const state = this[kStateSymbol];
+ if (state.connectState !== CONNECT_STATE_CONNECTED)
+ throw new ERR_SOCKET_DGRAM_NOT_CONNECTED();
+
+ const err = state.handle.disconnect();
+ if (err)
+ throw errnoException(err, 'connect');
+ else
+ state.connectState = CONNECT_STATE_DISCONNECTED;
+};
+
+
// Thin wrapper around `send`, here for compatibility with dgram_legacy.js
Socket.prototype.sendto = function(buffer,
offset,
@@ -398,8 +515,18 @@ function clearQueue() {
queue[i]();
}
+function isConnected(self) {
+ try {
+ this.remoteAddress();
+ return true;
+ } catch {
+ return false;
+ }
+}
+
// valid combinations
+// For connectionless sockets
// send(buffer, offset, length, port, address, callback)
// send(buffer, offset, length, port, address)
// send(buffer, offset, length, port, callback)
@@ -408,20 +535,39 @@ function clearQueue() {
// send(bufferOrList, port, address)
// send(bufferOrList, port, callback)
// send(bufferOrList, port)
+// For connected sockets
+// send(buffer, offset, length, callback)
+// send(buffer, offset, length)
+// send(bufferOrList, callback)
+// send(bufferOrList)
Socket.prototype.send = function(buffer,
offset,
length,
port,
address,
callback) {
- let list;
- if (address || (port && typeof port !== 'function')) {
+ let list;
+ const state = this[kStateSymbol];
+ const connected = state.connectState === CONNECT_STATE_CONNECTED;
+ if (!connected) {
+ if (address || (port && typeof port !== 'function')) {
+ buffer = sliceBuffer(buffer, offset, length);
+ } else {
+ callback = port;
+ port = offset;
+ address = length;
+ }
+ } else if (typeof length === 'number') {
buffer = sliceBuffer(buffer, offset, length);
+ if (typeof port === 'function') {
+ callback = port;
+ port = null;
+ } else if (port || address) {
+ throw new ERR_SOCKET_DGRAM_IS_CONNECTED();
+ }
} else {
- callback = port;
- port = offset;
- address = length;
+ callback = offset;
}
if (!Array.isArray(buffer)) {
@@ -439,9 +585,8 @@ Socket.prototype.send = function(buffer,
['Buffer', 'string'], buffer);
}
- port = port >>> 0;
- if (port === 0 || port > 65535)
- throw new ERR_SOCKET_BAD_PORT(port);
+ if (!connected)
+ port = validatePort(port);
// Normalize callback so it's either a function or undefined but not anything
// else.
@@ -457,8 +602,6 @@ Socket.prototype.send = function(buffer,
healthCheck(this);
- const state = this[kStateSymbol];
-
if (state.bindState === BIND_STATE_UNBOUND)
this.bind({ port: 0, exclusive: true }, null);
@@ -480,7 +623,11 @@ Socket.prototype.send = function(buffer,
);
};
- state.handle.lookup(address, afterDns);
+ if (!connected) {
+ state.handle.lookup(address, afterDns);
+ } else {
+ afterDns(null, null);
+ }
};
function doSend(ex, self, ip, list, address, port, callback) {
@@ -507,12 +654,11 @@ function doSend(ex, self, ip, list, address, port, callback) {
req.oncomplete = afterSend;
}
- const err = state.handle.send(req,
- list,
- list.length,
- port,
- ip,
- !!callback);
+ let err;
+ if (port)
+ err = state.handle.send(req, list, list.length, port, ip, !!callback);
+ else
+ err = state.handle.send(req, list, list.length, !!callback);
if (err && callback) {
// Don't emit as error, dgram_legacy.js compatibility
@@ -573,6 +719,21 @@ Socket.prototype.address = function() {
return out;
};
+Socket.prototype.remoteAddress = function() {
+ healthCheck(this);
+
+ const state = this[kStateSymbol];
+ if (state.connectState !== CONNECT_STATE_CONNECTED)
+ throw new ERR_SOCKET_DGRAM_NOT_CONNECTED();
+
+ var out = {};
+ var err = state.handle.getpeername(out);
+ if (err)
+ throw errnoException(err, 'getpeername');
+
+ return out;
+};
+
Socket.prototype.setBroadcast = function(arg) {
const err = this[kStateSymbol].handle.setBroadcast(arg ? 1 : 0);
diff --git a/lib/internal/dgram.js b/lib/internal/dgram.js
index 9dac221301d9dd..bb5e25a62e2197 100644
--- a/lib/internal/dgram.js
+++ b/lib/internal/dgram.js
@@ -45,6 +45,7 @@ function newHandle(type, lookup) {
handle.lookup = lookup6.bind(handle, lookup);
handle.bind = handle.bind6;
+ handle.connect = handle.connect6;
handle.send = handle.send6;
return handle;
}
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index c7dac93783fed3..3eb387d5896b80 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -1019,6 +1019,8 @@ E('ERR_SOCKET_BUFFER_SIZE',
SystemError);
E('ERR_SOCKET_CANNOT_SEND', 'Unable to send data', Error);
E('ERR_SOCKET_CLOSED', 'Socket is closed', Error);
+E('ERR_SOCKET_DGRAM_IS_CONNECTED', 'Already connected', Error);
+E('ERR_SOCKET_DGRAM_NOT_CONNECTED', 'Not connected', Error);
E('ERR_SOCKET_DGRAM_NOT_RUNNING', 'Not running', Error);
E('ERR_SRI_PARSE',
'Subresource Integrity string %s had an unexpected at %d',
diff --git a/src/udp_wrap.cc b/src/udp_wrap.cc
index e568bb66a6682d..c4be8a6068b097 100644
--- a/src/udp_wrap.cc
+++ b/src/udp_wrap.cc
@@ -114,11 +114,16 @@ void UDPWrap::Initialize(Local