use irtt_proto::{Clock, Params, ReceivedStats, ServerFill, StampAt, TimestampFields}; use super::support::{ client_params, core_with_tokens, echo_request, expect_echo_reply, expect_normal_open_reply, open_request, open_request_with_raw_params, param_int, peer, unthrottled, ScriptedTokens, KEY, }; use crate::{ServerConfig, TimestampAllowance, DEFAULT_MAX_PACKET_LENGTH}; const TOKEN_A: u64 = 0x7896_b6ab_8771_5213; #[test] fn an_ordinary_open_is_answered_and_creates_one_session() { let mut core = core_with_tokens(ServerConfig::default(), ScriptedTokens::new([TOKEN_A])); let packet = core .handle_datagram(peer(), &open_request(&client_params(), None)) .unwrap() .expect("an ordinary open must be answered"); let reply = expect_normal_open_reply(&packet, None); assert_eq!(reply.token, TOKEN_A); assert_eq!(core.session_count(), 1); let session = core.session(TOKEN_A).expect("the session must be live"); assert_eq!(session.params(), &reply.params); } #[test] fn a_session_records_the_exact_source_endpoint() { // Address family, address, port and — where present — the IPv6 scope all // form part of session identity, so the endpoint is stored verbatim rather // than normalized to an address. for endpoint in [ "198.51.100.7:41234", "198.51.100.7:41235", "[2001:db8::1]:2112", "[fe80::1%3]:2112", ] { let endpoint = endpoint.parse().unwrap(); let mut core = core_with_tokens(ServerConfig::default(), ScriptedTokens::new([TOKEN_A])); core.handle_datagram(endpoint, &open_request(&client_params(), None)) .unwrap() .expect("open must be answered"); assert_eq!(core.session(TOKEN_A).unwrap().peer(), endpoint); } } #[test] fn any_requested_protocol_version_is_accepted_and_answered_with_one() { // Version mismatch is detected client-side only: the server rewrites the // version and creates a normal session whatever was asked for. for requested in [Some(1), Some(0), Some(2), Some(-1), None] { let mut core = core_with_tokens(ServerConfig::default(), ScriptedTokens::new([TOKEN_A])); let params = Params { protocol_version: requested.unwrap_or(0), ..client_params() }; let request = match requested { // An explicit zero cannot be encoded by `Params::encode`, which // omits zero-valued integers, so encode that tag by hand. Some(0) => open_request_with_raw_params(¶m_int(1, 0), None), Some(_) => open_request(¶ms, None), None => open_request( &Params { protocol_version: 0, ..params }, None, ), }; let packet = core .handle_datagram(peer(), &request) .unwrap() .unwrap_or_else(|| panic!("protocol version {requested:?} must be answered")); let reply = expect_normal_open_reply(&packet, None); assert_eq!(reply.params.protocol_version, 1); assert_eq!(core.session_count(), 1); } } #[test] fn an_empty_parameter_payload_is_answered_with_only_a_version() { // The interval floor is disabled here so that the reply carries nothing but // the version: with one configured, an absent Interval negotiates up to it // and is encoded, which is interval policy rather than what this test is // about. let mut core = core_with_tokens(unthrottled(), ScriptedTokens::new([TOKEN_A])); let packet = core .handle_datagram(peer(), &open_request_with_raw_params(&[], None)) .unwrap() .expect("an empty parameter payload is a valid open"); // header (4) + token (8) + ProtocolVersion=1 (2). Every other parameter // negotiates to its wire-default zero and is omitted entirely, which is // only reachable if absent parameters really did decode as zero. assert_eq!(packet.bytes().len(), 14); let reply = expect_normal_open_reply(&packet, None); assert_eq!( reply.params, Params { protocol_version: 1, ..Params::default() } ); // An unspecified clock is not itself a defect. Only pairing it with a // selected `stamp_at` is, so a session requesting no timestamps at all is // fully valid with no Clock tag. assert_eq!(reply.params.stamp_at, StampAt::None); assert_eq!(reply.params.clock, Clock::Unspecified); assert_eq!(core.session_count(), 1); } #[test] fn unknown_parameter_tags_are_ignored_and_not_reflected_in_the_reply() { let mut core = core_with_tokens(unthrottled(), ScriptedTokens::new([TOKEN_A])); let mut payload = param_int(1, 1); payload.extend_from_slice(¶m_int(42, 7)); payload.extend_from_slice(¶m_int(200, 9)); let packet = core .handle_datagram(peer(), &open_request_with_raw_params(&payload, None)) .unwrap() .expect("unknown tags must not prevent an open"); let reply = expect_normal_open_reply(&packet, None); assert_eq!( reply.params, Params { protocol_version: 1, ..Params::default() } ); // The reply is the same 14 bytes an empty payload produces: no unknown tag // was copied through. assert_eq!(packet.bytes().len(), 14); assert_eq!(core.session_count(), 1); } #[test] fn selecting_timestamps_without_a_clock_is_silently_refused() { // Timestamps from no clock at all describe a session the server could not // run: the negotiated layout carries no timestamp field to fill. The open // is discarded rather than repaired — synthesizing a clock would answer // with a session the client never asked for, and dropping `stamp_at` would // silently discard the measurement it did ask for. // // Only an *absent* Clock tag reaches this: an explicit zero is already out // of range for the decoder, and a conforming client always sends a clock // when it selects timestamps. // // A server whose timestamp allowance is none accepts the very same request, // because then no timestamp is negotiated to be unproducible; see // `a_timestamp_disallowing_server_accepts_a_request_that_omitted_its_clock`. for stamp_at in [ StampAt::Send, StampAt::Receive, StampAt::Both, StampAt::Midpoint, ] { let requested = Params { clock: Clock::Unspecified, stamp_at, ..client_params() }; let tokens = ScriptedTokens::new([TOKEN_A]); let mut core = core_with_tokens(ServerConfig::default(), tokens.clone()); let reply = core .handle_datagram(peer(), &open_request(&requested, None)) .expect("an unusable timestamp request is not a server error"); assert!( reply.is_none(), "{stamp_at:?} without a clock must be silent" ); assert_eq!( core.session_count(), 0, "{stamp_at:?} without a clock must create no session" ); assert_eq!( tokens.remaining(), 1, "{stamp_at:?} without a clock must not consume a token" ); } } #[test] fn a_timestamp_disallowing_server_accepts_a_request_that_omitted_its_clock() { // The same request the test above refuses, against a server that provides no // timestamps at all. The restriction runs before the effective-session // check, so `stamp_at` is already none by the time executability is judged: // the absent clock then selects nothing that was going to be reported, and // the session is one the server can run exactly as negotiated. // // This is what the ordering exists for. Validating the *request* instead // would refuse an open that this policy makes perfectly serviceable. let requested = Params { clock: Clock::Unspecified, stamp_at: StampAt::Both, ..client_params() }; let mut core = core_with_tokens( unthrottled().with_timestamp_allowance(TimestampAllowance::None), ScriptedTokens::new([TOKEN_A]), ); let packet = core .handle_datagram(peer(), &open_request(&requested, None)) .unwrap() .expect("an omitted clock is safe once no timestamps are negotiated"); let reply = expect_normal_open_reply(&packet, None); assert_eq!(reply.params.stamp_at, StampAt::None); assert_eq!( reply.params.clock, Clock::Unspecified, "the absent clock tag stays absent" ); assert_eq!(core.session_count(), 1); let echo = core .handle_datagram( peer(), &echo_request(reply.token, 0, &reply.params, &[], None), ) .unwrap() .expect("the session must serve echoes"); assert_eq!( expect_echo_reply(&echo, &reply.params, None).timestamps, TimestampFields::default(), "no timestamps were negotiated, so the reply carries none" ); } #[test] fn current_non_length_parameters_survive_negotiation_unchanged() { // Statistics, timestamps and DSCP are what the capability restrictions // change when they are configured; this is the default configuration, which // restricts none of them. Version, an oversized packet // length, the duration maximum and the interval floor and cap are already // restricted and have their own tests; the length here is deliberately // under the default maximum, and the configuration below leaves duration // and interval unrestricted, so this test says nothing about either policy. // // The server fill below is a *valid* descriptor, which is what survives // unchanged. Fill negotiation has its own restriction rule and its own // tests in `fill`. // // This is about what *negotiation* rewrites, which is a separate question // from whether the core will acknowledge the result: an open whose // effective parameters are incoherent is still discarded, as // `selecting_timestamps_without_a_clock_is_silently_refused` shows. let unrestricted = Params { protocol_version: 2, duration_ns: 3_600_000_000_000, interval_ns: 1, length: 1472, received_stats: ReceivedStats::Window, stamp_at: StampAt::Midpoint, clock: Clock::Monotonic, dscp: 184, server_fill: Some(ServerFill { value: "pattern:abcd".to_owned(), }), }; // No duration maximum is configured by default, and the interval floor is // disabled, so a one-nanosecond interval is left where it was asked for. let mut core = core_with_tokens(unthrottled(), ScriptedTokens::new([TOKEN_A])); let packet = core .handle_datagram(peer(), &open_request(&unrestricted, None)) .unwrap() .expect("open must be answered"); let reply = expect_normal_open_reply(&packet, None); assert_eq!( reply.params, Params { protocol_version: 1, ..unrestricted } ); } #[test] fn a_positive_length_above_the_configured_maximum_is_reduced_to_it() { // The reduced value is what the client is told and what the session holds, // because it is what the server will actually emit. Both have to agree: an // upstream client treats a reply shorter than the negotiated length as // fatal, so an honest reduction at open is the only serviceable answer. let tokens = ScriptedTokens::new([TOKEN_A]); let mut core = core_with_tokens( ServerConfig::default().with_max_packet_length(64), tokens.clone(), ); let requested = Params { length: 1000, ..client_params() }; let packet = core .handle_datagram(peer(), &open_request(&requested, None)) .unwrap() .expect("a reducible length must still be answered"); let reply = expect_normal_open_reply(&packet, None); assert_eq!(reply.params.length, 64); assert_eq!(core.session_count(), 1); assert_eq!(tokens.remaining(), 0, "one token was consumed"); let session = core.session(TOKEN_A).expect("the session must be live"); assert_eq!( session.params().length, 64, "the session must enforce the reduced length" ); } #[test] fn the_default_maximum_reduces_an_absurd_requested_length() { // The largest length the wire can carry is representable as a `usize` on a // 64-bit target, so packet sizing would happily have turned it into an // allocation; the configured maximum is what stops it. Restriction happens // during negotiation, before any sizing, so this holds on a 32-bit target // too — the request never reaches a conversion that could fail. let mut core = core_with_tokens(ServerConfig::default(), ScriptedTokens::new([TOKEN_A])); let requested = Params { length: i64::MAX, ..client_params() }; let packet = core .handle_datagram(peer(), &open_request(&requested, None)) .unwrap() .expect("an absurd length is reduced, not refused"); let reply = expect_normal_open_reply(&packet, None); assert_eq!(reply.params.length, DEFAULT_MAX_PACKET_LENGTH as i64); let session = core.session(TOKEN_A).expect("the session must be live"); assert_eq!(session.params().length, DEFAULT_MAX_PACKET_LENGTH as i64); } #[test] fn an_open_whose_echo_field_block_exceeds_the_maximum_is_silently_refused() { // Length itself satisfies the policy — zero asks for nothing beyond the // mandatory fields — but the received-statistics fields alone make the // smallest compliant echo reply larger than 20 bytes. Capping the Length // parameter is therefore not enough on its own: this session could never be // answered, so it is never acknowledged. let requested = Params { protocol_version: 1, length: 0, received_stats: ReceivedStats::Both, stamp_at: StampAt::None, clock: Clock::Unspecified, ..Params::default() }; let tokens = ScriptedTokens::new([TOKEN_A]); let mut core = core_with_tokens( ServerConfig::default().with_max_packet_length(20), tokens.clone(), ); assert_eq!( core.handle_datagram(peer(), &open_request(&requested, None)) .expect("an unservable packet size is not a server error"), None ); assert_eq!(core.session_count(), 0); assert_eq!(tokens.remaining(), 1, "no token may be drawn"); } #[test] fn authentication_counts_toward_the_executable_echo_size() { // An authentication field adds 16 bytes to the mandatory echo block, so a // maximum between the two floors admits the same minimal session on an // unauthenticated server and refuses it on an authenticated one. The sizes // themselves come from `irtt_proto`; what this pins is that the server asks // about its own authentication mode rather than assuming the smaller floor. let minimal = Params { protocol_version: 1, ..Params::default() }; let tokens = ScriptedTokens::new([TOKEN_A]); let mut authenticated = core_with_tokens( ServerConfig::default() .with_hmac_key(KEY) .with_max_packet_length(31), tokens.clone(), ); assert_eq!( authenticated .handle_datagram(peer(), &open_request(&minimal, Some(KEY))) .expect("an unservable packet size is not a server error"), None ); assert_eq!(authenticated.session_count(), 0); assert_eq!(tokens.remaining(), 1, "no token may be drawn"); let mut unauthenticated = core_with_tokens( ServerConfig::default().with_max_packet_length(31), ScriptedTokens::new([TOKEN_A]), ); let packet = unauthenticated .handle_datagram(peer(), &open_request(&minimal, None)) .unwrap() .expect("the same session fits without an authentication field"); expect_normal_open_reply(&packet, None); assert_eq!(unauthenticated.session_count(), 1); } #[test] fn negative_and_out_of_range_length_and_dscp_survive_negotiation() { // These lengths are all at or below the default maximum, and DSCP is // accepted as decoded, including values a socket could never carry: // negotiation restricts neither, and a value that is not a byte is simply // transported unmarked (see `traffic_class`). // // A negative length in particular stays negative in both the reply and the // stored session; the packet-length policy reduces oversized *positive* // lengths and touches nothing else. It does not need clamping here to be // executable: echo packet sizing in `irtt-proto` floors it at the required // field block, so the session this creates can be run as negotiated. for (length, dscp) in [(-1, -1), (0, 0), (1472, 184), (-4096, 512)] { let requested = Params { protocol_version: 1, length, dscp, ..Params::default() }; let mut core = core_with_tokens(ServerConfig::default(), ScriptedTokens::new([TOKEN_A])); let packet = core .handle_datagram(peer(), &open_request(&requested, None)) .unwrap() .unwrap_or_else(|| panic!("length {length} dscp {dscp} must be answered")); let reply = expect_normal_open_reply(&packet, None); assert_eq!(reply.params.length, length); assert_eq!(reply.params.dscp, dscp); assert_eq!(core.session_count(), 1); let session = core.session(TOKEN_A).expect("the session must be live"); assert_eq!( session.params(), &reply.params, "the session must hold the negotiated values verbatim" ); } }