From a607d87432473b26ae0014dd807ce4209294aec3 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Wed, 8 Jul 2026 16:35:27 -0700 Subject: [PATCH] net: support sync connect for BoundSocket When a net.Socket is constructed from a net.BoundSocket, support synchronous connect(). Signed-off-by: Guy Bedford --- doc/api/net.md | 5 ++++ lib/net.js | 18 +++++++++++++- test/parallel/test-net-boundsocket.js | 36 +++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/doc/api/net.md b/doc/api/net.md index e2f06776409898..3238909f918114 100644 --- a/doc/api/net.md +++ b/doc/api/net.md @@ -1657,6 +1657,10 @@ Adoption transfers ownership of the socket; afterwards `address()` and `close()` throw [`ERR_SOCKET_HANDLE_ADOPTED`][]. A handle that is never adopted must be closed to avoid leaking the socket. +When an adopted `BoundSocket` connects to a numeric IP literal, `connect(2)` is +issued synchronously: [`socket.localAddress`][] is resolved once +[`socket.connect()`][] returns, and failures throw synchronously. + ```mjs import net from 'node:net'; @@ -2241,6 +2245,7 @@ net.isIPv6('fhqwhgads'); // returns false [`socket.connecting`]: #socketconnecting [`socket.destroy()`]: #socketdestroyerror [`socket.end()`]: #socketenddata-encoding-callback +[`socket.localAddress`]: #socketlocaladdress [`socket.pause()`]: #socketpause [`socket.resume()`]: #socketresume [`socket.setEncoding()`]: #socketsetencodingencoding diff --git a/lib/net.js b/lib/net.js index fa685769422186..c52e780f1c7c4f 100644 --- a/lib/net.js +++ b/lib/net.js @@ -1249,7 +1249,7 @@ function checkBindError(err, port, handle) { function internalConnect( - self, address, port, addressType, localAddress, localPort, flags) { + self, address, port, addressType, localAddress, localPort, flags, boundSocket) { // TODO return promise from Socket.prototype.connect which // wraps _connectReq. @@ -1313,6 +1313,12 @@ function internalConnect( } const ex = new ExceptionWithHostPort(err, 'connect', address, port, details); + // Synchronous connect(2): throw in-tick instead of a deferred 'error'. + if (boundSocket) { + self.connecting = false; + self.destroy(); + throw ex; + } self.destroy(ex); } else if ((addressType === 6 || addressType === 4) && hasObserver('net')) { startPerf(self, kPerfHooksNetConnectContext, { type: 'net', name: 'connect', detail: { host: address, port } }); @@ -1567,6 +1573,16 @@ function lookupAndConnect(self, options) { // If host is an IP, skip performing a lookup const addressType = isIP(host); if (addressType) { + // An adopted BoundSocket needs no lookup: issue connect(2) in-tick so the + // concrete source address resolves and failures throw synchronously. + if (self[kBoundSource]) { + defaultTriggerAsyncIdScope( + self[async_id_symbol], + internalConnect, + self, host, port, addressType, localAddress, localPort, 0, true, + ); + return; + } defaultTriggerAsyncIdScope(self[async_id_symbol], process.nextTick, () => { if (self.connecting) defaultTriggerAsyncIdScope( diff --git a/test/parallel/test-net-boundsocket.js b/test/parallel/test-net-boundsocket.js index c987faca0e9ecc..bc2e6726237b82 100644 --- a/test/parallel/test-net-boundsocket.js +++ b/test/parallel/test-net-boundsocket.js @@ -178,6 +178,42 @@ if (!common.isWindows && process.getuid() !== 0) { client.destroy(); } +// Adopted BoundSocket: connect(2) is synchronous, so localAddress/localPort +// resolve to the concrete source in-tick, before the 'connect' event. +{ + const server = net.createServer(); + server.listen(0, '127.0.0.1', common.mustCall(() => { + const serverPort = server.address().port; + const bound = new net.BoundSocket({ host: '127.0.0.1', port: 0 }); + const localPort = bound.address().port; + const client = new net.Socket({ handle: bound }); + + client.connect({ host: '127.0.0.1', port: serverPort }); + + assert.strictEqual(client.localAddress, '127.0.0.1'); + assert.strictEqual(client.localPort, localPort); + + client.on('connect', common.mustCall(() => { + assert.strictEqual(client.localAddress, '127.0.0.1'); + assert.strictEqual(client.localPort, localPort); + client.destroy(); + server.close(); + })); + })); +} + +// A synchronous connect(2) failure throws in-tick, without an 'error' event. +// An IPv4-bound handle connecting to an IPv6 literal fails on family mismatch. +{ + const bound = new net.BoundSocket({ host: '127.0.0.1', port: 0 }); + const client = new net.Socket({ handle: bound }); + client.on('error', common.mustNotCall()); + assert.throws(() => { + client.connect({ host: '::1', port: 1 }); + }, { syscall: 'connect' }); + assert.strictEqual(client.destroyed, true); +} + // reusePort: SO_REUSEPORT permits multiple listeners on the same port. Support // is platform-dependent, so probe first. {