Skip to content

endpoint: a relay that stalls after the handshake is undetectable — no client-initiated liveness probe on the relay wire #96

Description

@lannbot

A relay that completes the handshake and then goes silent is undetectable
today. #93 (fixed by #95) bounded the dial; this is the post-handshake
sibling: once RelayConn is pooled, nothing probes the wire, so a stall
that never errors the websocket pins the endpoint's relay state silently.

What exists

  • The client answers server-initiated pings (core/src/relay.rs,
    tag::PING -> pong reply) but never initiates its own, and tracks no
    pong deadline.
  • The pump arms a home-relay redial only when a receive or send on the
    websocket errors (endpoint/src/endpoint_impl.rs, the
    next_relay_event error arms). A stalled wire errors nothing: the
    receive future pends forever, and websocket sends buffer locally and
    resolve.
  • QUIC keep-alives (5s, KEEP_ALIVE_INTERVAL) ride the relay wire but
    protect connections, not the relay entry: they detect nothing when
    no connection is live, and when connections idle out at 30s the pool
    entry stays "healthy".

Consequences

  • Home relay: stalled-but-open wire means no redial is ever armed —
    the endpoint is home-dead exactly as in endpoint: relay dials are unbounded — a relay that accepts and stalls pins bind, ensure-relay, and the home-relay redial #93's redial case, but with no
    deadline anywhere on the path. New dials through the home relay
    black-hole; existing connections die at the QUIC idle timeout.
  • Foreign relays: the pool entry and its URL mapping stay live, so
    later dials through that relay black-hole instead of reconnecting
    (retire_relay only runs on a receive error).
  • A middlebox that holds TCP open while dropping payload produces exactly
    this shape; so does a relay process wedged after accept.

Open items

  1. Upstream parity: what does iroh-1.0.3's relay client actor do about a
    silent wire (client-initiated pings? pong deadlines? send-progress
    bounds)? Whatever the mechanism, it is the parity target — the frames
    (PING/PONG, core/src/relay_frames.rs) are already implemented.
  2. Send-side liveness is host-dependent and unmeasured: a stalled TCP
    peer grows the websocket send buffer. On the browser-profile host
    send resolves locally regardless; whether the wasmtime host's
    websocket provider can pend a send indefinitely (pinning the pump's
    inline send await) needs measurement.
  3. Whatever lands, the gate is an exam stub that completes the relay
    handshake and then goes silent (the natural extension of scenario 8's
    stall stub), with the endpoint required to retire/redial within a
    deadline.

Found during the #93 work (the dial-deadline sweep); recorded here per
the findings-are-issues rule.

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