fix(proto): floor negative echo lengths
This commit is contained in:
12 files changed
+107
-95
No files matched your search
@@ -66,7 +66,7 @@ pub fn encode_echo_reply(
|
||||
hmac_key: Option<&[u8]>,
|
||||
) -> Result<Vec<u8>> {
|
||||
let layout = PacketLayout::echo(hmac_key.is_some(), params);
|
||||
let len = echo_packet_len(hmac_key.is_some(), params)?;
|
||||
let len = echo_packet_len(hmac_key.is_some(), params);
|
||||
let payload_offset = layout.header_len();
|
||||
let available_payload_len = len.saturating_sub(payload_offset);
|
||||
if reply.payload.len() > available_payload_len {
|
||||
@@ -186,7 +186,7 @@ fn validate_echo_length(packet: &[u8], params: &Params, layout: PacketLayout) ->
|
||||
actual: packet.len(),
|
||||
});
|
||||
}
|
||||
let expected_len = echo_packet_len(layout.hmac, params)?;
|
||||
let expected_len = echo_packet_len(layout.hmac, params);
|
||||
if packet.len() == expected_len {
|
||||
return Ok(false);
|
||||
}
|
||||
@@ -396,7 +396,7 @@ mod tests {
|
||||
};
|
||||
params.received_stats = ReceivedStats::Both;
|
||||
let layout = PacketLayout::echo(true, ¶ms);
|
||||
let mut packet = Vec::with_capacity(echo_packet_len(true, ¶ms).unwrap());
|
||||
let mut packet = Vec::with_capacity(echo_packet_len(true, ¶ms));
|
||||
write_header(&mut packet, FLAG_REPLY | FLAG_HMAC);
|
||||
packet.extend_from_slice(&[0; HMAC_SIZE]);
|
||||
packet.extend_from_slice(&0x7896_b6ab_8771_5213u64.to_le_bytes());
|
||||
|
||||
@@ -30,8 +30,6 @@ pub enum ProtoError {
|
||||
InvalidUtf8,
|
||||
#[error("invalid enum value {value} for {name}")]
|
||||
InvalidEnum { name: &'static str, value: i64 },
|
||||
#[error("packet length must be non-negative, got {length}")]
|
||||
NegativePacketLength { length: i64 },
|
||||
#[error("parameter {tag} length is too large: {length} bytes")]
|
||||
ParameterLengthTooLarge { tag: u64, length: u64 },
|
||||
#[error("open reply has zero token without close flag")]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
params::{Params, StampAt},
|
||||
ProtoError, Result, HEADER_SIZE, HMAC_SIZE, RECV_COUNT_SIZE, RECV_WINDOW_SIZE, SEQ_SIZE,
|
||||
TIMESTAMP_SIZE, TOKEN_SIZE,
|
||||
HEADER_SIZE, HMAC_SIZE, RECV_COUNT_SIZE, RECV_WINDOW_SIZE, SEQ_SIZE, TIMESTAMP_SIZE,
|
||||
TOKEN_SIZE,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -111,13 +111,23 @@ pub fn echo_header_len(hmac: bool, params: &Params) -> usize {
|
||||
PacketLayout::echo(hmac, params).header_len()
|
||||
}
|
||||
|
||||
pub fn echo_packet_len(hmac: bool, params: &Params) -> Result<usize> {
|
||||
let header_len = echo_header_len(hmac, params);
|
||||
let requested =
|
||||
usize::try_from(params.length).map_err(|_| ProtoError::NegativePacketLength {
|
||||
length: params.length,
|
||||
})?;
|
||||
Ok(header_len.max(requested))
|
||||
/// The datagram length of an ECHO request or reply under `params`.
|
||||
///
|
||||
/// The negotiated length is a floor a peer asks for, never a ceiling: a packet
|
||||
/// can never be shorter than the field block its negotiated layout requires, so
|
||||
/// the result is `max(header, requested)`.
|
||||
///
|
||||
/// A negative negotiated length is a legitimate input rather than an encoder
|
||||
/// error. It is accepted during open and echoed back unchanged, so a session
|
||||
/// can genuinely carry one, and there is no datagram shorter than none — it
|
||||
/// therefore requests no space beyond the mandatory field block, exactly as
|
||||
/// zero does.
|
||||
pub fn echo_packet_len(hmac: bool, params: &Params) -> usize {
|
||||
// Saturating rather than wrapping: on a target whose `usize` is narrower
|
||||
// than the requested length, the largest representable packet is the
|
||||
// closest honest answer, and the encoder's own limits reject it from there.
|
||||
let requested = usize::try_from(params.length.max(0)).unwrap_or(usize::MAX);
|
||||
echo_header_len(hmac, params).max(requested)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -278,32 +288,18 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negotiated_length_never_smaller_than_header() {
|
||||
fn negotiated_length_is_floored_at_the_required_field_block() {
|
||||
// The 60-byte header of this layout is the floor. A negative negotiated
|
||||
// length is not an error: it asks for nothing beyond the mandatory
|
||||
// fields, exactly as zero does.
|
||||
let mut p = params(ReceivedStats::Both, StampAt::Both, Clock::Both);
|
||||
p.length = 20;
|
||||
assert_eq!(echo_packet_len(false, &p).unwrap(), 60);
|
||||
p.length = 92;
|
||||
assert_eq!(echo_packet_len(false, &p).unwrap(), 92);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checked_packet_len_rejects_negative_requested_length() {
|
||||
let mut p = params(ReceivedStats::Both, StampAt::Both, Clock::Both);
|
||||
p.length = -1;
|
||||
|
||||
assert_eq!(
|
||||
echo_packet_len(false, &p),
|
||||
Err(ProtoError::NegativePacketLength { length: -1 })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checked_packet_len_preserves_non_negative_header_floor() {
|
||||
let mut p = params(ReceivedStats::Both, StampAt::Both, Clock::Both);
|
||||
p.length = 0;
|
||||
assert_eq!(echo_packet_len(false, &p), Ok(60));
|
||||
|
||||
p.length = 92;
|
||||
assert_eq!(echo_packet_len(false, &p), Ok(92));
|
||||
for (length, expected) in [(-4096, 60), (-1, 60), (0, 60), (20, 60), (92, 92)] {
|
||||
p.length = length;
|
||||
assert_eq!(
|
||||
echo_packet_len(false, &p),
|
||||
expected,
|
||||
"unexpected packet length for negotiated length {length}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,8 +84,8 @@ pub enum DecodedRequestKind<'a> {
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ProtoError::PayloadTooLarge`] when an ECHO payload does not fit
|
||||
/// the negotiated length, and [`ProtoError::NegativePacketLength`] when the
|
||||
/// negotiated length is negative.
|
||||
/// the negotiated length. A negative negotiated length is not an error: see
|
||||
/// [`echo_packet_len`], which floors it at the mandatory field block.
|
||||
pub fn encode_request(request: RequestToEncode<'_>, hmac_key: Option<&[u8]>) -> Result<Vec<u8>> {
|
||||
match request {
|
||||
RequestToEncode::Open { params, no_test } => encode_open(params, no_test, hmac_key),
|
||||
@@ -205,7 +205,7 @@ fn encode_echo(
|
||||
hmac_key: Option<&[u8]>,
|
||||
) -> Result<Vec<u8>> {
|
||||
let layout = PacketLayout::echo(hmac_key.is_some(), params);
|
||||
let len = echo_packet_len(hmac_key.is_some(), params)?;
|
||||
let len = echo_packet_len(hmac_key.is_some(), params);
|
||||
let payload_offset = layout.header_len();
|
||||
let available_payload_len = len.saturating_sub(payload_offset);
|
||||
if payload.len() > available_payload_len {
|
||||
|
||||
@@ -305,6 +305,32 @@ fn echo_payload_is_copied_zero_filled_and_bounded_in_both_directions() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_negative_negotiated_length_still_encodes_in_both_directions() {
|
||||
// A server accepts a negative negotiated length during open and returns it
|
||||
// unchanged, so a live session can hold one and must stay executable. There
|
||||
// is no datagram shorter than none, so both directions fall back to the
|
||||
// mandatory field block — header (4) + token (8) + sequence (4) — exactly
|
||||
// as a zero length would.
|
||||
let params = Params {
|
||||
length: -4096,
|
||||
..Params::default()
|
||||
};
|
||||
assert_eq!(echo_packet_len(false, ¶ms), 16);
|
||||
|
||||
let request = encode_request(echo_request(¶ms, &[]), None)
|
||||
.expect("a negative negotiated length must still encode a request");
|
||||
assert_eq!(request.len(), 16);
|
||||
|
||||
let reply = encode_echo_reply(&echo_reply(¶ms, FLAG_REPLY), ¶ms, None)
|
||||
.expect("a negative negotiated length must still encode a reply");
|
||||
assert_eq!(reply.len(), 16);
|
||||
assert_eq!(
|
||||
decode_echo_reply(&reply, ¶ms, None).unwrap().token,
|
||||
TOKEN
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- Upstream midpoint ECHO-reply compatibility ----------
|
||||
//
|
||||
// Verified upstream irtt 0.9.1 behavior: for single-clock
|
||||
@@ -346,7 +372,7 @@ fn midpoint_params(clock: Clock, payload_space: usize) -> Params {
|
||||
|
||||
fn midpoint_compat_len(params: &Params, hmac: bool) -> usize {
|
||||
let compat_header = PacketLayout::echo(hmac, params).header_len() + 8;
|
||||
compat_header.max(echo_packet_len(hmac, params).unwrap())
|
||||
compat_header.max(echo_packet_len(hmac, params))
|
||||
}
|
||||
|
||||
/// Hand-builds upstream's dual-midpoint ECHO reply: both midpoint fields on
|
||||
@@ -399,7 +425,7 @@ fn midpoint_dual_field_reply_is_accepted_wherever_length_identifies_it() {
|
||||
for (payload_space, expected_extra) in [(0, 8), (1, 7), (4, 4), (7, 1)] {
|
||||
for clock in [Clock::Wall, Clock::Monotonic] {
|
||||
let params = midpoint_params(clock, payload_space);
|
||||
let normal_len = echo_packet_len(false, ¶ms).unwrap();
|
||||
let normal_len = echo_packet_len(false, ¶ms);
|
||||
let packet = build_midpoint_dual_field_reply(¶ms, FLAG_REPLY, 3, (111, 222), None);
|
||||
assert_eq!(
|
||||
packet.len(),
|
||||
@@ -438,7 +464,7 @@ fn midpoint_dual_field_reply_of_equal_length_is_accepted_but_not_correctable() {
|
||||
for payload_space in [8, 64] {
|
||||
for clock in [Clock::Wall, Clock::Monotonic] {
|
||||
let params = midpoint_params(clock, payload_space);
|
||||
let normal_len = echo_packet_len(false, ¶ms).unwrap();
|
||||
let normal_len = echo_packet_len(false, ¶ms);
|
||||
let packet = build_midpoint_dual_field_reply(¶ms, FLAG_REPLY, 4, (111, 222), None);
|
||||
assert_eq!(
|
||||
packet.len(),
|
||||
@@ -491,7 +517,7 @@ fn midpoint_dual_field_reply_authenticates_over_full_packet() {
|
||||
fn only_the_exact_compat_length_is_accepted_as_an_extension() {
|
||||
let params = midpoint_params(Clock::Wall, 0);
|
||||
let packet = build_midpoint_dual_field_reply(¶ms, FLAG_REPLY, 1, (10, 20), None);
|
||||
let expected_len = echo_packet_len(false, ¶ms).unwrap();
|
||||
let expected_len = echo_packet_len(false, ¶ms);
|
||||
assert_eq!(packet.len(), expected_len + 8);
|
||||
|
||||
// Every other length around the compatibility size stays rejected,
|
||||
|
||||
@@ -450,21 +450,34 @@ fn encoded_requests_decode_back_to_their_sender_side_identity() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_encoding_rejects_a_negative_negotiated_length() {
|
||||
fn echo_encoding_floors_a_negative_negotiated_length_at_the_field_block() {
|
||||
// A negative negotiated length is accepted during open and echoed back
|
||||
// unchanged, so a real session can carry one. It must therefore stay
|
||||
// encodable: it asks for no space beyond the mandatory fields.
|
||||
let params = Params {
|
||||
length: -1,
|
||||
..Params::default()
|
||||
};
|
||||
let packet = encode_request(
|
||||
RequestToEncode::Echo {
|
||||
token: TOKEN,
|
||||
sequence: SEQUENCE,
|
||||
params: ¶ms,
|
||||
payload: &[],
|
||||
},
|
||||
None,
|
||||
)
|
||||
.expect("a negative negotiated length must still encode");
|
||||
|
||||
// header (4) + token (8) + sequence (4).
|
||||
assert_eq!(packet.len(), 16);
|
||||
let request = decode_request(&packet).unwrap();
|
||||
assert_eq!(
|
||||
encode_request(
|
||||
RequestToEncode::Echo {
|
||||
token: TOKEN,
|
||||
sequence: SEQUENCE,
|
||||
params: ¶ms,
|
||||
payload: &[],
|
||||
},
|
||||
None,
|
||||
),
|
||||
Err(ProtoError::NegativePacketLength { length: -1 })
|
||||
request.kind,
|
||||
DecodedRequestKind::Echo {
|
||||
token: TOKEN,
|
||||
sequence: SEQUENCE,
|
||||
tail: &[],
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user