@@ -372,19 +372,35 @@ const kBoundSource = Symbol('kBoundSource');
372372// Server/Socket.
373373const kBoundSocketConsume = Symbol ( 'kBoundSocketConsume' ) ;
374374
375- // A role-neutral wrapper over a synchronously bound libuv TCP handle: bound to
376- // a local address but neither listening nor connecting until adopted by exactly
377- // one Server (server.listen) or Socket (new net.Socket({ handle })). Adoption
378- // transfers ownership; an un-adopted handle must be closed by the caller.
379- // bind(2) is non-blocking, so binding happens inline and errors throw
380- // synchronously. host must be a numeric IP literal; no DNS is performed.
375+ // Internal: read a pipe BoundSocket's bound path before adoption (undefined for
376+ // a TCP BoundSocket).
377+ const kBoundSocketPath = Symbol ( 'kBoundSocketPath' ) ;
378+
379+ // The source path of an adopted, bound client pipe, surfaced as localAddress.
380+ const kBoundPath = Symbol ( 'kBoundPath' ) ;
381+
382+ const isLinux = process . platform === 'linux' ;
383+
384+ // A role-neutral wrapper over a synchronously bound libuv handle: bound to a
385+ // local address (a numeric IP literal for TCP, or a filesystem/abstract path
386+ // for a unix-domain socket via { path }) but neither listening nor connecting
387+ // until adopted by exactly one Server (server.listen) or Socket
388+ // (new net.Socket({ handle })). Adoption transfers ownership; an un-adopted
389+ // handle must be closed by the caller. bind(2) is non-blocking, so binding
390+ // happens inline and errors throw synchronously. No DNS is performed.
381391class BoundSocket {
382392 #handle;
383393 #address = { } ;
394+ #path;
384395
385396 constructor ( options = kEmptyObject ) {
386397 validateObject ( options , 'options' ) ;
387398
399+ if ( options . path !== undefined ) {
400+ this . #bindPipe( options ) ;
401+ return ;
402+ }
403+
388404 const port = validatePort ( options . port ?? 0 , 'options.port' ) ;
389405
390406 const ipv6Only = options . ipv6Only ?? false ;
@@ -435,13 +451,47 @@ class BoundSocket {
435451 this . #handle = handle ;
436452 }
437453
438- // The kernel-assigned local address, resolved at construction; reflects the
439- // OS-assigned ephemeral port when the bind requested port 0.
454+ // Bind a named unix-domain socket (or Windows named pipe). A leading '\0'
455+ // selects the Linux abstract namespace. path is mutually exclusive with the
456+ // TCP options; uv_pipe_bind is synchronous so conflicts throw here.
457+ #bindPipe( options ) {
458+ const { path } = options ;
459+ if ( options . host !== undefined || options . port !== undefined ||
460+ options . ipv6Only !== undefined || options . reusePort !== undefined ) {
461+ throw new ERR_INVALID_ARG_VALUE (
462+ 'options' , options ,
463+ 'path is mutually exclusive with host, port, ipv6Only, and reusePort' ) ;
464+ }
465+ validateString ( path , 'options.path' ) ;
466+ if ( path [ 0 ] === '\0' && ! isLinux ) {
467+ throw new ERR_INVALID_ARG_VALUE (
468+ 'options.path' , path ,
469+ 'abstract socket paths are only supported on Linux' ) ;
470+ }
471+
472+ const handle = new Pipe ( PipeConstants . SOCKET ) ;
473+ const err = handle . bind ( path ) ;
474+ if ( err ) {
475+ handle . close ( ) ;
476+ throw new ErrnoException ( err , 'bind' ) ;
477+ }
478+
479+ this . #handle = handle ;
480+ this . #path = path ;
481+ }
482+
483+ // The bound local endpoint: an { address, family, port } object for TCP, or
484+ // the path string for a pipe (matching net.Server.address()). Resolved at
485+ // construction; a TCP bind of port 0 reflects the OS-assigned port.
440486 address ( ) {
441487 if ( this . #handle === null ) {
442488 throw new ERR_SOCKET_HANDLE_ADOPTED ( ) ;
443489 }
444- return this . #address;
490+ return this . #path ?? this . #address;
491+ }
492+
493+ get [ kBoundSocketPath ] ( ) {
494+ return this . #path;
445495 }
446496
447497 // The underlying OS file descriptor, or -1 where sockets have none (Windows).
@@ -480,6 +530,13 @@ class BoundSocket {
480530 this . #handle = null ;
481531 return handle ;
482532 }
533+
534+ // Reports whether this is a pipe (unix-domain) bind rather than TCP. Its mere
535+ // presence on the prototype ('isPipe' in net.BoundSocket.prototype) is the
536+ // capability signal that this build honors { path } instead of a TCP port.
537+ get isPipe ( ) {
538+ return this . #path !== undefined ;
539+ }
483540}
484541
485542function Socket ( options ) {
@@ -544,9 +601,24 @@ function Socket(options) {
544601 let boundNotConnected = false ;
545602 if ( options . handle ) {
546603 if ( options . handle instanceof BoundSocket ) {
604+ const boundPath = options . handle [ kBoundSocketPath ] ;
547605 this . _handle = options . handle [ kBoundSocketConsume ] ( ) ;
548606 this [ kBoundSource ] = true ;
549607 boundNotConnected = true ;
608+ // A bound client pipe owns a source path; surface it as localAddress.
609+ if ( boundPath !== undefined ) {
610+ this [ kBoundPath ] = boundPath ;
611+ // uv_pipe_bind() only assigns the fd; it does not open the stream, so a
612+ // later connect() would leave the handle without its readable/writable
613+ // flags and unusable. Re-open the already-bound fd (idempotent, sets the
614+ // flags) so the adopted client pipe connects to a working stream.
615+ if ( this . _handle . fd >= 0 ) {
616+ const err = this . _handle . open ( this . _handle . fd ) ;
617+ if ( err ) {
618+ throw new ErrnoException ( err , 'open' ) ;
619+ }
620+ }
621+ }
550622 } else {
551623 this . _handle = options . handle ; // private
552624 }
@@ -1109,6 +1181,11 @@ protoGetter('remotePort', function remotePort() {
11091181
11101182
11111183Socket . prototype . _getsockname = function ( ) {
1184+ // An adopted bound client pipe has no handle getsockname; its source path was
1185+ // captured at adoption and stays authoritative across the connect reset.
1186+ if ( this [ kBoundPath ] !== undefined ) {
1187+ return { address : this [ kBoundPath ] } ;
1188+ }
11121189 if ( ! this . _handle || ! this . _handle . getsockname ) {
11131190 return { } ;
11141191 } else if ( ! this . _sockname ) {
@@ -1467,7 +1544,9 @@ Socket.prototype.connect = function(...args) {
14671544 }
14681545
14691546 const { path } = options ;
1470- const pipe = ! ! path ;
1547+ // An adopted handle already fixes the transport; trust its type rather than
1548+ // inferring pipe-ness from a path option on the connect call.
1549+ const pipe = this . _handle ? this . _handle instanceof Pipe : ! ! path ;
14711550 debug ( 'pipe' , pipe , path ) ;
14721551
14731552 if ( ! this . _handle ) {
@@ -2292,7 +2371,12 @@ Server.prototype.listen = function(...args) {
22922371 boundSocket = options . handle ;
22932372 }
22942373 if ( boundSocket !== null ) {
2374+ const boundPath = boundSocket [ kBoundSocketPath ] ;
22952375 this . _handle = boundSocket [ kBoundSocketConsume ] ( ) ;
2376+ // A pipe-backed handle reports its path via Server.address().
2377+ if ( boundPath !== undefined ) {
2378+ this . _pipeName = boundPath ;
2379+ }
22962380 this [ async_id_symbol ] = this . _handle . getAsyncId ( ) ;
22972381 this . _listeningId ++ ;
22982382 listenInCluster ( this , null , - 1 , - 1 , backlogFromArgs , undefined , true ) ;
0 commit comments