docs(server): record IPv6 zone identity as project policy

Endpoint identity documented the IPv6 scope identifier as an established
component alongside family, address and port. The server specification does
list the zone, but its remaining-unknowns section records that multi-zone
behavior could not be tested on a single-host platform, so no evidence settles
how a reference server compares scoped addresses. Presenting the choice as
verified compatibility overstated what is known, which the crate's own guidance
forbids.

Say what it is instead: a deliberate policy taken under a recorded unknown. Two
peers reachable at the same link-local address in different zones are genuinely
different peers, so treating the zone as identity is the conservative reading —
the failure it prevents is one peer closing another's session, and the failure
it risks is a scoped client having to reopen. The excluded flow label is
labelled the same way, and `irtt-server` guidance now carries the rule so the
echo slice reuses this comparison rather than inventing a second one.

Behavior is unchanged; this is documentation and a test comment.
This commit is contained in:
Nils Andreas Svee committed 2026-08-12 10:48:42 +07:00
1 parent e79ea94318
commit 6c34fd3d20
4 files changed
+48 -15

No files matched your search

+8
View File
@@ -25,6 +25,14 @@ material does not establish a semantic, state the ambiguity instead of guessing.
- No statistics, CLI or presentation concerns. - No statistics, CLI or presentation concerns.
- `SocketAddr` belongs here: endpoint identity is session state. It does not - `SocketAddr` belongs here: endpoint identity is session state. It does not
belong in `irtt-proto`. belong in `irtt-proto`.
- Endpoint identity is compared field by field, not with `SocketAddr` equality.
Family, address and port are established components. The IPv6 flow label is
excluded because it identifies no endpoint; the IPv6 zone is included as a
deliberate project policy, because the specification records multi-zone
behavior as untested and two peers sharing a link-local address in different
zones are genuinely different peers. Reuse that one comparison rather than
writing a second identity rule, and keep both choices labelled as policy
rather than as verified compatibility.
## Core versus runtime ## Core versus runtime
+24 -8
View File
@@ -245,14 +245,30 @@ impl ServerCore {
/// Whether two source endpoints identify the same session endpoint. /// Whether two source endpoints identify the same session endpoint.
/// ///
/// Identity is the address family, the IP address, the UDP source port and — /// The address family, the IP address and the UDP source port are established
/// for IPv6 — the scope identifier. That is the whole list, so this is not /// identity components: a request from a different port, address or family is
/// simply `==`: [`SocketAddrV6`] equality also covers the flow label, which /// dropped and leaves its session live.
/// identifies no endpoint. It is routing metadata that a sender or a forwarding ///
/// path may legitimately vary within one flow, and a receiver reports it only /// The remaining two fields of [`SocketAddrV6`] are decided here rather than
/// when asked to, so admitting it into identity could strand a session that its /// observed, which is why this is not simply `==`.
/// own client can then never close. It grants nothing in exchange: the token, ///
/// address, port and scope are what an off-path attacker would have to guess. /// The flow label is excluded. It identifies no endpoint — it is routing
/// metadata a sender or forwarding path may legitimately vary within one flow,
/// and a receiver reports it only when asked to — so admitting it could strand
/// a session its own client can then never close. It grants nothing in
/// exchange: the token, address, port and scope are what an off-path attacker
/// would have to guess.
///
/// The scope identifier is included, and that is **`irtt-rs` policy, not
/// verified compatibility behavior**. The server specification lists the IPv6
/// zone as an identity component, but its remaining-unknowns section records
/// that multi-zone behavior could not be tested on a single-host platform, so
/// no evidence settles how a reference server compares scoped addresses. Two
/// peers reachable at the same link-local address in different zones are
/// genuinely different peers, so treating the zone as identity is the
/// conservative reading: the failure it prevents is one peer closing another's
/// session, while the failure it risks is a scoped client having to reopen.
/// Revisit if evidence ever settles the question.
/// ///
/// [`SocketAddrV6`]: std::net::SocketAddrV6 /// [`SocketAddrV6`]: std::net::SocketAddrV6
fn same_endpoint(session: SocketAddr, peer: SocketAddr) -> bool { fn same_endpoint(session: SocketAddr, peer: SocketAddr) -> bool {
+11 -5
View File
@@ -27,11 +27,17 @@ impl Session {
/// The exact source endpoint the open request arrived from. /// The exact source endpoint the open request arrived from.
/// ///
/// A session is bound to this endpoint, not merely to its address: address /// A session is bound to this endpoint, not merely to its address: the
/// family, address, UDP source port and — for IPv6 — the scope identifier /// address family, the address and the UDP source port all form part of its
/// all form part of its identity. Close processing requires both a token /// identity, as — by `irtt-rs` policy rather than observed behavior — does
/// match and an exact endpoint match before removing a session, and echo /// the IPv6 scope identifier. Close processing requires both a token match
/// processing will require the same. /// and an endpoint match before removing a session, and echo processing
/// will require the same; `same_endpoint` in `core` is where the comparison
/// and its reasoning live.
///
/// The address is stored exactly as received, including the flow label that
/// identity ignores, so a reply goes back to the endpoint the request
/// actually came from.
pub(crate) fn peer(&self) -> SocketAddr { pub(crate) fn peer(&self) -> SocketAddr {
self.peer self.peer
} }
+5 -2
View File
@@ -91,8 +91,11 @@ fn an_echo_after_a_close_is_a_stale_token() {
#[test] #[test]
fn a_close_from_any_other_endpoint_leaves_the_session_live() { fn a_close_from_any_other_endpoint_leaves_the_session_live() {
// Endpoint identity is exact: family, address, UDP source port and IPv6 // Possessing the token is not authority to close. The first four pairs
// scope all count. Possessing the token is not authority to close. // differ in an established identity component; the last differs only in
// IPv6 zone, which is `irtt-rs` policy rather than observed behavior — see
// `same_endpoint`. It is pinned so the choice cannot drift silently, not
// because evidence settled it.
for (bound, foreign) in [ for (bound, foreign) in [
("198.51.100.7:41234", "198.51.100.7:41235"), ("198.51.100.7:41234", "198.51.100.7:41235"),
("198.51.100.7:41234", "198.51.100.9:41234"), ("198.51.100.7:41234", "198.51.100.9:41234"),