Skip to content

Commit fe9e0db

Browse files
guybedfordaduh95
authored andcommitted
net: support AF_UNIX paths in net.BoundSocket
Signed-off-by: Guy Bedford <guybedford@gmail.com> PR-URL: #64399 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 c25b8e3 commit fe9e0db

3 files changed

Lines changed: 323 additions & 13 deletions

File tree

‎doc/api/net.md‎

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1699,9 +1699,20 @@ to `listen()` or `new net.Socket()` later on. For `listen()` this enables
16991699
synchronous port reservation, while for `new net.Socket()`, it allows control
17001700
over the local egress port/IP, via `bind(2)` semantics.
17011701

1702+
A `BoundSocket` binds either a TCP endpoint (`host` or `port`) or a
1703+
Unix domain/named-pipe endpoint (`path`); the two are mutually exclusive. For a
1704+
`path`, the file system entry is reserved in the constructor, so conflicts such
1705+
as `EADDRINUSE` throw synchronously exactly as a TCP bind does. On Linux a
1706+
leading `'\0'` in `path` selects the abstract namespace (no file system entry);
1707+
an abstract path on any other platform throws [`ERR_INVALID_ARG_VALUE`][].
1708+
17021709
Adoption transfers ownership of the socket; afterwards `address()` and `close()`
17031710
throw [`ERR_SOCKET_HANDLE_ADOPTED`][]. A handle that is never adopted must be
1704-
closed to avoid leaking the socket.
1711+
closed to avoid leaking the socket. Closing a pipe `BoundSocket` removes its
1712+
file system entry; abstract and TCP binds have none to remove.
1713+
1714+
When a pipe `BoundSocket` bound to a source `path` is adopted as a client, that
1715+
path is reported as the socket's `localAddress` once it connects.
17051716

17061717
When an adopted `BoundSocket` connects to a numeric IP literal, `connect(2)` is
17071718
issued synchronously, so [`socket.localAddress`][] is resolved once
@@ -1723,6 +1734,10 @@ server.listen(bound); // Adopt as a server, or pass to new net.Socket() instead.
17231734

17241735
<!-- YAML
17251736
added: v26.4.0
1737+
changes:
1738+
- version: REPLACEME
1739+
pr-url: https://github.com/nodejs/node/pull/64399
1740+
description: The `path` option is supported.
17261741
-->
17271742

17281743
*`options` {Object}
@@ -1737,19 +1752,41 @@ added: v26.4.0
17371752
*`reusePort` {boolean} Sets `SO_REUSEPORT`, allowing multiple sockets to bind
17381753
the same address and port for kernel-level load balancing. Support is
17391754
platform-dependent. **Default:**`false`.
1755+
*`path` {string} Binds a Unix domain socket (or Windows named pipe) at the
1756+
given path instead of a TCP endpoint. A leading `'\0'` selects the Linux
1757+
abstract namespace. Mutually exclusive with `host`, `port`, `ipv6Only`, and
1758+
`reusePort`; combining them throws [`ERR_INVALID_ARG_VALUE`][].
17401759

17411760
### `boundSocket.address()`
17421761

17431762
<!-- YAML
17441763
added: v26.4.0
1764+
changes:
1765+
- version: REPLACEME
1766+
pr-url: https://github.com/nodejs/node/pull/64399
1767+
description: The bound path is returned for a pipe bind.
17451768
-->
17461769

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

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

1777+
### `boundSocket.isPipe`
1778+
1779+
<!-- YAML
1780+
added: REPLACEME
1781+
-->
1782+
1783+
* {boolean}
1784+
1785+
`true` when the socket was bound with a `path` (a Unix domain socket or Windows
1786+
named pipe), `false` for a TCP bind. The getter's presence on
1787+
`net.BoundSocket.prototype` also serves as a capability probe for `path`
1788+
support.
1789+
17531790
### `boundSocket.fd()`
17541791

17551792
<!-- YAML
@@ -2256,6 +2293,7 @@ net.isIPv6('fhqwhgads'); // returns false
22562293
[`'listening'`]: #event-listening
22572294
[`'timeout'`]: #event-timeout
22582295
[`BoundSocket`]: #class-netboundsocket
2296+
[`ERR_INVALID_ARG_VALUE`]: errors.md#err_invalid_arg_value
22592297
[`ERR_SOCKET_HANDLE_ADOPTED`]: errors.md#err_socket_handle_adopted
22602298
[`EventEmitter`]: events.md#class-eventemitter
22612299
[`child_process.fork()`]: child_process.md#child_processforkmodulepath-args-options

‎lib/net.js‎

Lines changed: 98 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -380,19 +380,35 @@ const kBoundSource = Symbol('kBoundSource');
380380
// Server/Socket.
381381
constkBoundSocketConsume=Symbol('kBoundSocketConsume');
382382

383-
// A role-neutral wrapper over a synchronously bound libuv TCP handle: bound to
384-
// a local address but neither listening nor connecting until adopted by exactly
385-
// one Server (server.listen) or Socket (new net.Socket({ handle })). Adoption
386-
// transfers ownership; an un-adopted handle must be closed by the caller.
387-
// bind(2) is non-blocking, so binding happens inline and errors throw
388-
// synchronously. host must be a numeric IP literal; no DNS is performed.
383+
// Internal: read a pipe BoundSocket's bound path before adoption (undefined for
384+
// a TCP BoundSocket).
385+
constkBoundSocketPath=Symbol('kBoundSocketPath');
386+
387+
// The source path of an adopted, bound client pipe, surfaced as localAddress.
388+
constkBoundPath=Symbol('kBoundPath');
389+
390+
constisLinux=process.platform==='linux';
391+
392+
// A role-neutral wrapper over a synchronously bound libuv handle: bound to a
393+
// local address (a numeric IP literal for TCP, or a filesystem/abstract path
394+
// for a unix-domain socket via { path }) but neither listening nor connecting
395+
// until adopted by exactly one Server (server.listen) or Socket
396+
// (new net.Socket({ handle })). Adoption transfers ownership; an un-adopted
397+
// handle must be closed by the caller. bind(2) is non-blocking, so binding
398+
// happens inline and errors throw synchronously. No DNS is performed.
389399
classBoundSocket{
390400
#handle;
391401
#address ={};
402+
#path;
392403

393404
constructor(options=kEmptyObject){
394405
validateObject(options,'options');
395406

407+
if(options.path!==undefined){
408+
this.#bindPipe(options);
409+
return;
410+
}
411+
396412
constport=validatePort(options.port??0,'options.port');
397413

398414
constipv6Only=options.ipv6Only??false;
@@ -443,13 +459,47 @@ class BoundSocket {
443459
this.#handle =handle;
444460
}
445461

446-
// The kernel-assigned local address, resolved at construction; reflects the
447-
// OS-assigned ephemeral port when the bind requested port 0.
462+
// Bind a named unix-domain socket (or Windows named pipe). A leading '\0'
463+
// selects the Linux abstract namespace. path is mutually exclusive with the
464+
// TCP options; uv_pipe_bind is synchronous so conflicts throw here.
465+
#bindPipe(options){
466+
const{ path, host, port, ipv6Only, reusePort }=options;
467+
if(host!==undefined||port!==undefined||
468+
ipv6Only!==undefined||reusePort!==undefined){
469+
thrownewERR_INVALID_ARG_VALUE(
470+
'options',options,
471+
'path is mutually exclusive with host, port, ipv6Only, and reusePort');
472+
}
473+
validateString(path,'options.path');
474+
if(path[0]==='\0'&&!isLinux){
475+
thrownewERR_INVALID_ARG_VALUE(
476+
'options.path',path,
477+
'abstract socket paths are only supported on Linux');
478+
}
479+
480+
consthandle=newPipe(PipeConstants.SOCKET);
481+
consterr=handle.bind(path);
482+
if(err){
483+
handle.close();
484+
thrownewErrnoException(err,'bind');
485+
}
486+
487+
this.#handle =handle;
488+
this.#path =path;
489+
}
490+
491+
// The bound local endpoint: an { address, family, port } object for TCP, or
492+
// the path string for a pipe (matching net.Server.address()). Resolved at
493+
// construction; a TCP bind of port 0 reflects the OS-assigned port.
448494
address(){
449495
if(this.#handle ===null){
450496
thrownewERR_SOCKET_HANDLE_ADOPTED();
451497
}
452-
returnthis.#address;
498+
returnthis.#path ??this.#address;
499+
}
500+
501+
get[kBoundSocketPath](){
502+
returnthis.#path;
453503
}
454504

455505
// The underlying OS file descriptor, or -1 where sockets have none (Windows).
@@ -488,6 +538,13 @@ class BoundSocket {
488538
this.#handle =null;
489539
returnhandle;
490540
}
541+
542+
// Reports whether this is a pipe (unix-domain) bind rather than TCP. Its mere
543+
// presence on the prototype ('isPipe' in net.BoundSocket.prototype) is the
544+
// capability signal that this build honors { path } instead of a TCP port.
545+
getisPipe(){
546+
returnthis.#path !==undefined;
547+
}
491548
}
492549

493550
functionSocket(options){
@@ -555,9 +612,24 @@ function Socket(options) {
555612
letboundNotConnected=false;
556613
if(options.handle){
557614
if(options.handleinstanceofBoundSocket){
615+
constboundPath=options.handle[kBoundSocketPath];
558616
this._handle=options.handle[kBoundSocketConsume]();
559617
this[kBoundSource]=true;
560618
boundNotConnected=true;
619+
// A bound client pipe owns a source path; surface it as localAddress.
620+
if(boundPath!==undefined){
621+
this[kBoundPath]=boundPath;
622+
// uv_pipe_bind() only assigns the fd; it does not open the stream, so a
623+
// later connect() would leave the handle without its readable/writable
624+
// flags and unusable. Re-open the already-bound fd (idempotent, sets the
625+
// flags) so the adopted client pipe connects to a working stream.
626+
if(this._handle.fd>=0){
627+
consterr=this._handle.open(this._handle.fd);
628+
if(err){
629+
thrownewErrnoException(err,'open');
630+
}
631+
}
632+
}
561633
}else{
562634
this._handle=options.handle;// private
563635
}
@@ -1120,6 +1192,11 @@ protoGetter('remotePort', function remotePort() {
11201192

11211193

11221194
Socket.prototype._getsockname=function(){
1195+
// An adopted bound client pipe has no handle getsockname; its source path was
1196+
// captured at adoption and stays authoritative across the connect reset.
1197+
if(this[kBoundPath]!==undefined){
1198+
return{address: this[kBoundPath]};
1199+
}
11231200
if(!this._handle||!this._handle.getsockname){
11241201
return{};
11251202
}elseif(!this._sockname){
@@ -1478,7 +1555,13 @@ Socket.prototype.connect = function(...args) {
14781555
}
14791556

14801557
const{ path }=options;
1481-
constpipe=!!path;
1558+
// An adopted BoundSocket handle already fixes the transport; trust its type
1559+
// rather than inferring pipe-ness from a path option on the connect call.
1560+
// Once destroyed the adopted handle is gone (its reservation released), so
1561+
// fall back to the path option, as for other pre-existing handles (e.g. a
1562+
// TLSWrap) that are not transport handles.
1563+
constpipe=this[kBoundSource]&&this._handle ?
1564+
this._handleinstanceofPipe : !!path;
14821565
debug('pipe',pipe,path);
14831566

14841567
if(!this._handle){
@@ -2425,7 +2508,12 @@ Server.prototype.listen = function(...args) {
24252508
boundSocket=options.handle;
24262509
}
24272510
if(boundSocket!==null){
2511+
constboundPath=boundSocket[kBoundSocketPath];
24282512
this._handle=boundSocket[kBoundSocketConsume]();
2513+
// A pipe-backed handle reports its path via Server.address().
2514+
if(boundPath!==undefined){
2515+
this._pipeName=boundPath;
2516+
}
24292517
this[async_id_symbol]=this._handle.getAsyncId();
24302518
this._listeningId++;
24312519
listenInCluster(this,null,-1,-1,backlogFromArgs,undefined,true);

0 commit comments

Comments
 (0)