v2.4.0
Highlights
- Address announcement is now coordinated by a switch-owned
AddressManager. Listen addresses, explicit announcements, NAT mappings, relay addresses, and Identify observations feed a shared candidate set, with periodic reachability verification. Repeated observations from the same peer no longer count as independent confirmation. - Dialing gained optional address ranking and per-address/per-peer failure backoff. Ranked dialing overlaps DNS resolution with connection attempts, tries direct addresses without waiting for slow names, and processes DNS results as they arrive. Enable these features with
withDialRanking()andwithDialBackoff(). - AutoTLS received extensive issuance and renewal fixes, including certificate-expiry handling, retry accounting, DNS retries, ACME POST-as-GET requests, CSR encoding, and authentication edge cases. ACME requests are restricted to the directory origin, and Docker integration tests cover the issuance flow.
- Multiaddresses now support
/sni, and secure WebSocket dialing can use an explicit TLS server name. TCP dual-stack address reuse, WebSocket shutdown reporting, and SOCKS5 response and hostname validation were also fixed. - Kademlia routing-table admission and maintenance were hardened with per-bucket IP diversity limits, failed-probe backoff, automatic re-seeding of undersized tables, and safer handling when admission-probe capacity is exhausted. Service-table lookups no longer double-hash their targets, provider RPCs are tracked immediately, and refreshes preserve provider keys.
- Service Discovery now filters undialable addresses, republishes advertisements when addresses change, preserves advertisement sequence numbers across rotations, and uses Unix seconds for ticket timestamps. Registrar admission follows the shared Kademlia path, local advertisements are checked first, cancellation propagates correctly, and provider-discovery latency is measured.
- PubSub handling was hardened with malformed-message overhead accounting in FloodSub, a bounded FloodSub seen cache, graylisted-peer RPC rejection in GossipSub, and correct handling of non-canonical Peer ID aliases. Peer background tasks are stopped before connection teardown.
- Stream and connection cleanup was improved across BufferStream, Mplex, yamux, and negotiation paths. Fixes release read buffers, wake blocked readers and writers on close, preserve buffered data and EOF, and clean up failed negotiations and interrupted stream opens.
- AutoNAT dial-back validation now requires fresh inbound connections and validates successful response address indexes. Reachability classification distinguishes IPv4 from IPv6, while relay reservation and hole-punching tasks receive more reliable shutdown cleanup.
- Rendezvous now bounds its namespace table, prunes empty namespaces, releases expired registrations on the cleanup heartbeat, and avoids allocating an index for every registration during discovery.
- Logging now uses more consistent severities and fields, rate limits selected recurring messages, and shortens large values. Sensitive key and secret types are redacted during diagnostic formatting and generic JSON serialization. Public-key parsing and CID validation and hashing were also tightened.
- C bindings now include the remote Peer ID in incoming-stream events, require a running switch for service-discovery operations, and use nim-ffi 0.3.0. Dependency requirements were updated to nim-websock 0.4.1 and nim-libplum 0.6.2.
Integration notes
The largest changes affect which addresses a node advertises, how AutoNATv2 proves inbound reachability, and when KadDHT startup completes. Dial ranking and general dial backoff are available as opt-in features. Applications that call KadDHT APIs directly should also review the new Key and Value types.
Address management, dialing, stream cleanup, and diagnostics affect the shared networking stack. AutoNAT, KadDHT, Service Discovery, AutoTLS, PubSub, Rendezvous, and C-binding changes apply when your application uses those components. The sections below identify configuration-dependent behavior so users can distinguish automatic changes from features they must enable.
Address announcements: one shared view of how peers can reach the node
The switch now owns an AddressManager shared by Identify, NAT mapping, AutoNAT, relay services, and address announcement. These components contribute addresses to the same candidate set, retaining both their source and verification state.
Observable behavior:
- Repeated reports from one peer no longer establish consensus. Identify observations count distinct Peer IDs; a newer report replaces that peer's previous report. The default threshold remains three agreeing peers, with up to ten peer observations retained. Reconnecting repeatedly to the same peer cannot satisfy that threshold.
- An inferred address is not automatically a proven address. With AutoNATv2 candidate derivation enabled, addresses inferred solely from Identify are advertised only after a successful verification. Listen addresses and addresses contributed by other sources can still be advertised while unverified.
- Failed verification can change the advertised address list. Public candidates marked unreachable are excluded from automatic announcements. Private addresses are retained because a failed external dial-back does not establish whether LAN peers can reach them. The configured address policy still controls visibility.
- Relay announcements can give way to direct addresses. Once a public direct address is confirmed, automatic announcements omit relay addresses with the same IP family. Confirmation over IPv6 does not suppress an IPv4 relay address, or vice versa.
- Addresses disappear when their source withdraws them. An expired mapping or lost relay reservation removes that source's contribution. A candidate remains if another source still supplies it.
- Explicit announcements remain an operator override. A nonempty explicit announce list takes precedence over the manager's automatic selection, including its reachability filtering. Do not assume an explicitly configured address has passed verification.
Verification is optional: creating a switch does not itself install a verifier. AutoNATv2 supplies one when its service starts. The manager defaults to a five-minute verification interval and a two-minute budget for each pass; an inconclusive result retains the previous state. Generating additional probe candidates from Identify observations remains opt-in (enableDialableCandidates = false), as in v2.3.0. Previously, enabling it prepended inferred addresses to the service's multi-address dial requests; now it adds them to the shared manager for individual verification. The flag does not disable recording observations or using them in the address mapper after the node is classified reachable.
Integration implication: advertised addresses can evolve after startup. Applications that publish or cache their own discovery records should check whether those records follow local address updates or capture the startup list once. Tests that infer an external address by repeating observations from one Peer ID need distinct observers. Explicit announce lists need separate operational validation.
AutoNAT: stronger proof of inbound reachability
AutoNATv2 fixes false-positive reachability checks caused by connection reuse. The protocol calls for the server to dial the selected address and return the request's nonce over that connection. A stream on an already established connection does not prove that the requested address accepts a new inbound connection. nim-libp2p now forces a new connection on the server and rejects client-side dial-backs unless their underlying transport connection is inbound and opened after the pending request began.
This implements the protocol's intended reachability test; it is not a new protocol requirement. The spec does not explicitly mandate the connection-age check. go-libp2p's server uses a separate dialer host and closes its dial-back connection after the attempt, while the inspected Go client checks the nonce and address consistency without nim-libp2p's explicit inbound-direction and connection-age checks.
A successful response is checked against the requested address and the local address where the dial-back arrived. Out-of-range or inconsistent address indexes are rejected. An omitted index is interpreted as zero and still validated; omission does not bypass the check.
The AutoNATv2 verifier asks eligible outbound-connected peers, excludes peers with an existing inbound connection, and skips verification when fewer than two incoming connection slots remain. No eligible peer, timeout, or protocol error produces no new verdict. The node may therefore remain Unknown until it can obtain useful evidence.
Address eligibility now uses IP-family-aware classification: IPv6 private and link-local addresses are not treated as globally routable simply because they are not IPv4 private addresses. AutoNATv2's server also checks that it supports the requested address family and refuses nonglobal dial-back targets, including DNS and relay addresses at that eligibility check. The service still exposes an aggregate reachability status; a reachable node is not a promise that every advertised address or both IP families work.
Integration implication: AutoNATv2 can stop reporting success in setups that previously succeeded by reusing an existing connection. Reachability tests need a genuine inbound dial-back and spare incoming capacity. Check which AutoNAT version your application enables: the verifier and fresh-connection changes above are specifically v2 behavior.
KadDHT: startup returns sooner, unreachable peers stop consuming retries
- Startup no longer waits for bootstrap to finish. KadDHT starts bootstrap in the background and returns while the routing table is still being populated. Applications that previously treated completion of
start()as completion of initial bootstrap should useawait kad.waitBootstrap()where that sequencing matters. This waits for the bootstrap task to end, including timeout; it does not guarantee a populated routing table or successful discovery.bootstrapTimeoutdefaults tobucketRefreshTime(ten minutes by default). - Lookups move past peers whose dial failed. A peer that cannot be dialed is removed from the current lookup's retry path and recorded in Kademlia's failure backoff. This avoids repeatedly spending bootstrap/lookup time on the same unreachable peer. Changed addresses can make the peer eligible again. This behavior does not require enabling the general dialer's optional backoff.
- Full buckets can admit newly probed peers. Previously, a bucket full of still-useful peers could reject every newcomer. Successful admission probes can now rotate a member even in that situation, at most once per bucket per minute. Rotation protects the longest-held half of the bucket and selects a victim from the newer half. A healthy incumbent can therefore leave the routing table without having failed a liveness check.
- Liveness maintenance avoids a busy loop. Finished probe futures no longer cause the background loop to spin without yielding, avoiding that source of CPU usage and event-loop starvation.
- Refreshing a provided key no longer evicts another key at capacity. Calling
startProvidingagain for an already tracked key refreshes it without dropping the oldest entry. Optimistic provider RPCs are tracked immediately so shutdown can cancel them even while the initiating lookup is still running.
API migration: Key and Value are now distinct byte-sequence types. Code that previously passed seq[byte] directly may need explicit construction with Key.fromBytes / Value.fromBytes, and toBytes when reading raw bytes. Key.init builds a 32-byte, zero-padded routing key; Key.fromBytes preserves the supplied bytes. Choose according to the existing operation's semantics. This type distinction does not itself introduce a new wire encoding.
Integration implication: applications using KadDHT should review their startup readiness assumptions and direct DHT calls. Routing-table membership changes alone are no longer evidence that the removed peer became unreachable. Provider refreshes should preserve the set of tracked keys when it is full.
Dialing: optional ranking and failure backoff
Enable address ranking with withDialRanking() to start connection attempts without waiting for every DNS lookup to finish. Direct addresses can be tried while names resolve; DNS results enter the candidate set as they arrive. A slow sibling DNS result no longer holds up all usable candidates.
Enable general dial backoff separately with withDialBackoff(). Repeatedly failing addresses and peers are temporarily skipped instead of redialed on every request. The default starts at five seconds, doubles with further failures, and caps at five minutes, with jitter. A recovered peer may consequently wait for backoff to expire before another normal attempt.
Both features are disabled by default. Applications opting in should check assumptions about dial order and immediate retries. Outbound dialing also has a bounded timeout so a stuck attempt does not hold its resources indefinitely.
Service Discovery: advertisements follow address changes
These changes apply to the libp2p Service Discovery module, separately from ordinary KadDHT provider discovery.
- Wildcard hosts and unresolved port-zero addresses are filtered from publication and dialing paths by default. Bind addresses such as
/ip4/0.0.0.0/tcp/0must become actual reachable endpoints before use.withUndialableAddresses()is available for local tests. - Automatically generated advertisements are rebuilt and republished when local addresses change. Caller-supplied advertisement bytes remain the caller's responsibility.
- Maintenance rotations reuse cached advertisement bytes and sequence numbers, preventing the same advertisement from appearing as a new version on every rotation. Actual address updates rebuild the generated record.
- Registrar candidates go through shared Kademlia admission, including probing the discovery codec. Merely sending a registration or query does not earn a routing-table seat for a peer that cannot serve that protocol.
- Discovery checks local advertisements first, and cancellation propagates through RPCs. Ticket timestamps use Unix seconds rather than process-local monotonic time. Provider-discovery latency metrics are available.
Other changes
AutoTLS and secure WebSockets
Certificate renewal now uses certificate wall-clock expiry and applies the renewal buffer once. A failed initial issuance round no longer permanently stops the manager: later heartbeat runs retry. issueRetries means retries after the initial attempt, so zero still permits one attempt. DNS retries, ACME resource reads, CSR encoding, and authentication handling were corrected. ACME requests stay on the directory origin; servers returning cross-origin resource URLs are rejected.
AutoTLS callers should review configuration renames: acmeServerURL → acmeDirectoryURL, brokerURL → registrationURL (now a Uri), and dnsServerURL → domainSuffix. Certificate expiry is now a DateTime.
Multiaddresses support /sni, allowing secure WebSocket dialing to specify the TLS hostname independently of the destination IP. WebSocket transport shutdown reports closure without retrying; TCP dual-stack reuse and SOCKS5 response/hostname validation were corrected.
PubSub, streams, and shutdown
FloodSub accounts for malformed-message overhead and bounds its seen cache. GossipSub rejects RPCs from graylisted peers. Equivalent noncanonical Peer ID representations are handled consistently, and peer background tasks stop before connection teardown.
Closing streams releases read buffers and wakes blocked readers and writers. Mplex delivers buffered data and EOF before reporting the underlying connection down. Failed negotiations and interrupted stream opens clean up their resources. Relay reservation and hole-punching shutdown cancels and drains background work more reliably.
For applications, these fixes affect close/error timing and resource usage during disconnects, cancellation, and shutdown. They do not establish a guarantee that every interrupted operation completes successfully.
Rendezvous, diagnostics, and bindings
Rendezvous bounds the namespace table, prunes empty namespaces, and releases expired registrations on cleanup heartbeats. Discovery avoids allocating an index for every registration, reducing allocation overhead for large registries. Applications using many namespaces should review capacity behavior.
Log levels and fields have changed; selected recurring messages are rate limited and large values shortened. Log-based dashboards and alerts may need adjustments. Sensitive key/secret types are redacted in diagnostic formatting and generic JSON serialization. Public-key parsing is stricter, and equivalent CIDs hash consistently after validation fixes.
Incoming-stream events in C bindings now include the remote Peer ID. Service Discovery operations require a running switch. Dependency requirements include nim-ffi 0.3.0 for bindings, nim-websock ≥0.4.1, nim-libplum ≥0.6.2, lsquic ≥0.9.0, and nim-boringssl ≥0.0.11.
What's Changed
- feat(addr-mgr): the switch owns the ObservedAddrManager by @gmelodie in #2924
- fix(service-disco): validate advert length and re-check client mode by @gmelodie in #2922
- chore(cbind): use nim-ffi 0.3.0 by @gmelodie in #2909
- fix(dialer): bound outbound dials so one stuck peer cannot stall the node by @gmelodie in #2929
- refactor(kad): tables into virtual system by @SionoiS in #2918
- fix(autotls): checkDNSRecords retry loop by @rlve in #2930
- feat(addr-mgr): the AddressManager owns every announced address by @gmelodie in #2928
- chore(nat): bump libplum to v0.6.2 by @gmelodie in #2932
- test(autotls): broker by @rlve in #2931
- fix(kad): an admission probe no longer waits out the dialer timeout by @gmelodie in #2936
- test: Expand stream tests to yamux by @etan-status in #2812
- feat(addr-mgr): a Verifier confirms candidates on a heartbeat by @gmelodie in #2934
- fix(pubsub): initialize RPC handlers before connecting by @richard-ramos in #2941
- chore(kad): per-bucket IP diversity caps by @gmelodie in #2938
- fix(autotls): renewal never fires and renewBufferTime is applied twice by @rlve in #2933
- fix(autotls): retry a failed issuance round and fix the issueRetries count by @rlve in #2942
- fix(peerstore): bound the identify stream close by @gmelodie in #2940
- fix(ipaddr): a public IPv6 address counts as a public address by @gmelodie in #2944
- test(autotls): broker payload addresses, consolidate ACMEApi doubles by @rlve in #2950
- feat(kad): re-seed a routing table that fell below a minimum size by @gmelodie in #2937
- fix(TcpTransport): dual-stack reuse bug by @vladopajic in #2952
- feat(quic): add stream idle timeouts by @richard-ramos in #2947
- fix(mplex): serve buffered data and EOF before reporting the connection down by @gmelodie in #2946
- chore(tests): start/stop once by @vladopajic in #2955
- test(transports): unify tor transport test behavior for half closed connections by @vladopajic in #2954
- test(autotls): certificate handoff by @rlve in #2958
- chore: add linter for nix dependencies by @richard-ramos in #2960
- fix(tests): autotls DNS test segfault on Nim devel with refc by @gmelodie in #2959
- fix(ipaddr): compare IPv6 clients too by @gmelodie in #2956
- chore(kad): stop re-probing a peer whose admission probe just failed by @gmelodie in #2939
- feat(addr-mgr): register the verifier and migrate the consumers by @gmelodie in #2953
- fix(kad): findNode no longer double-hashes a service table's target by @gmelodie in #2943
- feat(multiaddress): add SNI protocol support by @richard-ramos in #2963
- fix(autonatv2): require address index on success by @richard-ramos in #2945
- feat(websocket): support explicit SNI addresses by @richard-ramos in #2964
- test(transports): cover accept-after-stop / autotls private key and ws address downgrade by @rlve in #2962
- test(autotls): certificate issuance by @rlve in #2966
- test(autotls): acme signed requests by @rlve in #2968
- fix(kad): keep peer addresses when the admission probe cap is full by @gmelodie in #2967
- feat(cbind): include peerId on IncomingStreamEvent by @gmelodie in #2969
- test(autotls): ACME client request flow by @rlve in #2971
- test(autotls): broker bearer lifecycle by @rlve in #2973
- refactor(pubsub): separate app's path from protocol messages send/brodcast path by @vladopajic in #2965
- test(autotls): PeerID Auth client by @rlve in #2976
- chore(pubsub):
sendResponseimprovement by @vladopajic in #2980 - test(autotls): certificate expiry parsing by @rlve in #2978
- test(autotls): acme utils and key authorization by @rlve in #2983
- test(autotls): config constructor and peer ID label by @rlve in #2988
- test(transports): quic dialer does not observe a remote connection close by @rlve in #2993
- fix(stream): free the read buffer when a BufferStream closes by @gmelodie in #2991
- fix(kad): drop a peer's stream as soon as it closes by @gmelodie in #2990
- chore(ci): drop i386 by @gmelodie in #2996
- fix(bufferstream): always wake the reader when the stream closes by @gmelodie in #2989
- test(autotls): certificate renewal is not propagated to the wss listener by @rlve in #2995
- feat(dialer): collect dial candidates before dialing, behind a flag by @gmelodie in #2992
- fix(bufferstream): avoid stranding pushes on close by @richard-ramos in #2998
- fix(cbind): guard service discovery entry points on a started switch by @gmelodie in #3006
- fix(autotls): read ACME resources with POST-as-GET and CSR padding by @rlve in #2997
- feat(dialer): overlap ranked dialing with name resolution by @gmelodie in #3001
- chore: remove deprecated unsafeAddr usage by @tersec in #3004
- test(autotls): full integration flow in docker by @rlve in #2999
- fix(autotls): raise an http error when a request url cannot be resolved by @rlve in #3010
- test(autotls): move integration coverage into the docker suite by @rlve in #3008
- chore(autotls): rename dnsServerURL to domainSuffix by @rlve in #3012
- test(autotls): reuse stub helpers and improve assertions by @rlve in #3013
- test(autorelay): stop leaves its reservation loops running - reproduction by @rlve in #3019
- feat(dialer): back off the addresses and the peers that keep failing by @gmelodie in #3011
- fix(floodsub): punish nodes sending malformed messages by @gmelodie in #3017
- chore(tests): standardize multiaddress creation by @vladopajic in #3021
- fix: handle disconnect while opening stream by @etan-status in #3015
- chore(tests): use switch builder by @vladopajic in #3020
- fix(rendezvous): release expired registrations on the deletion heartbeat by @gmelodie in #3023
- chore(logs): add log-severity policy by @vladopajic in #3032
- fix(rendezvous): count the registrations of a peer in a table by @gmelodie in #3029
- fix(utils): make the offsettedseq apply and flushIf procs compile by @gmelodie in #3027
- chore(wstransport): bump websock to 0.4.1 by @gmelodie in #3026
- fix(rendezvous): discover no longer allocates one int per registration by @gmelodie in #3025
- chore(logs): standardize log fields by @vladopajic in #3038
- chore(perf): rename uploadSize to responseSize by @gmelodie in #3040
- fix(floodsub): limit the entry count of the seen cache by @gmelodie in #3036
- fix(addr-mgr): count the peers which observe an address, not the reports by @gmelodie in #3037
- fix(mplex): refine stream setup buffering by @richard-ramos in #3035
- fix(crypto): improve public key parsing by @richard-ramos in #3034
- fix(dialer): dial dnsaddr results without waiting for siblings by @richard-ramos in #3007
- chore: native hkdf and autotls pem encoding by @gmelodie in #3024
- fix(service-disco): cache the advertisement bytes so rotations keep the seqNo by @gmelodie in #3033
- chore(logs): remove unecessery logs by @vladopajic in #3044
- fix(autonatv2): bound dial backs by @gmelodie in #3041
- chore(logs): add logs with background operation by @vladopajic in #3053
- fix(pubsub): handle non-canonical PeerId aliases by @richard-ramos in #3052
- chore(logs): messages improved by @vladopajic in #3050
- fix(gossipsub): ignore RPCs from graylisted peers by @gmelodie in #3042
- chore(tools): fix logs fields lint by @vladopajic in #3051
- ci(logs): add test build with trace log lvl by @vladopajic in #3059
- fix(autotls): keep ACME requests on the directory origin by @gmelodie in #3048
- chore(logs): use shortlog for large fields by @vladopajic in #3060
- fix(wstransport): report closed transport without retrying by @richard-ramos in #3058
- fix(tor): reject DNS names exceeding SOCKS5 length limit by @richard-ramos in #3055
- feat(dns): full DNS message codec by @gmelodie in #3039
- chore(logs): add logs with rate limit by @vladopajic in #3062
- chore(logs): redact field content by @vladopajic in #3064
- fix(service-discovery): use Unix seconds for ticket timestamps by @richard-ramos in #3057
- refactor: simplify error handling with isOkOr by @richard-ramos in #3065
- fix(dialer): retain lookup results across suspended dials by @richard-ramos in #3056
- chore(logs): add logs by @vladopajic in #3061
- ci: increasing timeout test workflows by @vladopajic in #3083
- chore(logs): add
formatItfor all types by @vladopajic in #3075 - feat(service-discovery): measure provider discovery latency by @gmelodie in #3063
- chore: remove hexdump example, simplify test setup and fix cancellation handling by @richard-ramos in #3071
- fix(cid): correct validation offset and hash equivalent CIDs consistently by @richard-ramos in #3069
- chore(logs): reuse
shortLogcode by @vladopajic in #3076 - fix(transports): harden SOCKS5 handling and simplify transport cleanup by @richard-ramos in #3066
- fix(cbind): exit examples safely on timeout and use local dependency paths by @richard-ramos in #3067
- fix(kademlia): track provider RPCs immediately and preserve keys on refresh by @richard-ramos in #3073
- fix(autotls): handle auth edge cases and clean up pending DNS queries by @richard-ramos in #3081
- refactor(kademlia): unify get-value response handling by @richard-ramos in #3072
- fix(rendezvous): cap the namespace table and prune empty namespaces by @gmelodie in #3028
- chore(kad): add
Valuetype by @vladopajic in #3085 - fix: clean up failed negotiations and reject truncated buffer reads by @richard-ramos in #3070
- fix(connmanager): restore readiness waits and tag decay on restart by @richard-ramos in #3068
- fix(tests): exclude local dependencies from test discovery by @richard-ramos in #3091
- fix(service_discovery): propagate RPC cancellation and check local ads first by @richard-ramos in #3074
- feat: redact sensitive types by @vladopajic in #3082
- fix(nat): enforce dial-back validation and clean up relay and hole-punching lifecycles by @richard-ramos in #3079
- fix(autonatv2): require fresh inbound dialback connections by @richard-ramos in #3089
- fix(autonat): classify addresses per family by @gmelodie in #3088
- fix(service-disco): gate registrar seats on the discovery codec by @gmelodie in #3093
- chore(kad): make
Key&Valuedistinc types by @vladopajic in #3086 - fix(tests): copy lent loop variables before asyncTest capture by @gmelodie in #3097
- chore(kad): utilize
Key&Valuein protobuf types by @vladopajic in #3099 - fix(service-disco): admit registrar senders through the shared Kademlia path by @gmelodie in #3087
- fix(pubsub): stop peer tasks before connection teardown by @richard-ramos in #3100
- fix(service-discovery): stop publishing and dialing undialable addresses by @gmelodie in #3080
- fix(quic): avoid caching partially constructed dial endpoints by @richard-ramos in #3111
- chore: bump lsquic by @richard-ramos in #3116
- fix(kad): non-blocking bootstrap by @gmelodie in #3134
- fix(kad): stop retrying unreachable peers during bootstrap by @gmelodie in #3135
- chore(logs): tune logs for kademlia & service-discovery by @vladopajic in #3140
- chore(kad): simplify
maintainLivenessby @vladopajic in #3172 - fix(kad): liveness loop spins on finished probes by @gmelodie in #3171
- fix(kad): let probed peers rotate full buckets by @gmelodie in #3180
Full Changelog: v2.3.0...v2.4.0