Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 38 additions & 3 deletions doc/api/net.md
Original file line number Diff line number Diff line change
Expand Up @@ -1653,9 +1653,24 @@ to `listen()` or `new net.Socket()` later on. For `listen()` this enables
synchronous port reservation, while for `new net.Socket()`, it allows control
over the local egress port/IP, via `bind(2)` semantics.

A `BoundSocket` binds either a TCP endpoint (`host`/`port`) or a
Unix domain/named-pipe endpoint (`path`); the two are mutually exclusive. For a
`path`, the file system entry is reserved in the constructor, so conflicts such
as `EADDRINUSE` throw synchronously exactly as a TCP bind does. On Linux a
leading `'\0'` in `path` selects the abstract namespace (no file system entry);
an abstract path on any other platform throws [`ERR_INVALID_ARG_VALUE`][].

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.
closed to avoid leaking the socket. Closing a pipe `BoundSocket` removes its
file system entry; abstract and TCP binds have none to remove.

The presence of the [`boundSocket.isPipe`][] getter on
`net.BoundSocket.prototype` is a capability signal that a build honors the
`path` option rather than silently binding a TCP ephemeral port.

When a pipe `BoundSocket` bound to a source `path` is adopted as a client, that
path is reported as the socket's `localAddress` once it connects.

```mjs
import net from 'node:net';
Expand Down Expand Up @@ -1686,19 +1701,37 @@ added: v26.4.0
* `reusePort` {boolean} Sets `SO_REUSEPORT`, allowing multiple sockets to bind
the same address and port for kernel-level load balancing. Support is
platform-dependent. **Default:** `false`.
* `path` {string} Binds a Unix domain socket (or Windows named pipe) at the
given path instead of a TCP endpoint. A leading `'\0'` selects the Linux
abstract namespace. Mutually exclusive with `host`, `port`, `ipv6Only`, and
`reusePort`; combining them throws [`ERR_INVALID_ARG_VALUE`][].

### `boundSocket.address()`

<!-- YAML
added: v26.4.0
-->

* Returns: {Object} An object with `address`, `family`, and `port` properties,
as [`server.address()`][] returns.
* Returns: {Object|string} For a TCP bind, an object with `address`, `family`,
and `port` properties, as [`server.address()`][] returns. For a pipe bind, the
bound path string, as [`server.address()`][] returns for a pipe server.

Returns the bound local address. When bound with `port: 0`, `port` is the
OS-assigned ephemeral port.

### `boundSocket.isPipe`

<!-- YAML
added: REPLACEME
-->

* {boolean}

`true` when the socket was bound with a `path` (a Unix domain socket or Windows
named pipe), `false` for a TCP bind. The getter's presence on
`net.BoundSocket.prototype` also serves as a capability probe for `path`
support.

### `boundSocket.fd()`

<!-- YAML
Expand Down Expand Up @@ -2204,8 +2237,10 @@ net.isIPv6('fhqwhgads'); // returns false
[`'listening'`]: #event-listening
[`'timeout'`]: #event-timeout
[`BoundSocket`]: #class-netboundsocket
[`ERR_INVALID_ARG_VALUE`]: errors.md#err_invalid_arg_value
[`ERR_SOCKET_HANDLE_ADOPTED`]: errors.md#err_socket_handle_adopted
[`EventEmitter`]: events.md#class-eventemitter
[`boundSocket.isPipe`]: #boundsocketispipe
[`child_process.fork()`]: child_process.md#child_processforkmodulepath-args-options
[`dns.lookup()`]: dns.md#dnslookuphostname-options-callback
[`dns.lookup()` hints]: dns.md#supported-getaddrinfo-flags
Expand Down
106 changes: 96 additions & 10 deletions lib/net.js
Original file line number Diff line number Diff line change
Expand Up @@ -372,19 +372,35 @@ const kBoundSource = Symbol('kBoundSource');
// Server/Socket.
const kBoundSocketConsume = Symbol('kBoundSocketConsume');

// A role-neutral wrapper over a synchronously bound libuv TCP handle: bound to
// a local address but neither listening nor connecting until adopted by exactly
// one Server (server.listen) or Socket (new net.Socket({ handle })). Adoption
// transfers ownership; an un-adopted handle must be closed by the caller.
// bind(2) is non-blocking, so binding happens inline and errors throw
// synchronously. host must be a numeric IP literal; no DNS is performed.
// Internal: read a pipe BoundSocket's bound path before adoption (undefined for
// a TCP BoundSocket).
const kBoundSocketPath = Symbol('kBoundSocketPath');

// The source path of an adopted, bound client pipe, surfaced as localAddress.
const kBoundPath = Symbol('kBoundPath');

const isLinux = process.platform === 'linux';

// A role-neutral wrapper over a synchronously bound libuv handle: bound to a
// local address (a numeric IP literal for TCP, or a filesystem/abstract path
// for a unix-domain socket via { path }) but neither listening nor connecting
// until adopted by exactly one Server (server.listen) or Socket
// (new net.Socket({ handle })). Adoption transfers ownership; an un-adopted
// handle must be closed by the caller. bind(2) is non-blocking, so binding
// happens inline and errors throw synchronously. No DNS is performed.
class BoundSocket {
#handle;
#address = {};
#path;

constructor(options = kEmptyObject) {
validateObject(options, 'options');

if (options.path !== undefined) {
this.#bindPipe(options);
return;
}

const port = validatePort(options.port ?? 0, 'options.port');

const ipv6Only = options.ipv6Only ?? false;
Expand Down Expand Up @@ -435,13 +451,47 @@ class BoundSocket {
this.#handle = handle;
}

// The kernel-assigned local address, resolved at construction; reflects the
// OS-assigned ephemeral port when the bind requested port 0.
// Bind a named unix-domain socket (or Windows named pipe). A leading '\0'
// selects the Linux abstract namespace. path is mutually exclusive with the
// TCP options; uv_pipe_bind is synchronous so conflicts throw here.
#bindPipe(options) {
const { path } = options;
if (options.host !== undefined || options.port !== undefined ||
options.ipv6Only !== undefined || options.reusePort !== undefined) {
throw new ERR_INVALID_ARG_VALUE(
'options', options,
'path is mutually exclusive with host, port, ipv6Only, and reusePort');
}
validateString(path, 'options.path');
if (path[0] === '\0' && !isLinux) {
throw new ERR_INVALID_ARG_VALUE(
'options.path', path,
'abstract socket paths are only supported on Linux');
}

const handle = new Pipe(PipeConstants.SOCKET);
const err = handle.bind(path);
if (err) {
handle.close();
throw new ErrnoException(err, 'bind');
}

this.#handle = handle;
this.#path = path;
}

// The bound local endpoint: an { address, family, port } object for TCP, or
// the path string for a pipe (matching net.Server.address()). Resolved at
// construction; a TCP bind of port 0 reflects the OS-assigned port.
address() {
if (this.#handle === null) {
throw new ERR_SOCKET_HANDLE_ADOPTED();
}
return this.#address;
return this.#path ?? this.#address;
}

get [kBoundSocketPath]() {
return this.#path;
}

// The underlying OS file descriptor, or -1 where sockets have none (Windows).
Expand Down Expand Up @@ -480,6 +530,13 @@ class BoundSocket {
this.#handle = null;
return handle;
}

// Reports whether this is a pipe (unix-domain) bind rather than TCP. Its mere
// presence on the prototype ('isPipe' in net.BoundSocket.prototype) is the
// capability signal that this build honors { path } instead of a TCP port.
get isPipe() {
return this.#path !== undefined;
}
}

function Socket(options) {
Expand Down Expand Up @@ -544,9 +601,24 @@ function Socket(options) {
let boundNotConnected = false;
if (options.handle) {
if (options.handle instanceof BoundSocket) {
const boundPath = options.handle[kBoundSocketPath];
this._handle = options.handle[kBoundSocketConsume]();
this[kBoundSource] = true;
boundNotConnected = true;
// A bound client pipe owns a source path; surface it as localAddress.
if (boundPath !== undefined) {
this[kBoundPath] = boundPath;
// uv_pipe_bind() only assigns the fd; it does not open the stream, so a
// later connect() would leave the handle without its readable/writable
// flags and unusable. Re-open the already-bound fd (idempotent, sets the
// flags) so the adopted client pipe connects to a working stream.
if (this._handle.fd >= 0) {
const err = this._handle.open(this._handle.fd);
if (err) {
throw new ErrnoException(err, 'open');
}
}
}
} else {
this._handle = options.handle; // private
}
Expand Down Expand Up @@ -1109,6 +1181,11 @@ protoGetter('remotePort', function remotePort() {


Socket.prototype._getsockname = function() {
// An adopted bound client pipe has no handle getsockname; its source path was
// captured at adoption and stays authoritative across the connect reset.
if (this[kBoundPath] !== undefined) {
return { address: this[kBoundPath] };
}
if (!this._handle || !this._handle.getsockname) {
return {};
} else if (!this._sockname) {
Expand Down Expand Up @@ -1467,7 +1544,11 @@ Socket.prototype.connect = function(...args) {
}

const { path } = options;
const pipe = !!path;
// An adopted BoundSocket handle already fixes the transport; trust its type
// rather than inferring pipe-ness from a path option on the connect call.
// Other pre-existing handles (e.g. a TLSWrap) are not transport handles, so
// fall back to the path option in that case.
const pipe = this[kBoundSource] ? this._handle instanceof Pipe : !!path;
debug('pipe', pipe, path);

if (!this._handle) {
Expand Down Expand Up @@ -2292,7 +2373,12 @@ Server.prototype.listen = function(...args) {
boundSocket = options.handle;
}
if (boundSocket !== null) {
const boundPath = boundSocket[kBoundSocketPath];
this._handle = boundSocket[kBoundSocketConsume]();
// A pipe-backed handle reports its path via Server.address().
if (boundPath !== undefined) {
this._pipeName = boundPath;
}
this[async_id_symbol] = this._handle.getAsyncId();
this._listeningId++;
listenInCluster(this, null, -1, -1, backlogFromArgs, undefined, true);
Expand Down
Loading
Loading