Implement reactive MtuExceeded error signal (0x22)

Add a new session-layer error signal that transit routers send back to
the source when a forwarded packet exceeds the next-hop transport MTU.
This complements the existing proactive path MTU discovery (min'd at
each hop) by providing immediate feedback when oversized packets are
dropped, closing the transient window before the proactive mechanism
converges.

Wire format: 36-byte payload (msg_type + flags + dest_addr + reporter +
mtu) with FSP phase=0x0 and U flag set, matching the existing
CoordsRequired/PathBroken pattern.

Changes:
- Add SessionMessageType::MtuExceeded (0x22) and MtuExceeded struct with
  encode/decode methods to protocol/session.rs
- Add NodeError::MtuExceeded variant to propagate structured MTU info
  from TransportError through send_encrypted_link_message()
- Catch MtuExceeded in the forwarding path and send error signal back to
  the datagram source via send_mtu_exceeded_error(), rate-limited by the
  existing routing_error_rate_limiter
- Handle incoming MtuExceeded at the source by calling
  PathMtuState::apply_notification() for immediate MTU decrease
- Add unit tests for encode/decode roundtrip, boundary MTU values, and
  too-short payload rejection
This commit is contained in:
Johnathan Corgan
2026-02-22 21:37:31 +00:00
parent 557a84c12b
commit 20cf6932cd
5 changed files with 281 additions and 18 deletions
+4 -4
View File
@@ -37,10 +37,10 @@ pub use tree::TreeAnnounce;
pub use filter::FilterAnnounce;
pub use discovery::{LookupRequest, LookupResponse};
pub use session::{
CoordsRequired, FspFlags, FspInnerFlags, PathBroken, PathMtuNotification, SessionAck,
SessionFlags, SessionMessageType, SessionReceiverReport, SessionSenderReport, SessionSetup,
COORDS_REQUIRED_SIZE, PATH_MTU_NOTIFICATION_SIZE, SESSION_RECEIVER_REPORT_SIZE,
SESSION_SENDER_REPORT_SIZE,
CoordsRequired, FspFlags, FspInnerFlags, MtuExceeded, PathBroken, PathMtuNotification,
SessionAck, SessionFlags, SessionMessageType, SessionReceiverReport, SessionSenderReport,
SessionSetup, COORDS_REQUIRED_SIZE, MTU_EXCEEDED_SIZE, PATH_MTU_NOTIFICATION_SIZE,
SESSION_RECEIVER_REPORT_SIZE, SESSION_SENDER_REPORT_SIZE,
};
pub(crate) use session::{coords_wire_size, decode_optional_coords, encode_coords};
+144
View File
@@ -42,6 +42,8 @@ pub enum SessionMessageType {
CoordsRequired = 0x20,
/// Routing failure — local minimum or unreachable (link-layer error signal).
PathBroken = 0x21,
/// MTU exceeded — forwarded packet too large for next-hop transport (link-layer error signal).
MtuExceeded = 0x22,
}
impl SessionMessageType {
@@ -57,6 +59,7 @@ impl SessionMessageType {
0x14 => Some(SessionMessageType::CoordsWarmup),
0x20 => Some(SessionMessageType::CoordsRequired),
0x21 => Some(SessionMessageType::PathBroken),
0x22 => Some(SessionMessageType::MtuExceeded),
_ => None,
}
}
@@ -79,6 +82,7 @@ impl fmt::Display for SessionMessageType {
SessionMessageType::CoordsWarmup => "CoordsWarmup",
SessionMessageType::CoordsRequired => "CoordsRequired",
SessionMessageType::PathBroken => "PathBroken",
SessionMessageType::MtuExceeded => "MtuExceeded",
};
write!(f, "{}", name)
}
@@ -952,6 +956,86 @@ impl PathBroken {
}
}
/// Error indicating a forwarded packet exceeded the next-hop transport MTU.
///
/// Generated by a transit router when `send_encrypted_link_message()`
/// fails with `TransportError::MtuExceeded`. The reporter includes the
/// bottleneck MTU so the source can immediately reduce its sending MTU.
///
/// ## Wire Format
///
/// | Offset | Field | Size | Description |
/// |--------|-----------|----------|------------------------------------|
/// | 0 | msg_type | 1 byte | 0x22 |
/// | 1 | flags | 1 byte | Reserved |
/// | 2 | dest_addr | 16 bytes | The destination we were forwarding |
/// | 18 | reporter | 16 bytes | NodeAddr of reporting router |
/// | 34 | mtu | 2 bytes | Bottleneck MTU (u16 LE) |
///
/// Payload: 36 bytes
#[derive(Clone, Debug)]
pub struct MtuExceeded {
/// Destination that the oversized packet was heading to.
pub dest_addr: NodeAddr,
/// Router that detected the MTU violation.
pub reporter: NodeAddr,
/// Transport MTU at the bottleneck hop.
pub mtu: u16,
}
/// Wire size of MtuExceeded payload: msg_type(1) + flags(1) + dest_addr(16) + reporter(16) + mtu(2).
pub const MTU_EXCEEDED_SIZE: usize = 36;
impl MtuExceeded {
/// Create a new MtuExceeded error.
pub fn new(dest_addr: NodeAddr, reporter: NodeAddr, mtu: u16) -> Self {
Self { dest_addr, reporter, mtu }
}
/// Encode as wire format (4-byte FSP prefix + msg_type + body).
///
/// Error signals use phase=0x0 with U flag set.
pub fn encode(&self) -> Vec<u8> {
let body_len = MTU_EXCEEDED_SIZE; // 36 bytes
let mut buf = Vec::with_capacity(4 + body_len);
// FSP prefix: version 0, phase 0x0, U flag set
buf.push(0x00); // version 0, phase 0x0
buf.push(0x04); // U flag
let payload_len = body_len as u16;
buf.extend_from_slice(&payload_len.to_le_bytes());
// msg_type byte
buf.push(SessionMessageType::MtuExceeded.to_byte());
buf.push(0x00); // reserved flags
buf.extend_from_slice(self.dest_addr.as_bytes());
buf.extend_from_slice(self.reporter.as_bytes());
buf.extend_from_slice(&self.mtu.to_le_bytes());
buf
}
/// Decode from wire format (after FSP prefix and msg_type byte consumed).
pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
// flags(1) + dest_addr(16) + reporter(16) + mtu(2) = 35
if payload.len() < 35 {
return Err(ProtocolError::MessageTooShort {
expected: 35,
got: payload.len(),
});
}
// payload[0] is flags (reserved, ignored)
let mut dest_bytes = [0u8; 16];
dest_bytes.copy_from_slice(&payload[1..17]);
let mut reporter_bytes = [0u8; 16];
reporter_bytes.copy_from_slice(&payload[17..33]);
let mtu = u16::from_le_bytes([payload[33], payload[34]]);
Ok(Self {
dest_addr: NodeAddr::from_bytes(dest_bytes),
reporter: NodeAddr::from_bytes(reporter_bytes),
mtu,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -979,6 +1063,7 @@ mod tests {
SessionMessageType::PathMtuNotification,
SessionMessageType::CoordsRequired,
SessionMessageType::PathBroken,
SessionMessageType::MtuExceeded,
];
for ty in types {
@@ -1405,4 +1490,63 @@ mod tests {
assert_eq!(decoded.path_mtu, mtu);
}
}
// ===== MtuExceeded Tests =====
#[test]
fn test_mtu_exceeded_encode_size() {
let err = MtuExceeded::new(make_node_addr(0xAA), make_node_addr(0xBB), 1400);
let encoded = err.encode();
// 4 prefix + 36 body = 40
assert_eq!(encoded.len(), 4 + MTU_EXCEEDED_SIZE);
}
#[test]
fn test_mtu_exceeded_encode_decode() {
let err = MtuExceeded::new(make_node_addr(0xAA), make_node_addr(0xBB), 1400);
let encoded = err.encode();
// Check FSP prefix: phase 0x0, U flag
assert_eq!(encoded[0], 0x00);
assert_eq!(encoded[1], 0x04); // U flag
// msg_type after prefix
assert_eq!(encoded[4], 0x22);
// decode after prefix + msg_type consumed
let decoded = MtuExceeded::decode(&encoded[5..]).unwrap();
assert_eq!(decoded.dest_addr, err.dest_addr);
assert_eq!(decoded.reporter, err.reporter);
assert_eq!(decoded.mtu, 1400);
}
#[test]
fn test_mtu_exceeded_decode_too_short() {
assert!(MtuExceeded::decode(&[]).is_err());
assert!(MtuExceeded::decode(&[0x00; 20]).is_err());
assert!(MtuExceeded::decode(&[0x00; 34]).is_err()); // exactly 1 byte short
}
#[test]
fn test_mtu_exceeded_boundary_mtu_values() {
for mtu in [0u16, 1280, 1500, u16::MAX] {
let err = MtuExceeded::new(make_node_addr(1), make_node_addr(2), mtu);
let encoded = err.encode();
let decoded = MtuExceeded::decode(&encoded[5..]).unwrap();
assert_eq!(decoded.mtu, mtu);
}
}
#[test]
fn test_mtu_exceeded_message_type_value() {
assert_eq!(SessionMessageType::MtuExceeded.to_byte(), 0x22);
assert_eq!(
SessionMessageType::from_byte(0x22),
Some(SessionMessageType::MtuExceeded)
);
}
#[test]
fn test_mtu_exceeded_display() {
assert_eq!(format!("{}", SessionMessageType::MtuExceeded), "MtuExceeded");
}
}