From eef1017e799e12cc1b671b6f1b25816ae9c6eb44 Mon Sep 17 00:00:00 2001
From: Santiago Gimeno <santiago.gimeno@gmail.com>
Date: Sat, 16 Mar 2019 23:03:48 +0100
Subject: [PATCH 1/2] dgram: add support for UDP connected sockets

Added the `dgram.connect()` and `dgram.disconnect()` methods that
associate/disassociate a udp socket to/from a remote address.
It optimizes for cases where lots of packets are sent to the same
address.
Also added the `dgram.remoteAddress()` method to retrieve the associated
remote address.
---
 doc/api/dgram.md                              |  75 ++++++-
 doc/api/errors.md                             |  14 ++
 lib/dgram.js                                  | 195 ++++++++++++++++--
 lib/internal/dgram.js                         |   1 +
 lib/internal/errors.js                        |   2 +
 src/udp_wrap.cc                               |  89 +++++++-
 src/udp_wrap.h                                |   5 +
 ...ram-connect-send-callback-buffer-length.js |  24 +++
 ...test-dgram-connect-send-callback-buffer.js |  21 ++
 ...gram-connect-send-callback-multi-buffer.js |  29 +++
 .../test-dgram-connect-send-default-host.js   |  48 +++++
 .../test-dgram-connect-send-empty-array.js    |  22 ++
 .../test-dgram-connect-send-empty-buffer.js   |  20 ++
 .../test-dgram-connect-send-empty-packet.js   |  28 +++
 ...st-dgram-connect-send-multi-buffer-copy.js |  30 +++
 ...t-dgram-connect-send-multi-string-array.js |  17 ++
 test/parallel/test-dgram-connect.js           |  76 +++++++
 .../parallel/test-dgram-send-bad-arguments.js | 103 ++++++---
 18 files changed, 736 insertions(+), 63 deletions(-)
 create mode 100644 test/parallel/test-dgram-connect-send-callback-buffer-length.js
 create mode 100644 test/parallel/test-dgram-connect-send-callback-buffer.js
 create mode 100644 test/parallel/test-dgram-connect-send-callback-multi-buffer.js
 create mode 100644 test/parallel/test-dgram-connect-send-default-host.js
 create mode 100644 test/parallel/test-dgram-connect-send-empty-array.js
 create mode 100644 test/parallel/test-dgram-connect-send-empty-buffer.js
 create mode 100644 test/parallel/test-dgram-connect-send-empty-packet.js
 create mode 100644 test/parallel/test-dgram-connect-send-multi-buffer-copy.js
 create mode 100644 test/parallel/test-dgram-connect-send-multi-string-array.js
 create mode 100644 test/parallel/test-dgram-connect.js

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'
+<!-- YAML
+added: REPLACEME
+-->
+
+The `'connect'` event is emitted after a socket is associated to a remote
+address as a result of a successful [`connect()`][] call.
+
 ### Event: 'error'
 <!-- YAML
 added: v0.1.99
@@ -237,6 +245,34 @@ added: v0.1.99
 Close the underlying socket and stop listening for data on it. If a callback is
 provided, it is added as a listener for the [`'close'`][] event.
 
+### socket.connect(port[, address][, callback])
+<!-- YAML
+added: REPLACEME
+-->
+
+* `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()
+<!-- YAML
+added: REPLACEME
+-->
+
+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])
 <!-- YAML
 added: v0.6.9
@@ -283,7 +319,18 @@ Calling `socket.ref()` multiples times will have no additional effect.
 The `socket.ref()` method returns a reference to the socket so calls can be
 chained.
 
-### socket.send(msg[, offset, length], port[, address][, callback])
+### socket.remoteAddress()
+<!-- YAML
+added: REPLACEME
+-->
+
+* 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])
 <!-- YAML
 added: v0.1.99
 changes:
@@ -301,6 +348,9 @@ changes:
     pr-url: https://github.com/nodejs/node/pull/4374
     description: The `msg` parameter can be an array now. Also, the `offset`
                  and `length` parameters are optional now.
+  - version: REPLACEME
+    pr-url: https://github.com/nodejs/node/pull/26871
+    description: Added support for sending data on connected sockets.
 -->
 
 * `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 2ad0bead8ec0bb..7f0cc36baedbd9 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -1655,6 +1655,17 @@ Data could be sent on a socket.
 
 An attempt was made to operate on an already closed socket.
 
+<a id="ERR_SOCKET_DGRAM_IS_CONNECTED"></a>
+### ERR_SOCKET_DGRAM_IS_CONNECTED
+
+A [`dgram.connect()`][] call was made on an already connected socket.
+
+<a id="ERR_SOCKET_DGRAM_NOT_CONNECTED"></a>
+### ERR_SOCKET_DGRAM_NOT_CONNECTED
+
+A [`dgram.disconnect()`][] or [`dgram.remoteAddress()`][] call was made on a
+disconnected socket.
+
 <a id="ERR_SOCKET_DGRAM_NOT_RUNNING"></a>
 ### ERR_SOCKET_DGRAM_NOT_RUNNING
 
@@ -2298,7 +2309,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<Object> target,
 
   env->SetProtoMethod(t, "open", Open);
   env->SetProtoMethod(t, "bind", Bind);
+  env->SetProtoMethod(t, "connect", Connect);
   env->SetProtoMethod(t, "send", Send);
   env->SetProtoMethod(t, "bind6", Bind6);
+  env->SetProtoMethod(t, "connect6", Connect6);
   env->SetProtoMethod(t, "send6", Send6);
+  env->SetProtoMethod(t, "disconnect", Disconnect);
   env->SetProtoMethod(t, "recvStart", RecvStart);
   env->SetProtoMethod(t, "recvStop", RecvStop);
+  env->SetProtoMethod(t, "getpeername",
+                      GetSockOrPeerName<UDPWrap, uv_udp_getpeername>);
   env->SetProtoMethod(t, "getsockname",
                       GetSockOrPeerName<UDPWrap, uv_udp_getsockname>);
   env->SetProtoMethod(t, "addMembership", AddMembership);
@@ -215,6 +220,30 @@ void UDPWrap::DoBind(const FunctionCallbackInfo<Value>& args, int family) {
 }
 
 
+void UDPWrap::DoConnect(const FunctionCallbackInfo<Value>& args, int family) {
+  UDPWrap* wrap;
+  ASSIGN_OR_RETURN_UNWRAP(&wrap,
+                          args.Holder(),
+                          args.GetReturnValue().Set(UV_EBADF));
+
+  CHECK_EQ(args.Length(), 2);
+
+  node::Utf8Value address(args.GetIsolate(), args[0]);
+  Local<Context> ctx = args.GetIsolate()->GetCurrentContext();
+  uint32_t port;
+  if (!args[1]->Uint32Value(ctx).To(&port))
+    return;
+  struct sockaddr_storage addr_storage;
+  int err = sockaddr_for_family(family, address.out(), port, &addr_storage);
+  if (err == 0) {
+    err = uv_udp_connect(&wrap->handle_,
+                         reinterpret_cast<const sockaddr*>(&addr_storage));
+  }
+
+  args.GetReturnValue().Set(err);
+}
+
+
 void UDPWrap::Open(const FunctionCallbackInfo<Value>& args) {
   UDPWrap* wrap;
   ASSIGN_OR_RETURN_UNWRAP(&wrap,
@@ -273,6 +302,30 @@ void UDPWrap::BufferSize(const FunctionCallbackInfo<Value>& args) {
   args.GetReturnValue().Set(size);
 }
 
+
+void UDPWrap::Connect(const FunctionCallbackInfo<Value>& args) {
+  DoConnect(args, AF_INET);
+}
+
+
+void UDPWrap::Connect6(const FunctionCallbackInfo<Value>& args) {
+  DoConnect(args, AF_INET6);
+}
+
+
+void UDPWrap::Disconnect(const FunctionCallbackInfo<Value>& args) {
+  UDPWrap* wrap;
+  ASSIGN_OR_RETURN_UNWRAP(&wrap,
+                          args.Holder(),
+                          args.GetReturnValue().Set(UV_EBADF));
+
+  CHECK_EQ(args.Length(), 0);
+
+  int err = uv_udp_connect(&wrap->handle_, nullptr);
+
+  args.GetReturnValue().Set(err);
+}
+
 #define X(name, fn)                                                            \
   void UDPWrap::name(const FunctionCallbackInfo<Value>& args) {                \
     UDPWrap* wrap = Unwrap<UDPWrap>(args.Holder());                            \
@@ -353,22 +406,28 @@ void UDPWrap::DoSend(const FunctionCallbackInfo<Value>& args, int family) {
                           args.Holder(),
                           args.GetReturnValue().Set(UV_EBADF));
 
-  // send(req, list, list.length, port, address, hasCallback)
+  CHECK(args.Length() == 4 || args.Length() == 6);
   CHECK(args[0]->IsObject());
   CHECK(args[1]->IsArray());
   CHECK(args[2]->IsUint32());
-  CHECK(args[3]->IsUint32());
-  CHECK(args[4]->IsString());
-  CHECK(args[5]->IsBoolean());
+
+  bool sendto = args.Length() == 6;
+  if (sendto) {
+    // send(req, list, list.length, port, address, hasCallback)
+    CHECK(args[3]->IsUint32());
+    CHECK(args[4]->IsString());
+    CHECK(args[5]->IsBoolean());
+  } else {
+    // send(req, list, list.length, hasCallback)
+    CHECK(args[3]->IsBoolean());
+  }
 
   Local<Object> req_wrap_obj = args[0].As<Object>();
   Local<Array> chunks = args[1].As<Array>();
   // it is faster to fetch the length of the
   // array in js-land
   size_t count = args[2].As<Uint32>()->Value();
-  const unsigned short port = args[3].As<Uint32>()->Value();
-  node::Utf8Value address(env->isolate(), args[4]);
-  const bool have_callback = args[5]->IsTrue();
+  const bool have_callback = sendto ? args[5]->IsTrue() : args[3]->IsTrue();
 
   SendWrap* req_wrap;
   {
@@ -391,14 +450,24 @@ void UDPWrap::DoSend(const FunctionCallbackInfo<Value>& args, int family) {
 
   req_wrap->msg_size = msg_size;
 
-  struct sockaddr_storage addr_storage;
-  int err = sockaddr_for_family(family, address.out(), port, &addr_storage);
+  int err = 0;
+  sockaddr* addr = nullptr;
+  if (sendto) {
+    struct sockaddr_storage addr_storage;
+    const unsigned short port = args[3].As<Uint32>()->Value();
+    node::Utf8Value address(env->isolate(), args[4]);
+    err = sockaddr_for_family(family, address.out(), port, &addr_storage);
+    if (err == 0) {
+      addr = reinterpret_cast<sockaddr*>(&addr_storage);
+    }
+  }
+
   if (err == 0) {
     err = req_wrap->Dispatch(uv_udp_send,
                              &wrap->handle_,
                              *bufs,
                              count,
-                             reinterpret_cast<const sockaddr*>(&addr_storage),
+                             addr,
                              OnSend);
   }
 
diff --git a/src/udp_wrap.h b/src/udp_wrap.h
index 97d95b57d3dd84..fb2a9362cdf2bc 100644
--- a/src/udp_wrap.h
+++ b/src/udp_wrap.h
@@ -45,9 +45,12 @@ class UDPWrap: public HandleWrap {
   static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void Open(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void Bind(const v8::FunctionCallbackInfo<v8::Value>& args);
+  static void Connect(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void Send(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void Bind6(const v8::FunctionCallbackInfo<v8::Value>& args);
+  static void Connect6(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void Send6(const v8::FunctionCallbackInfo<v8::Value>& args);
+  static void Disconnect(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void RecvStart(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void RecvStop(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void AddMembership(const v8::FunctionCallbackInfo<v8::Value>& args);
@@ -79,6 +82,8 @@ class UDPWrap: public HandleWrap {
 
   static void DoBind(const v8::FunctionCallbackInfo<v8::Value>& args,
                      int family);
+  static void DoConnect(const v8::FunctionCallbackInfo<v8::Value>& args,
+                     int family);
   static void DoSend(const v8::FunctionCallbackInfo<v8::Value>& args,
                      int family);
   static void SetMembership(const v8::FunctionCallbackInfo<v8::Value>& args,
diff --git a/test/parallel/test-dgram-connect-send-callback-buffer-length.js b/test/parallel/test-dgram-connect-send-callback-buffer-length.js
new file mode 100644
index 00000000000000..3dc089e7386b5a
--- /dev/null
+++ b/test/parallel/test-dgram-connect-send-callback-buffer-length.js
@@ -0,0 +1,24 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+
+const dgram = require('dgram');
+const client = dgram.createSocket('udp4');
+
+const buf = Buffer.allocUnsafe(256);
+const offset = 20;
+const len = buf.length - offset;
+
+const messageSent = common.mustCall(function messageSent(err, bytes) {
+  assert.ifError(err);
+  assert.notStrictEqual(bytes, buf.length);
+  assert.strictEqual(bytes, buf.length - offset);
+  client.close();
+});
+
+client.bind(0, () => {
+  client.connect(client.address().port, common.mustCall(() => {
+    client.send(buf, offset, len, messageSent);
+  }));
+});
diff --git a/test/parallel/test-dgram-connect-send-callback-buffer.js b/test/parallel/test-dgram-connect-send-callback-buffer.js
new file mode 100644
index 00000000000000..a03d45429b5cf7
--- /dev/null
+++ b/test/parallel/test-dgram-connect-send-callback-buffer.js
@@ -0,0 +1,21 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const dgram = require('dgram');
+
+const client = dgram.createSocket('udp4');
+
+const buf = Buffer.allocUnsafe(256);
+
+const onMessage = common.mustCall(function(err, bytes) {
+  assert.ifError(err);
+  assert.strictEqual(bytes, buf.length);
+  client.close();
+});
+
+client.bind(0, () => {
+  client.connect(client.address().port, common.mustCall(() => {
+    client.send(buf, onMessage);
+  }));
+});
diff --git a/test/parallel/test-dgram-connect-send-callback-multi-buffer.js b/test/parallel/test-dgram-connect-send-callback-multi-buffer.js
new file mode 100644
index 00000000000000..defc73697c1f80
--- /dev/null
+++ b/test/parallel/test-dgram-connect-send-callback-multi-buffer.js
@@ -0,0 +1,29 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const dgram = require('dgram');
+
+const client = dgram.createSocket('udp4');
+
+const messageSent = common.mustCall((err, bytes) => {
+  assert.strictEqual(bytes, buf1.length + buf2.length);
+});
+
+const buf1 = Buffer.alloc(256, 'x');
+const buf2 = Buffer.alloc(256, 'y');
+
+client.on('listening', common.mustCall(() => {
+  const port = client.address().port;
+  client.connect(port, common.mustCall(() => {
+    client.send([buf1, buf2], messageSent);
+  }));
+}));
+
+client.on('message', common.mustCall((buf, info) => {
+  const expected = Buffer.concat([buf1, buf2]);
+  assert.ok(buf.equals(expected), 'message was received correctly');
+  client.close();
+}));
+
+client.bind(0);
diff --git a/test/parallel/test-dgram-connect-send-default-host.js b/test/parallel/test-dgram-connect-send-default-host.js
new file mode 100644
index 00000000000000..cd22cd2b645be5
--- /dev/null
+++ b/test/parallel/test-dgram-connect-send-default-host.js
@@ -0,0 +1,48 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const dgram = require('dgram');
+
+const client = dgram.createSocket('udp4');
+const server = dgram.createSocket('udp4');
+
+const toSend = [Buffer.alloc(256, 'x'),
+                Buffer.alloc(256, 'y'),
+                Buffer.alloc(256, 'z'),
+                'hello'];
+
+const received = [];
+
+server.on('listening', common.mustCall(() => {
+  const port = server.address().port;
+  client.connect(port, (err) => {
+    assert.ifError(err);
+    client.send(toSend[0], 0, toSend[0].length);
+    client.send(toSend[1]);
+    client.send([toSend[2]]);
+    client.send(toSend[3], 0, toSend[3].length);
+
+    client.send(new Uint8Array(toSend[0]), 0, toSend[0].length);
+    client.send(new Uint8Array(toSend[1]));
+    client.send([new Uint8Array(toSend[2])]);
+    client.send(new Uint8Array(Buffer.from(toSend[3])),
+                0, toSend[3].length);
+  });
+}));
+
+server.on('message', common.mustCall((buf, info) => {
+  received.push(buf.toString());
+
+  if (received.length === toSend.length * 2) {
+    // The replies may arrive out of order -> sort them before checking.
+    received.sort();
+
+    const expected = toSend.concat(toSend).map(String).sort();
+    assert.deepStrictEqual(received, expected);
+    client.close();
+    server.close();
+  }
+}, toSend.length * 2));
+
+server.bind(0);
diff --git a/test/parallel/test-dgram-connect-send-empty-array.js b/test/parallel/test-dgram-connect-send-empty-array.js
new file mode 100644
index 00000000000000..7727e43041f184
--- /dev/null
+++ b/test/parallel/test-dgram-connect-send-empty-array.js
@@ -0,0 +1,22 @@
+'use strict';
+
+const common = require('../common');
+
+const assert = require('assert');
+const dgram = require('dgram');
+
+const client = dgram.createSocket('udp4');
+
+client.on('message', common.mustCall((buf, info) => {
+  const expected = Buffer.alloc(0);
+  assert.ok(buf.equals(expected), `Expected empty message but got ${buf}`);
+  client.close();
+}));
+
+client.on('listening', common.mustCall(() => {
+  client.connect(client.address().port,
+                 common.localhostIPv4,
+                 common.mustCall(() => client.send([])));
+}));
+
+client.bind(0);
diff --git a/test/parallel/test-dgram-connect-send-empty-buffer.js b/test/parallel/test-dgram-connect-send-empty-buffer.js
new file mode 100644
index 00000000000000..c41ce8309f59bd
--- /dev/null
+++ b/test/parallel/test-dgram-connect-send-empty-buffer.js
@@ -0,0 +1,20 @@
+'use strict';
+const common = require('../common');
+
+const assert = require('assert');
+const dgram = require('dgram');
+
+const client = dgram.createSocket('udp4');
+
+client.bind(0, common.mustCall(function() {
+  const port = this.address().port;
+  client.connect(port, common.mustCall(() => {
+    const buf = Buffer.alloc(0);
+    client.send(buf, 0, 0, common.mustCall());
+  }));
+
+  client.on('message', common.mustCall((buffer) => {
+    assert.strictEqual(buffer.length, 0);
+    client.close();
+  }));
+}));
diff --git a/test/parallel/test-dgram-connect-send-empty-packet.js b/test/parallel/test-dgram-connect-send-empty-packet.js
new file mode 100644
index 00000000000000..577a9cbefdd7a4
--- /dev/null
+++ b/test/parallel/test-dgram-connect-send-empty-packet.js
@@ -0,0 +1,28 @@
+'use strict';
+const common = require('../common');
+
+const assert = require('assert');
+const dgram = require('dgram');
+
+const client = dgram.createSocket('udp4');
+
+client.bind(0, common.mustCall(function() {
+  client.connect(client.address().port, common.mustCall(() => {
+    client.on('message', common.mustCall(callback));
+    const buf = Buffer.alloc(1);
+
+    const interval = setInterval(function() {
+      client.send(buf, 0, 0, common.mustCall(callback));
+    }, 10);
+
+    function callback(firstArg) {
+      // If client.send() callback, firstArg should be null.
+      // If client.on('message') listener, firstArg should be a 0-length buffer.
+      if (firstArg instanceof Buffer) {
+        assert.strictEqual(firstArg.length, 0);
+        clearInterval(interval);
+        client.close();
+      }
+    }
+  }));
+}));
diff --git a/test/parallel/test-dgram-connect-send-multi-buffer-copy.js b/test/parallel/test-dgram-connect-send-multi-buffer-copy.js
new file mode 100644
index 00000000000000..24de99a1e9d61d
--- /dev/null
+++ b/test/parallel/test-dgram-connect-send-multi-buffer-copy.js
@@ -0,0 +1,30 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const dgram = require('dgram');
+
+const client = dgram.createSocket('udp4');
+
+const onMessage = common.mustCall(common.mustCall((err, bytes) => {
+  assert.ifError(err);
+  assert.strictEqual(bytes, buf1.length + buf2.length);
+}));
+
+const buf1 = Buffer.alloc(256, 'x');
+const buf2 = Buffer.alloc(256, 'y');
+
+client.on('listening', function() {
+  const toSend = [buf1, buf2];
+  client.connect(client.address().port, common.mustCall(() => {
+    client.send(toSend, onMessage);
+  }));
+});
+
+client.on('message', common.mustCall(function onMessage(buf, info) {
+  const expected = Buffer.concat([buf1, buf2]);
+  assert.ok(buf.equals(expected), 'message was received correctly');
+  client.close();
+}));
+
+client.bind(0);
diff --git a/test/parallel/test-dgram-connect-send-multi-string-array.js b/test/parallel/test-dgram-connect-send-multi-string-array.js
new file mode 100644
index 00000000000000..e69aa82d47f451
--- /dev/null
+++ b/test/parallel/test-dgram-connect-send-multi-string-array.js
@@ -0,0 +1,17 @@
+'use strict';
+const common = require('../common');
+const assert = require('assert');
+const dgram = require('dgram');
+const socket = dgram.createSocket('udp4');
+const data = ['foo', 'bar', 'baz'];
+
+socket.on('message', common.mustCall((msg, rinfo) => {
+  socket.close();
+  assert.deepStrictEqual(msg.toString(), data.join(''));
+}));
+
+socket.bind(0, () => {
+  socket.connect(socket.address().port, common.mustCall(() => {
+    socket.send(data);
+  }));
+});
diff --git a/test/parallel/test-dgram-connect.js b/test/parallel/test-dgram-connect.js
new file mode 100644
index 00000000000000..16c0c35941a86e
--- /dev/null
+++ b/test/parallel/test-dgram-connect.js
@@ -0,0 +1,76 @@
+'use strict';
+
+const common = require('../common');
+const { addresses } = require('../common/internet');
+const assert = require('assert');
+const dgram = require('dgram');
+
+const PORT = 12345;
+
+const client = dgram.createSocket('udp4');
+client.connect(PORT, common.mustCall(() => {
+  const remoteAddr = client.remoteAddress();
+  assert.strictEqual(remoteAddr.port, PORT);
+  assert.throws(() => {
+    client.connect(PORT, common.mustNotCall());
+  }, {
+    name: 'Error',
+    message: 'Already connected',
+    code: 'ERR_SOCKET_DGRAM_IS_CONNECTED'
+  });
+
+  client.disconnect();
+  assert.throws(() => {
+    client.disconnect();
+  }, {
+    name: 'Error',
+    message: 'Not connected',
+    code: 'ERR_SOCKET_DGRAM_NOT_CONNECTED'
+  });
+
+  assert.throws(() => {
+    client.remoteAddress();
+  }, {
+    name: 'Error',
+    message: 'Not connected',
+    code: 'ERR_SOCKET_DGRAM_NOT_CONNECTED'
+  });
+
+  client.connect(PORT, addresses.INVALID_HOST, common.mustCall((err) => {
+    assert.strictEqual(err.code, 'ENOTFOUND');
+
+    client.once('error', common.mustCall((err) => {
+      assert.strictEqual(err.code, 'ENOTFOUND');
+      client.once('connect', common.mustCall(() => client.close()));
+      client.connect(PORT);
+    }));
+
+    client.connect(PORT, addresses.INVALID_HOST);
+  }));
+}));
+
+assert.throws(() => {
+  client.connect(PORT);
+}, {
+  name: 'Error',
+  message: 'Already connected',
+  code: 'ERR_SOCKET_DGRAM_IS_CONNECTED'
+});
+
+assert.throws(() => {
+  client.disconnect();
+}, {
+  name: 'Error',
+  message: 'Not connected',
+  code: 'ERR_SOCKET_DGRAM_NOT_CONNECTED'
+});
+
+[ 0, null, 78960, undefined ].forEach((port) => {
+  assert.throws(() => {
+    client.connect(port);
+  }, {
+    name: 'RangeError',
+    message: /^Port should be >= 0 and < 65536/,
+    code: 'ERR_SOCKET_BAD_PORT'
+  });
+});
diff --git a/test/parallel/test-dgram-send-bad-arguments.js b/test/parallel/test-dgram-send-bad-arguments.js
index 20356ba50c477a..467efbb7b94a2c 100644
--- a/test/parallel/test-dgram-send-bad-arguments.js
+++ b/test/parallel/test-dgram-send-bad-arguments.js
@@ -28,40 +28,77 @@ const buf = Buffer.from('test');
 const host = '127.0.0.1';
 const sock = dgram.createSocket('udp4');
 
-// First argument should be a buffer.
-common.expectsError(
-  () => { sock.send(); },
-  {
-    code: 'ERR_INVALID_ARG_TYPE',
-    type: TypeError,
-    message: 'The "buffer" argument must be one of type ' +
-    'Buffer, Uint8Array, or string. Received type undefined'
-  }
-);
+function checkArgs(connected) {
+  // First argument should be a buffer.
+  common.expectsError(
+    () => { sock.send(); },
+    {
+      code: 'ERR_INVALID_ARG_TYPE',
+      type: TypeError,
+      message: 'The "buffer" argument must be one of type ' +
+      'Buffer, Uint8Array, or string. Received type undefined'
+    }
+  );
 
-// send(buf, offset, length, port, host)
-assert.throws(() => { sock.send(buf, 1, 1, -1, host); }, RangeError);
-assert.throws(() => { sock.send(buf, 1, 1, 0, host); }, RangeError);
-assert.throws(() => { sock.send(buf, 1, 1, 65536, host); }, RangeError);
+  // send(buf, offset, length, port, host)
+  if (connected) {
+    common.expectsError(
+      () => { sock.send(buf, 1, 1, -1, host); },
+      {
+        code: 'ERR_SOCKET_DGRAM_IS_CONNECTED',
+        type: Error,
+        message: 'Already connected'
+      }
+    );
 
-// send(buf, port, host)
-common.expectsError(
-  () => { sock.send(23, 12345, host); },
-  {
-    code: 'ERR_INVALID_ARG_TYPE',
-    type: TypeError,
-    message: 'The "buffer" argument must be one of type ' +
-    'Buffer, Uint8Array, or string. Received type number'
-  }
-);
+    common.expectsError(
+      () => { sock.send(buf, 1, 1, 0, host); },
+      {
+        code: 'ERR_SOCKET_DGRAM_IS_CONNECTED',
+        type: Error,
+        message: 'Already connected'
+      }
+    );
 
-// send([buf1, ..], port, host)
-common.expectsError(
-  () => { sock.send([buf, 23], 12345, host); },
-  {
-    code: 'ERR_INVALID_ARG_TYPE',
-    type: TypeError,
-    message: 'The "buffer list arguments" argument must be one of type ' +
-    'Buffer or string. Received type object'
+    common.expectsError(
+      () => { sock.send(buf, 1, 1, 65536, host); },
+      {
+        code: 'ERR_SOCKET_DGRAM_IS_CONNECTED',
+        type: Error,
+        message: 'Already connected'
+      }
+    );
+  } else {
+    assert.throws(() => { sock.send(buf, 1, 1, -1, host); }, RangeError);
+    assert.throws(() => { sock.send(buf, 1, 1, 0, host); }, RangeError);
+    assert.throws(() => { sock.send(buf, 1, 1, 65536, host); }, RangeError);
   }
-);
+
+  // send(buf, port, host)
+  common.expectsError(
+    () => { sock.send(23, 12345, host); },
+    {
+      code: 'ERR_INVALID_ARG_TYPE',
+      type: TypeError,
+      message: 'The "buffer" argument must be one of type ' +
+      'Buffer, Uint8Array, or string. Received type number'
+    }
+  );
+
+  // send([buf1, ..], port, host)
+  common.expectsError(
+    () => { sock.send([buf, 23], 12345, host); },
+    {
+      code: 'ERR_INVALID_ARG_TYPE',
+      type: TypeError,
+      message: 'The "buffer list arguments" argument must be one of type ' +
+      'Buffer or string. Received type object'
+    }
+  );
+}
+
+checkArgs();
+sock.connect(12345, common.mustCall(() => {
+  checkArgs(true);
+  sock.close();
+}));

From 25b3daa0579cd0e0f612613f1c8ef58f28515696 Mon Sep 17 00:00:00 2001
From: Santiago Gimeno <santiago.gimeno@gmail.com>
Date: Tue, 2 Apr 2019 07:00:00 +0200
Subject: [PATCH 2/2] fixup: allow EAI_AGAIN as a valid error code

---
 test/parallel/test-dgram-connect.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/parallel/test-dgram-connect.js b/test/parallel/test-dgram-connect.js
index 16c0c35941a86e..8fba27bcd2ac83 100644
--- a/test/parallel/test-dgram-connect.js
+++ b/test/parallel/test-dgram-connect.js
@@ -37,10 +37,10 @@ client.connect(PORT, common.mustCall(() => {
   });
 
   client.connect(PORT, addresses.INVALID_HOST, common.mustCall((err) => {
-    assert.strictEqual(err.code, 'ENOTFOUND');
+    assert.ok(err.code === 'ENOTFOUND' || err.code === 'EAI_AGAIN');
 
     client.once('error', common.mustCall((err) => {
-      assert.strictEqual(err.code, 'ENOTFOUND');
+      assert.ok(err.code === 'ENOTFOUND' || err.code === 'EAI_AGAIN');
       client.once('connect', common.mustCall(() => client.close()));
       client.connect(PORT);
     }));