Commit 1216481

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 ea82bc4 commit 1216481

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
@@ -1695,9 +1695,20 @@ to `listen()` or `new net.Socket()` later on. For `listen()` this enables
16951695
synchronous port reservation, while for `new net.Socket()`, it allows control
16961696
over the local egress port/IP, via `bind(2)` semantics.
16971697

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

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

17201731
<!-- YAML
17211732
added: v24.19.0
1733+
changes:
1734+
- version: REPLACEME
1735+
pr-url: https://github.com/nodejs/node/pull/64399
1736+
description: The `path` option is supported.
17221737
-->
17231738

17241739
*`options` {Object}
@@ -1733,19 +1748,41 @@ added: v24.19.0
17331748
*`reusePort` {boolean} Sets `SO_REUSEPORT`, allowing multiple sockets to bind
17341749
the same address and port for kernel-level load balancing. Support is
17351750
platform-dependent. **Default:**`false`.
1751+
*`path` {string} Binds a Unix domain socket (or Windows named pipe) at the
1752+
given path instead of a TCP endpoint. A leading `'\0'` selects the Linux
1753+
abstract namespace. Mutually exclusive with `host`, `port`, `ipv6Only`, and
1754+
`reusePort`; combining them throws [`ERR_INVALID_ARG_VALUE`][].
17361755

17371756
### `boundSocket.address()`
17381757

17391758
<!-- YAML
17401759
added: v24.19.0
1760+
changes:
1761+
- version: REPLACEME
1762+
pr-url: https://github.com/nodejs/node/pull/64399
1763+
description: The bound path is returned for a pipe bind.
17411764
-->
17421765

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

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

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

17511788
<!-- YAML
@@ -2252,6 +2289,7 @@ net.isIPv6('fhqwhgads'); // returns false
22522289
[`'listening'`]: #event-listening
22532290
[`'timeout'`]: #event-timeout
22542291
[`BoundSocket`]: #class-netboundsocket
2292+
[`ERR_INVALID_ARG_VALUE`]: errors.md#err_invalid_arg_value
22552293
[`ERR_SOCKET_HANDLE_ADOPTED`]: errors.md#err_socket_handle_adopted
22562294
[`EventEmitter`]: events.md#class-eventemitter
22572295
[`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)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Commit 1216481

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 ea82bc4 commit 1216481

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
@@ -1695,9 +1695,20 @@ to `listen()` or `new net.Socket()` later on. For `listen()` this enables
16951695
synchronous port reservation, while for `new net.Socket()`, it allows control
16961696
over the local egress port/IP, via `bind(2)` semantics.
16971697

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

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

17201731
<!-- YAML
17211732
added: v24.19.0
1733+
changes:
1734+
- version: REPLACEME
1735+
pr-url: https://github.com/nodejs/node/pull/64399
1736+
description: The `path` option is supported.
17221737
-->
17231738

17241739
*`options` {Object}
@@ -1733,19 +1748,41 @@ added: v24.19.0
17331748
*`reusePort` {boolean} Sets `SO_REUSEPORT`, allowing multiple sockets to bind
17341749
the same address and port for kernel-level load balancing. Support is
17351750
platform-dependent. **Default:**`false`.
1751+
*`path` {string} Binds a Unix domain socket (or Windows named pipe) at the
1752+
given path instead of a TCP endpoint. A leading `'\0'` selects the Linux
1753+
abstract namespace. Mutually exclusive with `host`, `port`, `ipv6Only`, and
1754+
`reusePort`; combining them throws [`ERR_INVALID_ARG_VALUE`][].
17361755

17371756
### `boundSocket.address()`
17381757

17391758
<!-- YAML
17401759
added: v24.19.0
1760+
changes:
1761+
- version: REPLACEME
1762+
pr-url: https://github.com/nodejs/node/pull/64399
1763+
description: The bound path is returned for a pipe bind.
17411764
-->
17421765

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

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

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

17511788
<!-- YAML
@@ -2252,6 +2289,7 @@ net.isIPv6('fhqwhgads'); // returns false
22522289
[`'listening'`]: #event-listening
22532290
[`'timeout'`]: #event-timeout
22542291
[`BoundSocket`]: #class-netboundsocket
2292+
[`ERR_INVALID_ARG_VALUE`]: errors.md#err_invalid_arg_value
22552293
[`ERR_SOCKET_HANDLE_ADOPTED`]: errors.md#err_socket_handle_adopted
22562294
[`EventEmitter`]: events.md#class-eventemitter
22572295
[`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)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 1216481

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 ea82bc4 commit 1216481

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
@@ -1695,9 +1695,20 @@ to `listen()` or `new net.Socket()` later on. For `listen()` this enables
16951695
synchronous port reservation, while for `new net.Socket()`, it allows control
16961696
over the local egress port/IP, via `bind(2)` semantics.
16971697

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

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

17201731
<!-- YAML
17211732
added: v24.19.0
1733+
changes:
1734+
- version: REPLACEME
1735+
pr-url: https://github.com/nodejs/node/pull/64399
1736+
description: The `path` option is supported.
17221737
-->
17231738

17241739
*`options` {Object}
@@ -1733,19 +1748,41 @@ added: v24.19.0
17331748
*`reusePort` {boolean} Sets `SO_REUSEPORT`, allowing multiple sockets to bind
17341749
the same address and port for kernel-level load balancing. Support is
17351750
platform-dependent. **Default:**`false`.
1751+
*`path` {string} Binds a Unix domain socket (or Windows named pipe) at the
1752+
given path instead of a TCP endpoint. A leading `'\0'` selects the Linux
1753+
abstract namespace. Mutually exclusive with `host`, `port`, `ipv6Only`, and
1754+
`reusePort`; combining them throws [`ERR_INVALID_ARG_VALUE`][].
17361755

17371756
### `boundSocket.address()`
17381757

17391758
<!-- YAML
17401759
added: v24.19.0
1760+
changes:
1761+
- version: REPLACEME
1762+
pr-url: https://github.com/nodejs/node/pull/64399
1763+
description: The bound path is returned for a pipe bind.
17411764
-->
17421765

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

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

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

17511788
<!-- YAML
@@ -2252,6 +2289,7 @@ net.isIPv6('fhqwhgads'); // returns false
22522289
[`'listening'`]: #event-listening
22532290
[`'timeout'`]: #event-timeout
22542291
[`BoundSocket`]: #class-netboundsocket
2292+
[`ERR_INVALID_ARG_VALUE`]: errors.md#err_invalid_arg_value
22552293
[`ERR_SOCKET_HANDLE_ADOPTED`]: errors.md#err_socket_handle_adopted
22562294
[`EventEmitter`]: events.md#class-eventemitter
22572295
[`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)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 1216481

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 ea82bc4 commit 1216481

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
@@ -1695,9 +1695,20 @@ to `listen()` or `new net.Socket()` later on. For `listen()` this enables
16951695
synchronous port reservation, while for `new net.Socket()`, it allows control
16961696
over the local egress port/IP, via `bind(2)` semantics.
16971697

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

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

17201731
<!-- YAML
17211732
added: v24.19.0
1733+
changes:
1734+
- version: REPLACEME
1735+
pr-url: https://github.com/nodejs/node/pull/64399
1736+
description: The `path` option is supported.
17221737
-->
17231738

17241739
*`options` {Object}
@@ -1733,19 +1748,41 @@ added: v24.19.0
17331748
*`reusePort` {boolean} Sets `SO_REUSEPORT`, allowing multiple sockets to bind
17341749
the same address and port for kernel-level load balancing. Support is
17351750
platform-dependent. **Default:**`false`.
1751+
*`path` {string} Binds a Unix domain socket (or Windows named pipe) at the
1752+
given path instead of a TCP endpoint. A leading `'\0'` selects the Linux
1753+
abstract namespace. Mutually exclusive with `host`, `port`, `ipv6Only`, and
1754+
`reusePort`; combining them throws [`ERR_INVALID_ARG_VALUE`][].
17361755

17371756
### `boundSocket.address()`
17381757

17391758
<!-- YAML
17401759
added: v24.19.0
1760+
changes:
1761+
- version: REPLACEME
1762+
pr-url: https://github.com/nodejs/node/pull/64399
1763+
description: The bound path is returned for a pipe bind.
17411764
-->
17421765

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

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

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

17511788
<!-- YAML
@@ -2252,6 +2289,7 @@ net.isIPv6('fhqwhgads'); // returns false
22522289
[`'listening'`]: #event-listening
22532290
[`'timeout'`]: #event-timeout
22542291
[`BoundSocket`]: #class-netboundsocket
2292+
[`ERR_INVALID_ARG_VALUE`]: errors.md#err_invalid_arg_value
22552293
[`ERR_SOCKET_HANDLE_ADOPTED`]: errors.md#err_socket_handle_adopted
22562294
[`EventEmitter`]: events.md#class-eventemitter
22572295
[`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)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Commit 1216481

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 ea82bc4 commit 1216481

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
@@ -1695,9 +1695,20 @@ to `listen()` or `new net.Socket()` later on. For `listen()` this enables
16951695
synchronous port reservation, while for `new net.Socket()`, it allows control
16961696
over the local egress port/IP, via `bind(2)` semantics.
16971697

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

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

17201731
<!-- YAML
17211732
added: v24.19.0
1733+
changes:
1734+
- version: REPLACEME
1735+
pr-url: https://github.com/nodejs/node/pull/64399
1736+
description: The `path` option is supported.
17221737
-->
17231738

17241739
*`options` {Object}
@@ -1733,19 +1748,41 @@ added: v24.19.0
17331748
*`reusePort` {boolean} Sets `SO_REUSEPORT`, allowing multiple sockets to bind
17341749
the same address and port for kernel-level load balancing. Support is
17351750
platform-dependent. **Default:**`false`.
1751+
*`path` {string} Binds a Unix domain socket (or Windows named pipe) at the
1752+
given path instead of a TCP endpoint. A leading `'\0'` selects the Linux
1753+
abstract namespace. Mutually exclusive with `host`, `port`, `ipv6Only`, and
1754+
`reusePort`; combining them throws [`ERR_INVALID_ARG_VALUE`][].
17361755

17371756
### `boundSocket.address()`
17381757

17391758
<!-- YAML
17401759
added: v24.19.0
1760+
changes:
1761+
- version: REPLACEME
1762+
pr-url: https://github.com/nodejs/node/pull/64399
1763+
description: The bound path is returned for a pipe bind.
17411764
-->
17421765

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

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

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

17511788
<!-- YAML
@@ -2252,6 +2289,7 @@ net.isIPv6('fhqwhgads'); // returns false
22522289
[`'listening'`]: #event-listening
22532290
[`'timeout'`]: #event-timeout
22542291
[`BoundSocket`]: #class-netboundsocket
2292+
[`ERR_INVALID_ARG_VALUE`]: errors.md#err_invalid_arg_value
22552293
[`ERR_SOCKET_HANDLE_ADOPTED`]: errors.md#err_socket_handle_adopted
22562294
[`EventEmitter`]: events.md#class-eventemitter
22572295
[`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)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 1216481

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 ea82bc4 commit 1216481

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
@@ -1695,9 +1695,20 @@ to `listen()` or `new net.Socket()` later on. For `listen()` this enables
16951695
synchronous port reservation, while for `new net.Socket()`, it allows control
16961696
over the local egress port/IP, via `bind(2)` semantics.
16971697

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

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

17201731
<!-- YAML
17211732
added: v24.19.0
1733+
changes:
1734+
- version: REPLACEME
1735+
pr-url: https://github.com/nodejs/node/pull/64399
1736+
description: The `path` option is supported.
17221737
-->
17231738

17241739
*`options` {Object}
@@ -1733,19 +1748,41 @@ added: v24.19.0
17331748
*`reusePort` {boolean} Sets `SO_REUSEPORT`, allowing multiple sockets to bind
17341749
the same address and port for kernel-level load balancing. Support is
17351750
platform-dependent. **Default:**`false`.
1751+
*`path` {string} Binds a Unix domain socket (or Windows named pipe) at the
1752+
given path instead of a TCP endpoint. A leading `'\0'` selects the Linux
1753+
abstract namespace. Mutually exclusive with `host`, `port`, `ipv6Only`, and
1754+
`reusePort`; combining them throws [`ERR_INVALID_ARG_VALUE`][].
17361755

17371756
### `boundSocket.address()`
17381757

17391758
<!-- YAML
17401759
added: v24.19.0
1760+
changes:
1761+
- version: REPLACEME
1762+
pr-url: https://github.com/nodejs/node/pull/64399
1763+
description: The bound path is returned for a pipe bind.
17411764
-->
17421765

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

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

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

17511788
<!-- YAML
@@ -2252,6 +2289,7 @@ net.isIPv6('fhqwhgads'); // returns false
22522289
[`'listening'`]: #event-listening
22532290
[`'timeout'`]: #event-timeout
22542291
[`BoundSocket`]: #class-netboundsocket
2292+
[`ERR_INVALID_ARG_VALUE`]: errors.md#err_invalid_arg_value
22552293
[`ERR_SOCKET_HANDLE_ADOPTED`]: errors.md#err_socket_handle_adopted
22562294
[`EventEmitter`]: events.md#class-eventemitter
22572295
[`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)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 1216481

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 ea82bc4 commit 1216481

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
@@ -1695,9 +1695,20 @@ to `listen()` or `new net.Socket()` later on. For `listen()` this enables
16951695
synchronous port reservation, while for `new net.Socket()`, it allows control
16961696
over the local egress port/IP, via `bind(2)` semantics.
16971697

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

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

17201731
<!-- YAML
17211732
added: v24.19.0
1733+
changes:
1734+
- version: REPLACEME
1735+
pr-url: https://github.com/nodejs/node/pull/64399
1736+
description: The `path` option is supported.
17221737
-->
17231738

17241739
*`options` {Object}
@@ -1733,19 +1748,41 @@ added: v24.19.0
17331748
*`reusePort` {boolean} Sets `SO_REUSEPORT`, allowing multiple sockets to bind
17341749
the same address and port for kernel-level load balancing. Support is
17351750
platform-dependent. **Default:**`false`.
1751+
*`path` {string} Binds a Unix domain socket (or Windows named pipe) at the
1752+
given path instead of a TCP endpoint. A leading `'\0'` selects the Linux
1753+
abstract namespace. Mutually exclusive with `host`, `port`, `ipv6Only`, and
1754+
`reusePort`; combining them throws [`ERR_INVALID_ARG_VALUE`][].
17361755

17371756
### `boundSocket.address()`
17381757

17391758
<!-- YAML
17401759
added: v24.19.0
1760+
changes:
1761+
- version: REPLACEME
1762+
pr-url: https://github.com/nodejs/node/pull/64399
1763+
description: The bound path is returned for a pipe bind.
17411764
-->
17421765

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

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

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

17511788
<!-- YAML
@@ -2252,6 +2289,7 @@ net.isIPv6('fhqwhgads'); // returns false
22522289
[`'listening'`]: #event-listening
22532290
[`'timeout'`]: #event-timeout
22542291
[`BoundSocket`]: #class-netboundsocket
2292+
[`ERR_INVALID_ARG_VALUE`]: errors.md#err_invalid_arg_value
22552293
[`ERR_SOCKET_HANDLE_ADOPTED`]: errors.md#err_socket_handle_adopted
22562294
[`EventEmitter`]: events.md#class-eventemitter
22572295
[`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)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Commit 1216481

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 ea82bc4 commit 1216481

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
@@ -1695,9 +1695,20 @@ to `listen()` or `new net.Socket()` later on. For `listen()` this enables
16951695
synchronous port reservation, while for `new net.Socket()`, it allows control
16961696
over the local egress port/IP, via `bind(2)` semantics.
16971697

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

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

17201731
<!-- YAML
17211732
added: v24.19.0
1733+
changes:
1734+
- version: REPLACEME
1735+
pr-url: https://github.com/nodejs/node/pull/64399
1736+
description: The `path` option is supported.
17221737
-->
17231738

17241739
*`options` {Object}
@@ -1733,19 +1748,41 @@ added: v24.19.0
17331748
*`reusePort` {boolean} Sets `SO_REUSEPORT`, allowing multiple sockets to bind
17341749
the same address and port for kernel-level load balancing. Support is
17351750
platform-dependent. **Default:**`false`.
1751+
*`path` {string} Binds a Unix domain socket (or Windows named pipe) at the
1752+
given path instead of a TCP endpoint. A leading `'\0'` selects the Linux
1753+
abstract namespace. Mutually exclusive with `host`, `port`, `ipv6Only`, and
1754+
`reusePort`; combining them throws [`ERR_INVALID_ARG_VALUE`][].
17361755

17371756
### `boundSocket.address()`
17381757

17391758
<!-- YAML
17401759
added: v24.19.0
1760+
changes:
1761+
- version: REPLACEME
1762+
pr-url: https://github.com/nodejs/node/pull/64399
1763+
description: The bound path is returned for a pipe bind.
17411764
-->
17421765

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

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

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

17511788
<!-- YAML
@@ -2252,6 +2289,7 @@ net.isIPv6('fhqwhgads'); // returns false
22522289
[`'listening'`]: #event-listening
22532290
[`'timeout'`]: #event-timeout
22542291
[`BoundSocket`]: #class-netboundsocket
2292+
[`ERR_INVALID_ARG_VALUE`]: errors.md#err_invalid_arg_value
22552293
[`ERR_SOCKET_HANDLE_ADOPTED`]: errors.md#err_socket_handle_adopted
22562294
[`EventEmitter`]: events.md#class-eventemitter
22572295
[`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)