@@ -380,19 +380,35 @@ const kBoundSource = Symbol('kBoundSource');
380380// Server/Socket.
381381const kBoundSocketConsume = 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+ const kBoundSocketPath = Symbol ( 'kBoundSocketPath' ) ;
386+
387+ // The source path of an adopted, bound client pipe, surfaced as localAddress.
388+ const kBoundPath = Symbol ( 'kBoundPath' ) ;
389+
390+ const isLinux = 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.
389399class BoundSocket {
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 const port = validatePort ( options . port ?? 0 , 'options.port' ) ;
397413
398414 const ipv6Only = 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+ throw new ERR_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+ throw new ERR_INVALID_ARG_VALUE (
476+ 'options.path' , path ,
477+ 'abstract socket paths are only supported on Linux' ) ;
478+ }
479+
480+ const handle = new Pipe ( PipeConstants . SOCKET ) ;
481+ const err = handle . bind ( path ) ;
482+ if ( err ) {
483+ handle . close ( ) ;
484+ throw new ErrnoException ( 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 throw new ERR_SOCKET_HANDLE_ADOPTED ( ) ;
451497 }
452- return this . #address;
498+ return this . #path ?? this . #address;
499+ }
500+
501+ get [ kBoundSocketPath ] ( ) {
502+ return this . #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 return handle ;
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+ get isPipe ( ) {
546+ return this . #path !== undefined ;
547+ }
491548}
492549
493550function Socket ( options ) {
@@ -555,9 +612,24 @@ function Socket(options) {
555612 let boundNotConnected = false ;
556613 if ( options . handle ) {
557614 if ( options . handle instanceof BoundSocket ) {
615+ const boundPath = 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+ const err = this . _handle . open ( this . _handle . fd ) ;
628+ if ( err ) {
629+ throw new ErrnoException ( err , 'open' ) ;
630+ }
631+ }
632+ }
561633 } else {
562634 this . _handle = options . handle ; // private
563635 }
@@ -1120,6 +1192,11 @@ protoGetter('remotePort', function remotePort() {
11201192
11211193
11221194Socket . 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 } else if ( ! this . _sockname ) {
@@ -1478,7 +1555,13 @@ Socket.prototype.connect = function(...args) {
14781555 }
14791556
14801557 const { path } = options ;
1481- const pipe = ! ! 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+ const pipe = this [ kBoundSource ] && this . _handle ?
1564+ this . _handle instanceof Pipe : ! ! 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+ const boundPath = 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