Skip to content

Commit 0725b90

Browse files
guybedfordaduh95
authored andcommitted
net: early TCP binding via synchronous net.BoundSocket
Signed-off-by: Guy Bedford <guybedford@gmail.com> PR-URL: #63951 Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 0d24dea commit 0725b90

6 files changed

Lines changed: 514 additions & 5 deletions

File tree

doc/api/errors.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2837,6 +2837,14 @@ disconnected socket.
28372837

28382838
A call was made and the UDP subsystem was not running.
28392839

2840+
<a id="ERR_SOCKET_HANDLE_ADOPTED"></a>
2841+
2842+
### `ERR_SOCKET_HANDLE_ADOPTED`
2843+
2844+
An operation was attempted on a [`BoundSocket`][] that had already been adopted
2845+
by a [`net.Server`][] or [`net.Socket`][]. Once a bound socket is adopted, its
2846+
`address()` and `close()` methods can no longer be used.
2847+
28402848
<a id="ERR_SOURCE_MAP_CORRUPT"></a>
28412849

28422850
### `ERR_SOURCE_MAP_CORRUPT`
@@ -4410,6 +4418,7 @@ An error occurred trying to allocate memory. This should never happen.
44104418
[`--force-fips`]: cli.md#--force-fips
44114419
[`--no-addons`]: cli.md#--no-addons
44124420
[`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode
4421+
[`BoundSocket`]: net.md#class-netboundsocket
44134422
[`Class: assert.AssertionError`]: assert.md#class-assertassertionerror
44144423
[`ERR_INCOMPATIBLE_OPTION_PAIR`]: #err_incompatible_option_pair
44154424
[`ERR_INVALID_ARG_TYPE`]: #err_invalid_arg_type
@@ -4451,7 +4460,9 @@ An error occurred trying to allocate memory. This should never happen.
44514460
[`http`]: http.md
44524461
[`https`]: https.md
44534462
[`libuv Error handling`]: https://docs.libuv.org/en/v1.x/errors.html
4463+
[`net.Server`]: net.md#class-netserver
44544464
[`net.Socket.write()`]: net.md#socketwritedata-encoding-callback
4465+
[`net.Socket`]: net.md#class-netsocket
44554466
[`net`]: net.md
44564467
[`new URL(input)`]: url.md#new-urlinput-base
44574468
[`new URLPattern(input)`]: url.md#new-urlpatternstring-baseurl-options

doc/api/net.md

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -519,8 +519,12 @@ Start a server listening for connections on a given `handle` that has
519519
already been bound to a port, a Unix domain socket, or a Windows named pipe.
520520

521521
The `handle` object can be either a server, a socket (anything with an
522-
underlying `_handle` member), or an object with an `fd` member that is a
523-
valid file descriptor.
522+
underlying `_handle` member), a [`BoundSocket`][], or an object with an `fd`
523+
member that is a valid file descriptor.
524+
525+
When `handle` is a [`BoundSocket`][], the server adopts the already-bound
526+
socket and starts listening on it. Adoption consumes the bound socket (see
527+
[ownership transfer][`BoundSocket`]).
524528

525529
Listening on a file descriptor is not supported on Windows.
526530

@@ -546,6 +550,10 @@ changes:
546550
* `backlog` {number} Common parameter of [`server.listen()`][]
547551
functions.
548552
* `exclusive` {boolean} **Default:** `false`
553+
* `handle` {net.BoundSocket} A pre-bound [`BoundSocket`][]. The server adopts
554+
the already-bound socket and listens on it, ignoring `host`, `port`, and
555+
`path`. Adoption consumes the bound socket (see
556+
[ownership transfer][`BoundSocket`]).
549557
* `host` {string}
550558
* `ipv6Only` {boolean} For TCP servers, setting `ipv6Only` to `true` will
551559
disable dual-stack support, i.e., binding to host `::` won't make
@@ -569,7 +577,8 @@ changes:
569577
functions.
570578
* Returns: {net.Server}
571579

572-
If `port` is specified, it behaves the same as
580+
If `handle` is specified, the server adopts that pre-bound socket. Otherwise, if
581+
`port` is specified, it behaves the same as
573582
[`server.listen([port[, host[, backlog]]][, callback])`][`server.listen(port)`].
574583
Otherwise, if `path` is specified, it behaves the same as
575584
[`server.listen(path[, backlog][, callback])`][`server.listen(path)`].
@@ -763,6 +772,12 @@ changes:
763772
access to specific IP addresses, IP ranges, or IP subnets.
764773
* `fd` {number} If specified, wrap around an existing socket with
765774
the given file descriptor, otherwise a new socket will be created.
775+
* `handle` {net.BoundSocket} If specified, wrap around the bound socket from a
776+
[`BoundSocket`][]. A subsequent
777+
[`socket.connect()`][`socket.connect()`] uses the bound socket as the
778+
connection's source binding (honoring the bound local address and port).
779+
Adoption consumes the bound socket (see
780+
[ownership transfer][`BoundSocket`]).
766781
* `keepAlive` {boolean} If set to `true`, it enables keep-alive functionality on
767782
the socket immediately after the connection is established, similarly on what
768783
is done in [`socket.setKeepAlive()`][]. **Default:** `false`.
@@ -1617,6 +1632,94 @@ This property represents the state of the connection as a string.
16171632
* If the stream is readable and not writable, it is `readOnly`.
16181633
* If the stream is not readable and writable, it is `writeOnly`.
16191634

1635+
## Class: `net.BoundSocket`
1636+
1637+
<!-- YAML
1638+
added: REPLACEME
1639+
-->
1640+
1641+
Allows for the synchronous creation of a pre-bound socket, that can be passed
1642+
to `listen()` or `new net.Socket()` later on. For `listen()` this enables
1643+
synchronous port reservation, while for `new net.Socket()`, it allows control
1644+
over the local egress port/IP, via `bind(2)` semantics.
1645+
1646+
Adoption transfers ownership of the socket; afterwards `address()` and `close()`
1647+
throw [`ERR_SOCKET_HANDLE_ADOPTED`][]. A handle that is never adopted must be
1648+
closed to avoid leaking the socket.
1649+
1650+
```mjs
1651+
import net from 'node:net';
1652+
1653+
const bound = new net.BoundSocket();
1654+
const { port } = bound.address();
1655+
console.log(`Reserved port ${port} for server`);
1656+
1657+
const server = net.createServer();
1658+
server.listen(bound); // Adopt as a server, or pass to new net.Socket() instead.
1659+
```
1660+
1661+
### `new net.BoundSocket([options])`
1662+
1663+
<!-- YAML
1664+
added: REPLACEME
1665+
-->
1666+
1667+
* `options` {Object}
1668+
* `host` {string} Local address to bind. Must be a numeric IP literal; no DNS
1669+
resolution is performed. **Default:** `'0.0.0.0'`, or `'::'` when
1670+
`ipv6Only` is `true`.
1671+
* `port` {number} Local port. `0` requests an OS-assigned ephemeral port.
1672+
**Default:** `0`.
1673+
* `ipv6Only` {boolean} Sets `IPV6_V6ONLY`, disabling dual-stack support so the
1674+
socket binds IPv6 only. Only meaningful for IPv6 binds. **Default:**
1675+
`false`.
1676+
* `reusePort` {boolean} Sets `SO_REUSEPORT`, allowing multiple sockets to bind
1677+
the same address and port for kernel-level load balancing. Support is
1678+
platform-dependent. **Default:** `false`.
1679+
1680+
### `boundSocket.address()`
1681+
1682+
<!-- YAML
1683+
added: REPLACEME
1684+
-->
1685+
1686+
* Returns: {Object} An object with `address`, `family`, and `port` properties,
1687+
as [`server.address()`][] returns.
1688+
1689+
Returns the bound local address. When bound with `port: 0`, `port` is the
1690+
OS-assigned ephemeral port.
1691+
1692+
### `boundSocket.fd()`
1693+
1694+
<!-- YAML
1695+
added: REPLACEME
1696+
-->
1697+
1698+
* Returns: {integer} The underlying OS file descriptor, or `-1` on platforms
1699+
that do not expose one for sockets (such as Windows).
1700+
1701+
Returns the file descriptor of the bound socket. Ownership remains with the
1702+
`BoundSocket`, so the descriptor must not be closed by the caller. The
1703+
descriptor is only available before the handle is adopted; afterwards it belongs
1704+
to the adopting [`net.Server`][] or [`net.Socket`][] and `fd()` throws
1705+
[`ERR_SOCKET_HANDLE_ADOPTED`][].
1706+
1707+
### `boundSocket.close()`
1708+
1709+
<!-- YAML
1710+
added: REPLACEME
1711+
-->
1712+
1713+
Releases the bound socket. Only needed when the handle is never adopted.
1714+
1715+
### `boundSocket[Symbol.dispose]()`
1716+
1717+
<!-- YAML
1718+
added: REPLACEME
1719+
-->
1720+
1721+
Closes the handle if it has not been adopted or closed; otherwise a no-op.
1722+
16201723
## `net.connect()`
16211724

16221725
Aliases to
@@ -1711,6 +1814,9 @@ and [`socket.connect(options[, connectListener])`][`socket.connect(options)`].
17111814

17121815
Additional options:
17131816

1817+
* `handle` {net.BoundSocket} A pre-bound [`BoundSocket`][] used as the
1818+
connection's source binding, honoring its local address and port. Adoption
1819+
consumes the bound socket (see [ownership transfer][`BoundSocket`]).
17141820
* `timeout` {number} If set, will be used to call
17151821
[`socket.setTimeout(timeout)`][] after the socket is created, but before
17161822
it starts the connection.
@@ -2087,6 +2193,8 @@ net.isIPv6('fhqwhgads'); // returns false
20872193
[`'error'`]: #event-error_1
20882194
[`'listening'`]: #event-listening
20892195
[`'timeout'`]: #event-timeout
2196+
[`BoundSocket`]: #class-netboundsocket
2197+
[`ERR_SOCKET_HANDLE_ADOPTED`]: errors.md#err_socket_handle_adopted
20902198
[`EventEmitter`]: events.md#class-eventemitter
20912199
[`child_process.fork()`]: child_process.md#child_processforkmodulepath-args-options
20922200
[`dns.lookup()`]: dns.md#dnslookuphostname-options-callback
@@ -2106,6 +2214,7 @@ net.isIPv6('fhqwhgads'); // returns false
21062214
[`net.getDefaultAutoSelectFamilyAttemptTimeout()`]: #netgetdefaultautoselectfamilyattempttimeout
21072215
[`new net.Socket(options)`]: #new-netsocketoptions
21082216
[`readable.setEncoding()`]: stream.md#readablesetencodingencoding
2217+
[`server.address()`]: #serveraddress
21092218
[`server.close()`]: #serverclosecallback
21102219
[`server.dropMaxConnection`]: #serverdropmaxconnection
21112220
[`server.listen()`]: #serverlisten

lib/internal/errors.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1763,6 +1763,8 @@ E('ERR_SOCKET_CONNECTION_TIMEOUT',
17631763
E('ERR_SOCKET_DGRAM_IS_CONNECTED', 'Already connected', Error);
17641764
E('ERR_SOCKET_DGRAM_NOT_CONNECTED', 'Not connected', Error);
17651765
E('ERR_SOCKET_DGRAM_NOT_RUNNING', 'Not running', Error);
1766+
E('ERR_SOCKET_HANDLE_ADOPTED',
1767+
'The bound socket has already been adopted by a server or socket', Error);
17661768
E('ERR_SOURCE_MAP_CORRUPT', `The source map for '%s' does not exist or is corrupt.`, Error);
17671769
E('ERR_SOURCE_MAP_MISSING_SOURCE', `Cannot find '%s' imported from the source map for '%s'`, Error);
17681770
E('ERR_SRI_PARSE',

0 commit comments

Comments
 (0)