@@ -380,19 +380,35 @@ const kBoundSource = Symbol('kBoundSource'); |
380 | 380 | // Server/Socket. |
381 | 381 | const kBoundSocketConsume = Symbol('kBoundSocketConsume'); |
382 | 382 |
|
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. |
389 | 399 | class BoundSocket { |
390 | 400 | #handle; |
391 | 401 | #address = {}; |
| 402 | + #path; |
392 | 403 |
|
393 | 404 | constructor(options = kEmptyObject) { |
394 | 405 | validateObject(options, 'options'); |
395 | 406 |
|
| 407 | + if (options.path !== undefined) { |
| 408 | + this.#bindPipe(options); |
| 409 | + return; |
| 410 | + } |
| 411 | + |
396 | 412 | const port = validatePort(options.port ?? 0, 'options.port'); |
397 | 413 |
|
398 | 414 | const ipv6Only = options.ipv6Only ?? false; |
@@ -443,13 +459,47 @@ class BoundSocket { |
443 | 459 | this.#handle = handle; |
444 | 460 | } |
445 | 461 |
|
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. |
448 | 494 | address() { |
449 | 495 | if (this.#handle === null) { |
450 | 496 | throw new ERR_SOCKET_HANDLE_ADOPTED(); |
451 | 497 | } |
452 | | - return this.#address; |
| 498 | + return this.#path ?? this.#address; |
| 499 | + } |
| 500 | + |
| 501 | + get [kBoundSocketPath]() { |
| 502 | + return this.#path; |
453 | 503 | } |
454 | 504 |
|
455 | 505 | // The underlying OS file descriptor, or -1 where sockets have none (Windows). |
@@ -488,6 +538,13 @@ class BoundSocket { |
488 | 538 | this.#handle = null; |
489 | 539 | return handle; |
490 | 540 | } |
| 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 | + } |
491 | 548 | } |
492 | 549 |
|
493 | 550 | function Socket(options) { |
@@ -555,9 +612,24 @@ function Socket(options) { |
555 | 612 | let boundNotConnected = false; |
556 | 613 | if (options.handle) { |
557 | 614 | if (options.handle instanceof BoundSocket) { |
| 615 | + const boundPath = options.handle[kBoundSocketPath]; |
558 | 616 | this._handle = options.handle[kBoundSocketConsume](); |
559 | 617 | this[kBoundSource] = true; |
560 | 618 | 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 | + } |
561 | 633 | } else { |
562 | 634 | this._handle = options.handle; // private |
563 | 635 | } |
@@ -1120,6 +1192,11 @@ protoGetter('remotePort', function remotePort() { |
1120 | 1192 |
|
1121 | 1193 |
|
1122 | 1194 | 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 | + } |
1123 | 1200 | if (!this._handle || !this._handle.getsockname) { |
1124 | 1201 | return {}; |
1125 | 1202 | } else if (!this._sockname) { |
@@ -1478,7 +1555,13 @@ Socket.prototype.connect = function(...args) { |
1478 | 1555 | } |
1479 | 1556 |
|
1480 | 1557 | 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; |
1482 | 1565 | debug('pipe', pipe, path); |
1483 | 1566 |
|
1484 | 1567 | if (!this._handle) { |
@@ -2425,7 +2508,12 @@ Server.prototype.listen = function(...args) { |
2425 | 2508 | boundSocket = options.handle; |
2426 | 2509 | } |
2427 | 2510 | if (boundSocket !== null) { |
| 2511 | + const boundPath = boundSocket[kBoundSocketPath]; |
2428 | 2512 | this._handle = boundSocket[kBoundSocketConsume](); |
| 2513 | + // A pipe-backed handle reports its path via Server.address(). |
| 2514 | + if (boundPath !== undefined) { |
| 2515 | + this._pipeName = boundPath; |
| 2516 | + } |
2429 | 2517 | this[async_id_symbol] = this._handle.getAsyncId(); |
2430 | 2518 | this._listeningId++; |
2431 | 2519 | listenInCluster(this, null, -1, -1, backlogFromArgs, undefined, true); |
|