Skip to content

Relay panics on circuit request when the destination has a connection without a reservation #6569

Description

@yexiyue

Summary

libp2p-relay's server behaviour picks an arbitrary connection to the destination peer and
asserts that it holds the reservation:

// protocols/relay/src/behaviour.rs:714-719
} else if let Some((dst_conn, status)) = self
    .connections
    .get(&inbound_circuit_req.dst())
    .and_then(|cs| cs.iter().next())     // any connection
{
    assert_eq!(*status, Reservation::Active);

connections is a HashMap<PeerId, HashMap<ConnectionId, Reservation>>. Every connection is
inserted as Reservation::None on ConnectionEstablished and only flipped to Active once a
reservation is accepted. So as soon as the destination has any connection to the relay that
does not carry the reservation, the randomly ordered iterator can yield it and the assertion
takes down the whole relay task.

This is reachable remotely: the source peer only has to send a circuit request while the
destination has an extra connection open. That happens on its own during a reconnect (the old
connection lingers) or when a peer dials the relay again for any other reason — no malice needed,
though it is trivially reproducible on purpose.

Note that Reservation::is_active() already exists but is not used at this call site, which
suggests the check was simply missed here.

Expected behavior

The relay picks the connection that actually holds the reservation. If none does, it denies the
circuit request with NO_RESERVATION — the branch right below already does exactly this.

Actual behavior

The relay process panics and dies.

There is a second, quieter consequence: even when the assertion happens to pass, the circuit is
routed over whichever connection the iterator returned, which need not be the reserved one.

Relevant log output

From a production relay (libp2p pinned at 989cb61, a fork of master with two unrelated WebRTC
patches; this code path is untouched):

2026-07-28T07:02:29.634519414Z thread 'tokio-rt-worker' (8) panicked at protocols/relay/src/behaviour.rs:719:21:
2026-07-28T07:02:29.634522794Z assertion `left == right` failed
2026-07-28T07:02:29.634525201Z   left: None
2026-07-28T07:02:29.634534572Z  right: Active

The node had been up for ~44 minutes serving browser clients over WebRTC-Direct; the log ends
there because the process exited.

Possible Solution

Look for the reserved connection instead of asserting on an arbitrary one:

.and_then(|cs| cs.iter().find(|(_, status)| status.is_active()))

and drop the assert_eq!. Requests with no reserved connection then fall through to the existing
NO_RESERVATION branch.

I have a patch with a regression test ready and will open a PR.

Version

master (reproduced at 38b8a2c0). The code is unchanged since at least libp2p-relay 0.21.

Would you like to work on fixing this bug?

Yes


Disclosure: this report was written with AI assistance (Claude Code). The crash is from my own
production relay; I read the code path and reproduced the panic in a test before filing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions