Skip to content

fix(identify): cross-protocol translation for shared-port transports#6277

Open
lthibault wants to merge 5 commits intolibp2p:masterfrom
lthibault:feature/identify-fix
Open

fix(identify): cross-protocol translation for shared-port transports#6277
lthibault wants to merge 5 commits intolibp2p:masterfrom
lthibault:feature/identify-fix

Conversation

@lthibault
Copy link

@lthibault lthibault commented Feb 18, 2026

Description

When an outbound connection triggers identify, the behaviour attempts to translate
the observed address into a stable external address candidate by substituting the
observed IP into a matching listen address. The translation guard requires both
addresses to be the same transport type (TCP→TCP, QUICv1→QUICv1, etc.).

When no same-protocol listen address exists — e.g. a node whose only listener is
QUIC but whose outbound TCP connection causes an observe — translated_addresses
is empty and the fallback unconditionally emits the raw observed address as a
NewExternalAddrCandidate. The observed address carries the OS-assigned ephemeral
source port, not the node's listen port, so the emitted candidate is wrong.

AutoNAT then receives that ephemeral port as a dial-back target, finds it closed,
and can incorrectly conclude the node is unreachable (NatStatus::Private).

This scenario arises in any deployment where TCP and QUIC share a single listen
port (e.g. both on port 4001). In that case the observed IP is valid for all
transports on that port, and cross-protocol translation is correct.

Fix: when same-protocol translation yields nothing, attempt cross-protocol
translation against listen addresses whose port equals the observed port. Fall
back to the raw observed address only when no listen address exists at all
(pure dial-out nodes), which preserves existing behaviour for that case.

Notes & open questions

  • The port-equality gate (port_of(listen) == port_of(observed)) is what makes
    cross-protocol translation safe. If TCP and QUIC use different ports, the gate
    prevents a wrong candidate and the existing fallback applies unchanged.
  • Pure dial-out nodes (no listeners) are unaffected: the cross-protocol candidate
    list is empty, so the raw observed address is still emitted as before.
  • The port_of helper extracts the first Tcp or Udp port component from a
    multiaddr. It returns None for relay circuit addresses and other non-port
    address types, naturally excluding them from cross-protocol translation.

Change checklist

  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works
  • A changelog entry has been made in the appropriate crates

Add PeerScoreParameters struct to expose granular breakdown of peer scores:
- Individual contributions for P1-P7 score parameters
- Slow peer penalty tracking
- Final score field

Add peer_score_params() public API method to retrieve detailed score breakdown.

This enables external tooling and debugging to inspect individual components
of the gossipsub peer scoring algorithm per the v1.1 spec.
…nsports

When an outbound ephemeral TCP connection triggers identify, the observed
address carries an ephemeral source port. The existing same-protocol
translation guard correctly pairs TCP observations with TCP listen addresses
and QUIC observations with QUIC listen addresses, but produces an empty
result when the node has only a QUIC listener and receives a TCP observation
(or vice versa).

Previously the fallback unconditionally emitted the raw observed address —
including its ephemeral port — as an external address candidate. AutoNAT
then probes that ephemeral port, finds it closed, and incorrectly concludes
the node is unreachable (NatStatus::Private).

This is a real-world failure mode when TCP and QUIC share the same port
(a common deployment pattern). In that case the observed IP is correct for
both transports, so cross-protocol translation is valid: use the observed IP
with the listen port, gated on the ports matching across the two addresses.

Changes:
- Add `port_of(addr)` helper to extract TCP/UDP port from a multiaddr.
- When same-protocol translation yields nothing, attempt cross-protocol
  translation against listen addresses whose port matches the observed port.
- Fall back to the raw observed address only when no listen addresses exist
  at all (pure dial-out nodes), preserving existing behaviour for that case.
…on fix

- Bump version to 0.47.1 (0.47.0 is already released on crates.io)
- Add changelog entry under 0.47.1
- Add unit tests for port_of() helper
@lthibault lthibault changed the title fix(identify): cross-protocol address translation for shared-port transports fix(identify): cross-protocol translation for shared-port transports Feb 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant