Skip to content

Stateless reset tokens are not scoped by remote address #3262

Description

@LiD0209

Stateless reset tokens are not scoped by remote address

Summary

RFC 9000 requires endpoints to remember stateless reset tokens associated with both connection IDs and remote addresses for recently sent datagrams. s2n-quic stores tokens in a global token -> InternalConnectionId map and matches a received datagram by token only. The implementation has an explicit exception file saying it intentionally does not use the remote address for this check, so this is a real RFC mismatch rather than missing static evidence.

Standard Requirement

Official standard: https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1

RFC 9000 Section 10.3.1:

An endpoint remembers all stateless reset
tokens associated with the connection IDs and remote addresses for
datagrams it has recently sent.

The same paragraph later states:

The endpoint identifies a received datagram as a Stateless Reset by
comparing the last 16 bytes of the datagram with all stateless reset
tokens associated with the remote address on which the datagram was
received.

The token set is therefore scoped by the datagram's remote address.

Relevant Source Code

implementions/s2n-quic/quic/s2n-quic-transport/src/connection/connection_id_mapper.rs:53-84

pub(crate) struct StatelessResetMap {
    /// Maps from a hash of peer stateless reset token to internal connection IDs
    map: HashMap<stateless_reset::Token, InternalConnectionId, HashState>,
}

pub(crate) fn insert(
    &mut self,
    token: stateless_reset::Token,
    internal_id: InternalConnectionId,
) {
    self.map.insert(token, internal_id);
}

pub(crate) fn remove(
    &mut self,
    token: &stateless_reset::Token,
) -> Option<InternalConnectionId> {
    self.map.remove(token)
}

The stored key is only the stateless reset token; no remote address is stored.

implementions/s2n-quic/quic/s2n-quic-transport/src/connection/connection_id_mapper.rs:300-319

pub fn remove_internal_connection_id_by_stateless_reset_token(
    &mut self,
    peer_stateless_reset_token: &stateless_reset::Token,
) -> Option<InternalConnectionId> {
    let mut guard = self
        .state
        .lock()
        .expect("should succeed unless the lock is poisoned");
    guard.stateless_reset_map.remove(peer_stateless_reset_token)
}

The lookup API accepts only a token, not the datagram remote address.

implementions/s2n-quic/quic/s2n-quic-transport/src/endpoint/mod.rs:905-916

let token_index = payload.len().checked_sub(StatelessResetTokenLen)?;
let buffer = buffer.skip(token_index).ok()?;
let (token, _) = buffer.decode().ok()?;
let endpoint_context = self.config.context();
let internal_id = self
    .connection_id_mapper
    .remove_internal_connection_id_by_stateless_reset_token(&token)?;

The endpoint extracts the last 16 bytes and performs a global token lookup.

implementions/s2n-quic/specs/exceptions/transport/10.3.1.toml

reason = '''
s2n-quic maintains a hash map of all Stateless Reset Tokens to the connection
they are associated with.
...
In addition, validating the remote address matches the
remote address associated with the Stateless Reset Token provides minimal additional
security benefit ...
'''

The project records this as an intentional exception, not as implemented RFC behavior.

Implementation Behavior

When a peer CID becomes used, PeerIdRegistry inserts its token into stateless_reset_map. When an inbound datagram cannot be processed normally, endpoint code extracts the trailing 16-byte token and asks ConnectionIdMapper for a matching connection. Neither insertion nor lookup includes the remote address.

Inconsistency Reason

The RFC requires remembering and comparing tokens associated with the remote address on which the datagram was received. s2n-quic compares against a global token map. That means a valid token can match independently of the datagram's remote address, which is broader than the RFC-scoped comparison.

Runtime Evidence

Token lookup uses the global map:

cargo test -p s2n-quic-transport connection::connection_id_mapper::tests::remove_internal_connection_id_by_stateless_reset_token_test -- --exact --nocapture

Result: passed, exit code 0.

Token insertion occurs when a peer CID is consumed:

cargo test -p s2n-quic-transport connection::peer_id_registry::tests::consume_new_id_should_return_id -- --exact --nocapture

Result: passed, exit code 0.

  • opt/runs/rfc9000/rfc9000-s2nquic/501-1000/reports/runtime/0232-token-insert-on-peer-id-use.stdout.log
  • opt/runs/rfc9000/rfc9000-s2nquic/501-1000/reports/runtime/0232-token-insert-on-peer-id-use.stderr.log
  • opt/runs/rfc9000/rfc9000-s2nquic/501-1000/reports/runtime/0232-token-insert-on-peer-id-use.exitcode.txt

Impact

Stateless Reset detection is not constrained to tokens associated with the received datagram's remote address. This expands the matching scope beyond RFC 9000's address-scoped token set.

Fix Direction

Store stateless reset tokens with the relevant remote address, and pass the received datagram's remote address into Stateless Reset lookup so comparison is limited to tokens associated with that address.

Merged Redundant Reports

The following report described the same root cause and the same required fix, so it was merged into this report and removed:

  • 0234-static unresolved requirement.md: stateless reset lookup uses the token without scoping the match to the datagram remote address.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions