Skip to content

quic: a client that changes address is blackholed - the path is fixed at accept #204

Description

@MDA2AV

NAT rebinding and Wi-Fi-to-cellular are ordinary events on a UDP transport, and QUIC exists partly to survive them. ioxide currently does not.

The shim discards the address every datagram arrives from:

int iq_conn_read(iq_conn *c, const void *remote_sa, size_t remote_salen, ...)
{
    (void)remote_sa; (void)remote_salen;   /* milestone: no migration; path is fixed at accept */

so ngtcp2 is told every packet came from the accept-time path, never emits a PATH_CHALLENGE, and every reply goes to the original 4-tuple. QuicConnection.UpdatePeerAddress exists, is public, is documented as "adopt a validated peer migration" — and has zero callers anywhere in the repo.

The failure is silent on both sides. The clients packets still route (the connection id has not changed), the server keeps answering an address nobody is listening on, and the connection dies at the idle sweep with no error to either peer.

Two things to be careful about when this is implemented, both from nginxs quic_migration.t:

  1. A migration must be validated before it is adoptedthis was wrong, and PR quic: let a connection survive its peer changing address #209's reviewers caught it. ngtcp2 adopts first and validates after: conn_recv_non_probing_pkt_on_new_path copies the new DCID into dcid.current before creating the path validation, and for a plain remote-address change the pref_addr guard is false, so adoption is unconditional. What prevents amplification is not the ordering: the packet must decrypt under 1-RTT keys, which an off-path spoofer cannot forge, and ngtcp2 caps an unvalidated path at 3× what it received. A binding that waited for validation before moving its send address would send ngtcp2's own new-path packets to the old address.
  2. nginx ticket #2488 was $remote_addr coming back truncated mid-migration, because a stream cached a pointer plus an old length while the address was rewritten under it. UpdatePeerAddress memcpys into the same PeerAddr allocation and updates PeerAddrLen afterwards — the identical shape, so it wants the length updated first or the buffer swapped rather than edited.

Blocked in practice on #205: on a multi-reactor server the datagram may not even reach the reactor that owns the connection.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions