bobnil
I'd like to revisit a long-standing `net.Socket` behavior, following an investigation and proof of concept in libuv: https://github.com/libuv/libuv/issues/5308 This is closely related to [nodejs/node#23858](https://github.com/nodejs/node/issues/23858), opened in 2018, where `socket.remoteAddress`, `remotePort`, and `remoteFamily` could already be `undefined` inside the server's connection handler. That discussion ended without a fix, but a maintainer comment also left room for reconsideration if a stronger technical case could be made. I've tried to approach this follow-up in that spirit: I traced the behavior through the Node.js and libuv history, examined the relevant Linux kernel semantics, built a libuv proof of concept that preserves the accept-time peer address, and benchmarked its connection-throughput cost. The findings point to an information-lifetime issue at the libuv layer. Preserving the accept-time address also has a different performance profile from restoring the old unconditional `getpeername()` call. ## What happens On Unix, libuv accepts incoming TCP connections without requesting the peer sockaddr from `accept()` / `accept4()`. When Node.js later needs the remote address, libuv calls `getpeername()` on the accepted socket. Normally that works. However, a connection can complete successfully and then be reset before the application processes it. The distinction between obtaining the peer address as part of `accept()` and querying it later with `getpeername()` is part of the original BSD sockets API design, introduced with 4.2BSD in 1983; it is not specific to Linux. The Linux reproducer demonstrates why this distinction matters: `accept()` can still return the peer address even when a subsequent `getpeername()` fails with `ENOTCONN`. Once the accept-time sockaddr has been discarded, the later query cannot recover it. A reproducer in the libuv issue demonstrates this using a normal TCP `connect()` followed by an abortive close with `SO_LINGER`. In one 100,000-connection run using Node.js v26.10.0 / libuv v1.52.1: ```text upstream: Address: 3,268 Undefined: 96,732 patched: Address: 100,000 Undefined: 0 ``` The exact percentage is timing-dependent. The relevant observation is that the connections completed and were accepted, but their peer addresses could become unavailable before the later query. Preserving the sockaddr returned at accept time eliminated that failure mode in this test. ## Historical context I documented the relevant Node.js and libuv history here: https://github.com/bobnil/libuv-accept-peer-address-poc/blob/main/HISTORICAL_CONTEXT.md Before the libuv backend, Node's networking code retained the peer address returned by the accept operation. The first public Node.js/libuv backend retained the accepted fd but did not propagate the accept-time peer address with it. The address was subsequently obtained through a separate `getpeername()` query. Node initially performed that query eagerly. In 2011, it was made lazy, avoiding an unconditional syscall for applications that never used the remote address and improving the connection-heavy `http_simple` benchmark by about 1%. In 2012, libuv also stopped requesting the sockaddr from `accept()`, since it was not being retained anyway. Each of these decisions makes sense in isolation. Together, however, they mean that a successfully accepted connection does not necessarily retain the peer information that was available at accept time. The same underlying race appears to have been identified in [nodejs/node-v0.x-archive#7566](https://github.com/nodejs/node-v0.x-archive/issues/7566) in 2014, where retaining the address returned by `accept()` was already discussed as a possible solution. The 2018 report in #23858 is particularly relevant because the remote properties were accessed directly inside the server's connection handler: the information could already be unavailable at the first application-visible opportunity to read it. Later Node.js changes improved caching after a successful peer-address lookup. That helps preserve information already obtained, but does not address the case where the socket state changes before the first lookup succeeds. ## Performance I built a proof-of-concept libuv patch that retrieves and preserves the accept-time sockaddr, and benchmarked it against the same unmodified libuv revision: https://github.com/bobnil/libuv-accept-peer-address-poc/blob/main/BENCHMARK.md The benchmark deliberately favors upstream: the server accepts and immediately closes each connection without querying the peer address. Upstream therefore performs neither accept-time address retrieval nor a later `getpeername()`, while the patched build retrieves and retains an address that the application never uses. Tests included local loopback and two-machine runs, ranging from 500,000 to 50 million connections. Across the datasets, the direction of the measured difference changed: some favored upstream and some favored the patched build, with run-to-run variation larger than the observed differences. I therefore could not identify a reproducible connection-throughput regression from retrieving and preserving the accept-time sockaddr in this Linux environment. This does not establish zero cost on every platform or workload. It does suggest that this approach merits evaluation separately from the historical cost of an unconditional `getpeername()` call: asking `accept()` to return the peer address does not add that extra syscall. ## Where to go from here The implementation question belongs primarily in libuv, which is why I opened the issue there. A production-quality solution needs somewhere to retain the peer sockaddr together with the pending accepted connection while respecting libuv's API/ABI compatibility requirements. My patch changes internal storage and is intended as a proof of concept, rather than a proposed final implementation. I'm raising it here because the behavior is directly visible to Node.js applications. A server can successfully accept a TCP connection yet be unable to determine its remote endpoint even in the connection callback. That can matter for connection logging, auditing, diagnostics, and IP-based abuse mitigation. I'd be interested in the Node.js networking/libuv maintainers' thoughts on two questions: 1. Should an accepted TCP connection ideally retain the peer address that was available when it was accepted, rather than depending entirely on a later socket-state query? 2. If so, what would be the best way for Node.js and libuv to coordinate on preserving that information without disrupting libuv 1.x ABI compatibility? The [libuv issue](https://github.com/libuv/libuv/issues/5308) contains the reproducer, proof of concept, kernel references, and more detailed analysis. Thanks for taking a look. ### Disclaimer _I used AI tools alongside other search tools to help investigate the history, organize the source material, and draft this report in technical English, which is not my native language._ _I personally checked the cited sources, reviewed and edited the text. I also ran the reproducer and benchmarks myself. I stand behind the findings and take responsibility for the contents of this report._