Skip to content

Commit a607d87

Browse files
committed
net: support sync connect for BoundSocket
When a net.Socket is constructed from a net.BoundSocket, support synchronous connect(). Signed-off-by: Guy Bedford <guybedford@gmail.com>
1 parent 0032189 commit a607d87

3 files changed

Lines changed: 58 additions & 1 deletion

File tree

doc/api/net.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1657,6 +1657,10 @@ Adoption transfers ownership of the socket; afterwards `address()` and `close()`
16571657
throw [`ERR_SOCKET_HANDLE_ADOPTED`][]. A handle that is never adopted must be
16581658
closed to avoid leaking the socket.
16591659

1660+
When an adopted `BoundSocket` connects to a numeric IP literal, `connect(2)` is
1661+
issued synchronously: [`socket.localAddress`][] is resolved once
1662+
[`socket.connect()`][] returns, and failures throw synchronously.
1663+
16601664
```mjs
16611665
import net from 'node:net';
16621666

@@ -2241,6 +2245,7 @@ net.isIPv6('fhqwhgads'); // returns false
22412245
[`socket.connecting`]: #socketconnecting
22422246
[`socket.destroy()`]: #socketdestroyerror
22432247
[`socket.end()`]: #socketenddata-encoding-callback
2248+
[`socket.localAddress`]: #socketlocaladdress
22442249
[`socket.pause()`]: #socketpause
22452250
[`socket.resume()`]: #socketresume
22462251
[`socket.setEncoding()`]: #socketsetencodingencoding

lib/net.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1249,7 +1249,7 @@ function checkBindError(err, port, handle) {
12491249

12501250

12511251
function internalConnect(
1252-
self, address, port, addressType, localAddress, localPort, flags) {
1252+
self, address, port, addressType, localAddress, localPort, flags, boundSocket) {
12531253
// TODO return promise from Socket.prototype.connect which
12541254
// wraps _connectReq.
12551255

@@ -1313,6 +1313,12 @@ function internalConnect(
13131313
}
13141314

13151315
const ex = new ExceptionWithHostPort(err, 'connect', address, port, details);
1316+
// Synchronous connect(2): throw in-tick instead of a deferred 'error'.
1317+
if (boundSocket) {
1318+
self.connecting = false;
1319+
self.destroy();
1320+
throw ex;
1321+
}
13161322
self.destroy(ex);
13171323
} else if ((addressType === 6 || addressType === 4) && hasObserver('net')) {
13181324
startPerf(self, kPerfHooksNetConnectContext, { type: 'net', name: 'connect', detail: { host: address, port } });
@@ -1567,6 +1573,16 @@ function lookupAndConnect(self, options) {
15671573
// If host is an IP, skip performing a lookup
15681574
const addressType = isIP(host);
15691575
if (addressType) {
1576+
// An adopted BoundSocket needs no lookup: issue connect(2) in-tick so the
1577+
// concrete source address resolves and failures throw synchronously.
1578+
if (self[kBoundSource]) {
1579+
defaultTriggerAsyncIdScope(
1580+
self[async_id_symbol],
1581+
internalConnect,
1582+
self, host, port, addressType, localAddress, localPort, 0, true,
1583+
);
1584+
return;
1585+
}
15701586
defaultTriggerAsyncIdScope(self[async_id_symbol], process.nextTick, () => {
15711587
if (self.connecting)
15721588
defaultTriggerAsyncIdScope(

test/parallel/test-net-boundsocket.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,42 @@ if (!common.isWindows && process.getuid() !== 0) {
178178
client.destroy();
179179
}
180180

181+
// Adopted BoundSocket: connect(2) is synchronous, so localAddress/localPort
182+
// resolve to the concrete source in-tick, before the 'connect' event.
183+
{
184+
const server = net.createServer();
185+
server.listen(0, '127.0.0.1', common.mustCall(() => {
186+
const serverPort = server.address().port;
187+
const bound = new net.BoundSocket({ host: '127.0.0.1', port: 0 });
188+
const localPort = bound.address().port;
189+
const client = new net.Socket({ handle: bound });
190+
191+
client.connect({ host: '127.0.0.1', port: serverPort });
192+
193+
assert.strictEqual(client.localAddress, '127.0.0.1');
194+
assert.strictEqual(client.localPort, localPort);
195+
196+
client.on('connect', common.mustCall(() => {
197+
assert.strictEqual(client.localAddress, '127.0.0.1');
198+
assert.strictEqual(client.localPort, localPort);
199+
client.destroy();
200+
server.close();
201+
}));
202+
}));
203+
}
204+
205+
// A synchronous connect(2) failure throws in-tick, without an 'error' event.
206+
// An IPv4-bound handle connecting to an IPv6 literal fails on family mismatch.
207+
{
208+
const bound = new net.BoundSocket({ host: '127.0.0.1', port: 0 });
209+
const client = new net.Socket({ handle: bound });
210+
client.on('error', common.mustNotCall());
211+
assert.throws(() => {
212+
client.connect({ host: '::1', port: 1 });
213+
}, { syscall: 'connect' });
214+
assert.strictEqual(client.destroyed, true);
215+
}
216+
181217
// reusePort: SO_REUSEPORT permits multiple listeners on the same port. Support
182218
// is platform-dependent, so probe first.
183219
{

0 commit comments

Comments
 (0)