fix(server): exclude IPv6 flow information from endpoint identity
Session identity is the address family, the IP address, the UDP source port and — for IPv6 — the scope identifier. `SocketAddrV6` equality additionally covers the flow label, so comparing endpoints with `==` admitted a field that identifies no endpoint: a client whose flow label changed mid-session could be refused its own close and strand the session, which nothing reclaims while expiry is unimplemented. It bought nothing in exchange, since the token, address, port and scope are what an off-path attacker would have to guess. Compare the specified components instead. Scope still counts, and the address is still stored verbatim, so a later reply goes back to exactly the endpoint the request came from. Not reachable through a default socket today: a receiver reports the flow label only when asked to, and there is no runtime slice yet. It is reachable through the public core, which takes caller-supplied addresses.
This commit is contained in:
2 files changed
+56
-1
No files matched your search
@@ -6,6 +6,8 @@
|
||||
//! authentication — is the same silence with the session left live, so these
|
||||
//! tests assert the table as well as the reply.
|
||||
|
||||
use std::net::{SocketAddr, SocketAddrV6};
|
||||
|
||||
use irtt_proto::{encode_request, RequestToEncode};
|
||||
|
||||
use super::support::{
|
||||
@@ -124,6 +126,36 @@ fn a_close_from_any_other_endpoint_leaves_the_session_live() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv6_flow_information_is_not_part_of_endpoint_identity() {
|
||||
// Identity is family, address, port and scope. The flow label is routing
|
||||
// metadata that may vary within one flow, so letting `SocketAddrV6`
|
||||
// equality decide would strand a session its own client could never close.
|
||||
let ip = "2001:db8::1".parse().unwrap();
|
||||
let unlabeled = SocketAddr::V6(SocketAddrV6::new(ip, 2112, 0, 0));
|
||||
let labeled = SocketAddr::V6(SocketAddrV6::new(ip, 2112, 0x000f_1234, 0));
|
||||
assert_ne!(
|
||||
unlabeled, labeled,
|
||||
"the two must differ only in flow information"
|
||||
);
|
||||
|
||||
for (bound, closing) in [(unlabeled, labeled), (labeled, unlabeled)] {
|
||||
let mut core = core_with_tokens(ServerConfig::default(), ScriptedTokens::new([TOKEN_A]));
|
||||
let token = open_session(&mut core, bound, None);
|
||||
|
||||
assert_eq!(
|
||||
core.handle_datagram(closing, &close_request(token, None))
|
||||
.unwrap(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
core.session_count(),
|
||||
0,
|
||||
"a close from {closing} must release the session opened from {bound}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unknown_or_zero_token_closes_nothing() {
|
||||
// Zero is reserved for no-test replies and is never issued to a session, so
|
||||
|
||||
Reference in new issue
Block a user