Skip to content

feat: implement circuit-v2 client transport for relayed connections - #270

Merged
adust09 merged 1 commit into
mainfrom
feat/266-circuit-client-transport
Aug 23, 2026
Merged

feat: implement circuit-v2 client transport for relayed connections#270
adust09 merged 1 commit into
mainfrom
feat/266-circuit-client-transport

Conversation

@adust09

@adust09 adust09 commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Closes #266. Unblocks #258.

Problem

A relayed peer could never become a Connection. The client-side relay exchanges returned a bare StreamIO that nothing upgraded, no Transport accepted a p2p-circuit multiaddr, and registerRelayStopHandler handed the raw stop stream to an application callback with no security handshake.

That blocks #258: every acceptance criterion there that distinguishes a direct connection from a relay connection presupposes a relayed Connection in swConnPool.

Spec

specs/relay/circuit-v2: once the hop / stop CONNECT exchange succeeds, "the original stream becomes the relayed connection", which clients upgrade "with a security protocol and a multiplexer, just like they would e.g. upgrade a TCP connection".

Change

New LibP2P.NAT.Relay.Transport:

  • Dial — reach the relay, negotiate hop, send CONNECT, return the stream as a RawConnection. dialNewInner then runs the existing upgradeOutbound, so Noise + Yamux apply unchanged.
  • ListentransportListen on <relayAddr>/p2p/<relay>/p2p-circuit makes the reservation and exposes inbound stop streams through listenerAccept. They flow into Switch.Listen.handleInbound, so gating, upgrade, resource limits, pooling, notifiers and teardown all apply without new code. listenerAddr reports the reservation address, which puts the circuit address into switchListenAddrs for Identify to advertise.
  • Identification — a relayed connection is one whose connRemoteAddr contains p2p-circuit (existing Relay.isRelayedAddr). Connection gains no new field, so the address stays the single source of truth.

registerNATHandlers now registers the transport and returns (RelayState, CircuitState).

Breaking change

NATConfig.ncOnRelayedStream is removed. The stop stream is now a connection, not an application-owned stream, so there is nothing left for that callback to own. registerNATHandlers returns a tuple.

Tests

LibP2P.NAT.Relay.TransportSpec — address parsing and transportCanDial in isolation, plus four end-to-end tests over three in-process switches on loopback TCP (relay R, target B reserving on R, dialer A):

  • B's reservation address is advertised as a circuit listen address
  • A's dial through the circuit yields an upgraded Connection to B, and B sees the matching inbound relayed connection
  • a ping round-trips over the relayed connection
  • a dial to a target holding no reservation fails

RegistrationSpec's raw-byte bridging test is replaced by the above, which exercises the same path plus the security and muxer upgrade. Full suite: 1145 examples, 0 failures.

Deliberately not in scope

Relayed peers were never reachable as connections. The relay client
exchanges (makeReservation, connectViaRelay, handleStop) returned a bare
StreamIO that nothing upgraded, no Transport accepted a p2p-circuit
multiaddr, and the stop handler handed the raw stream to an application
callback with no security handshake. A relayed Connection therefore could
not exist in the pool.

specs/relay/circuit-v2 states that once the hop/stop CONNECT exchange
succeeds "the original stream becomes the relayed connection", which
clients upgrade "with a security protocol and a multiplexer, just like
they would e.g. upgrade a TCP connection".

Add LibP2P.NAT.Relay.Transport, a Transport that claims p2p-circuit
addresses:

- transportDial reaches the relay, negotiates hop, sends CONNECT and
  returns the stream as a RawConnection, so the existing upgradeOutbound
  pipeline applies Noise and Yamux unchanged.
- transportListen reserves on the relay and exposes inbound stop streams
  through listenerAccept, routing them into Switch.Listen.handleInbound
  so gating, resource limits, pooling, notifiers and teardown all apply.
  listenerAddr reports the reservation address, which puts the circuit
  address into switchListenAddrs for Identify to advertise.
- Relayed connections are recognised by p2p-circuit in connRemoteAddr
  via the existing Relay.isRelayedAddr; Connection gains no new field.

registerNATHandlers now registers the transport and returns the circuit
state alongside the relay state. NATConfig loses ncOnRelayedStream: the
stop stream is a connection, not an application stream, so there is no
longer anything for that callback to own.

The relay's advertised limit is still discarded (#269) and reservations
are not refreshed before expiry (#268).

Unblocks #258.
Closes #266.
@adust09
adust09 requested a review from seetadev August 23, 2026 00:55
@adust09
adust09 merged commit 3cb6ab2 into main Aug 23, 2026
3 checks passed
@adust09
adust09 deleted the feat/266-circuit-client-transport branch August 23, 2026 04:45
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.

relay: implement circuit-v2 client transport (dial p2p-circuit, upgrade relayed streams)

1 participant