mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-22 07:48:26 +00:00
Add MTU fields to lookup packets for path MTU discovery
Add min_mtu (u16) to LookupRequest and path_mtu (u16) to LookupResponse, enabling the discovery system to report transport MTU capability along the lookup path. LookupRequest carries min_mtu (origin's minimum MTU requirement, default 0 = no requirement). LookupResponse carries path_mtu (initialized to u16::MAX by the target, reduced by transit nodes via min(path_mtu, outgoing_link_mtu) on the reverse path). path_mtu is a transit annotation like SessionDatagram.path_mtu and is NOT included in the proof signature. The originator stores the discovered path_mtu in CacheEntry alongside cached coordinates. Wire format: +2 bytes each for LookupRequest and LookupResponse.
This commit is contained in:
26
src/cache/coord_cache.rs
vendored
26
src/cache/coord_cache.rs
vendored
@@ -79,6 +79,32 @@ impl CoordCache {
|
||||
self.entries.insert(addr, entry);
|
||||
}
|
||||
|
||||
/// Insert or update a cache entry with path MTU information.
|
||||
///
|
||||
/// Used by discovery response handling to store the discovered path MTU
|
||||
/// alongside the target's coordinates.
|
||||
pub fn insert_with_path_mtu(
|
||||
&mut self,
|
||||
addr: NodeAddr,
|
||||
coords: TreeCoordinate,
|
||||
current_time_ms: u64,
|
||||
path_mtu: u16,
|
||||
) {
|
||||
if let Some(entry) = self.entries.get_mut(&addr) {
|
||||
entry.update(coords, current_time_ms, self.default_ttl_ms);
|
||||
entry.set_path_mtu(path_mtu);
|
||||
return;
|
||||
}
|
||||
|
||||
if self.entries.len() >= self.max_entries {
|
||||
self.evict_one(current_time_ms);
|
||||
}
|
||||
|
||||
let mut entry = CacheEntry::new(coords, current_time_ms, self.default_ttl_ms);
|
||||
entry.set_path_mtu(path_mtu);
|
||||
self.entries.insert(addr, entry);
|
||||
}
|
||||
|
||||
/// Insert with a custom TTL.
|
||||
pub fn insert_with_ttl(
|
||||
&mut self,
|
||||
|
||||
17
src/cache/entry.rs
vendored
17
src/cache/entry.rs
vendored
@@ -13,6 +13,12 @@ pub struct CacheEntry {
|
||||
last_used: u64,
|
||||
/// When this entry expires (Unix milliseconds).
|
||||
expires_at: u64,
|
||||
/// Path MTU discovered during lookup (if available).
|
||||
///
|
||||
/// Set from the `LookupResponse.path_mtu` field when a discovery
|
||||
/// response is cached. `None` when populated from SessionSetup or
|
||||
/// other sources that don't carry path MTU information.
|
||||
path_mtu: Option<u16>,
|
||||
}
|
||||
|
||||
impl CacheEntry {
|
||||
@@ -23,6 +29,7 @@ impl CacheEntry {
|
||||
created_at: current_time_ms,
|
||||
last_used: current_time_ms,
|
||||
expires_at: current_time_ms.saturating_add(ttl_ms),
|
||||
path_mtu: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +53,16 @@ impl CacheEntry {
|
||||
self.expires_at
|
||||
}
|
||||
|
||||
/// Get the path MTU discovered during lookup, if available.
|
||||
pub fn path_mtu(&self) -> Option<u16> {
|
||||
self.path_mtu
|
||||
}
|
||||
|
||||
/// Set the path MTU discovered during lookup.
|
||||
pub fn set_path_mtu(&mut self, mtu: u16) {
|
||||
self.path_mtu = Some(mtu);
|
||||
}
|
||||
|
||||
/// Check if this entry has expired.
|
||||
pub fn is_expired(&self, current_time_ms: u64) -> bool {
|
||||
current_time_ms > self.expires_at
|
||||
|
||||
@@ -92,14 +92,14 @@ impl Node {
|
||||
/// Processing steps:
|
||||
/// 1. Decode and validate
|
||||
/// 2. Check recent_requests to determine if we originated or are forwarding
|
||||
/// 3. If originator: verify proof signature, then cache target_coords in coord_cache
|
||||
/// 4. If transit: reverse-path forward to from_peer
|
||||
/// 3. If originator: verify proof signature, then cache target_coords and path_mtu in coord_cache
|
||||
/// 4. If transit: apply path_mtu min(outgoing_link_mtu), reverse-path forward to from_peer
|
||||
pub(in crate::node) async fn handle_lookup_response(
|
||||
&mut self,
|
||||
from: &NodeAddr,
|
||||
payload: &[u8],
|
||||
) {
|
||||
let response = match LookupResponse::decode(payload) {
|
||||
let mut response = match LookupResponse::decode(payload) {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
debug!(from = %self.peer_display_name(from), error = %e, "Malformed LookupResponse");
|
||||
@@ -114,10 +114,23 @@ impl Node {
|
||||
// Transit node: reverse-path forward
|
||||
let from_peer = recent.from_peer;
|
||||
|
||||
// Apply path_mtu min() from the outgoing link's transport MTU
|
||||
if let Some(peer) = self.peers.get(&from_peer)
|
||||
&& let Some(tid) = peer.transport_id()
|
||||
&& let Some(transport) = self.transports.get(&tid)
|
||||
{
|
||||
if let Some(addr) = peer.current_addr() {
|
||||
response.path_mtu = response.path_mtu.min(transport.link_mtu(addr));
|
||||
} else {
|
||||
response.path_mtu = response.path_mtu.min(transport.mtu());
|
||||
}
|
||||
}
|
||||
|
||||
debug!(
|
||||
request_id = response.request_id,
|
||||
target = %self.peer_display_name(&response.target),
|
||||
next_hop = %self.peer_display_name(&from_peer),
|
||||
path_mtu = response.path_mtu,
|
||||
"Reverse-path forwarding LookupResponse"
|
||||
);
|
||||
|
||||
@@ -132,6 +145,7 @@ impl Node {
|
||||
} else {
|
||||
// We originated this request — verify proof before caching
|
||||
let target = response.target;
|
||||
let path_mtu = response.path_mtu;
|
||||
|
||||
// Look up the target's public key from identity_cache
|
||||
let mut prefix = [0u8; 15];
|
||||
@@ -169,13 +183,15 @@ impl Node {
|
||||
request_id = response.request_id,
|
||||
target = %self.peer_display_name(&target),
|
||||
depth = response.target_coords.depth(),
|
||||
path_mtu = path_mtu,
|
||||
"Received LookupResponse, proof verified, caching route"
|
||||
);
|
||||
|
||||
self.coord_cache.insert(
|
||||
self.coord_cache.insert_with_path_mtu(
|
||||
target,
|
||||
response.target_coords,
|
||||
now_ms,
|
||||
path_mtu,
|
||||
);
|
||||
|
||||
// Clean up pending lookup tracking
|
||||
@@ -321,7 +337,7 @@ impl Node {
|
||||
pub(in crate::node) async fn initiate_lookup(&mut self, target: &NodeAddr, ttl: u8) {
|
||||
let origin = *self.node_addr();
|
||||
let origin_coords = self.tree_state().my_coords().clone();
|
||||
let mut request = LookupRequest::generate(*target, origin, origin_coords, ttl);
|
||||
let mut request = LookupRequest::generate(*target, origin, origin_coords, ttl, 0);
|
||||
|
||||
// Add ourselves to the visited filter so forwarding nodes
|
||||
// won't send the request back to us
|
||||
|
||||
@@ -34,7 +34,7 @@ async fn test_request_dedup() {
|
||||
let origin = make_node_addr(0xCC);
|
||||
let coords = TreeCoordinate::from_addrs(vec![origin, make_node_addr(0)]).unwrap();
|
||||
|
||||
let request = LookupRequest::new(999, target, origin, coords, 5);
|
||||
let request = LookupRequest::new(999, target, origin, coords, 5, 0);
|
||||
let payload = &request.encode()[1..]; // skip msg_type byte
|
||||
|
||||
// First request: accepted
|
||||
@@ -54,7 +54,7 @@ async fn test_request_visited_filter_self() {
|
||||
let origin = make_node_addr(0xCC);
|
||||
let coords = TreeCoordinate::from_addrs(vec![origin, make_node_addr(0)]).unwrap();
|
||||
|
||||
let mut request = LookupRequest::new(888, target, origin, coords, 5);
|
||||
let mut request = LookupRequest::new(888, target, origin, coords, 5, 0);
|
||||
// Mark ourselves as already visited
|
||||
request.visited.insert(node.node_addr());
|
||||
|
||||
@@ -75,7 +75,7 @@ async fn test_request_target_is_self() {
|
||||
let coords = TreeCoordinate::from_addrs(vec![origin, make_node_addr(0)]).unwrap();
|
||||
|
||||
// Request targeting us
|
||||
let request = LookupRequest::new(777, my_addr, origin, coords, 5);
|
||||
let request = LookupRequest::new(777, my_addr, origin, coords, 5, 0);
|
||||
let payload = &request.encode()[1..];
|
||||
|
||||
// Should succeed without panic (response send will fail silently
|
||||
@@ -92,7 +92,7 @@ async fn test_request_ttl_zero_not_forwarded() {
|
||||
let origin = make_node_addr(0xCC);
|
||||
let coords = TreeCoordinate::from_addrs(vec![origin, make_node_addr(0)]).unwrap();
|
||||
|
||||
let request = LookupRequest::new(666, target, origin, coords, 0);
|
||||
let request = LookupRequest::new(666, target, origin, coords, 0, 0);
|
||||
let payload = &request.encode()[1..];
|
||||
|
||||
node.handle_lookup_request(&from, payload).await;
|
||||
@@ -362,7 +362,7 @@ async fn test_recent_request_expiry() {
|
||||
let target = make_node_addr(0xBB);
|
||||
let origin = make_node_addr(0xCC);
|
||||
let coords = TreeCoordinate::from_addrs(vec![origin, make_node_addr(0)]).unwrap();
|
||||
let request = LookupRequest::new(789, target, origin, coords, 3);
|
||||
let request = LookupRequest::new(789, target, origin, coords, 3, 0);
|
||||
let payload = &request.encode()[1..];
|
||||
node.handle_lookup_request(&make_node_addr(0xAA), payload).await;
|
||||
|
||||
@@ -389,7 +389,7 @@ async fn test_request_forwarding_two_node() {
|
||||
let root = make_node_addr(0);
|
||||
|
||||
let coords = TreeCoordinate::from_addrs(vec![node0_addr, root]).unwrap();
|
||||
let request = LookupRequest::new(42, target, node0_addr, coords, 5);
|
||||
let request = LookupRequest::new(42, target, node0_addr, coords, 5, 0);
|
||||
let payload = &request.encode()[1..];
|
||||
|
||||
// Handle on node0 as if we received it from outside
|
||||
@@ -509,7 +509,7 @@ async fn test_request_dedup_convergent_paths() {
|
||||
let root = make_node_addr(0);
|
||||
|
||||
let coords = TreeCoordinate::from_addrs(vec![node0_addr, root]).unwrap();
|
||||
let request = LookupRequest::new(300, target, node0_addr, coords, 5);
|
||||
let request = LookupRequest::new(300, target, node0_addr, coords, 5, 0);
|
||||
let payload = &request.encode()[1..];
|
||||
|
||||
// Node0 handles the request (forwards to both node1 and node2)
|
||||
@@ -695,3 +695,194 @@ async fn test_discovery_100_nodes() {
|
||||
|
||||
cleanup_nodes(&mut nodes).await;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Integration Tests — MTU Propagation
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_response_path_mtu_two_node() {
|
||||
// Two-node topology: node0 — node1
|
||||
// Node0 initiates lookup for node1. The response should carry path_mtu
|
||||
// reflecting the transport MTU (1280 in tests) clamped by transit.
|
||||
// In a two-node setup: node1 (target) initializes path_mtu=u16::MAX,
|
||||
// then the response is sent directly to node0. Since node1 is the
|
||||
// target and sends directly, the transit logic does not apply for the
|
||||
// first hop (the target sends directly). But node0 is the originator
|
||||
// and doesn't apply transit MTU. So path_mtu should be u16::MAX in
|
||||
// this simple case (no transit nodes to clamp it).
|
||||
let edges = vec![(0, 1)];
|
||||
let mut nodes = run_tree_test(2, &edges, false).await;
|
||||
|
||||
let node1_addr = *nodes[1].node.node_addr();
|
||||
|
||||
nodes[0].node.initiate_lookup(&node1_addr, 5).await;
|
||||
|
||||
for _ in 0..4 {
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
process_available_packets(&mut nodes).await;
|
||||
}
|
||||
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
|
||||
assert!(
|
||||
nodes[0].node.coord_cache().contains(&node1_addr, now_ms),
|
||||
"Node 0 should have cached node 1's route"
|
||||
);
|
||||
|
||||
// Check that path_mtu was stored in the cache entry
|
||||
let entry = nodes[0].node.coord_cache().get_entry(&node1_addr).unwrap();
|
||||
let path_mtu = entry.path_mtu().expect("path_mtu should be set from discovery");
|
||||
// In a 2-node setup, no transit node applies the min() so path_mtu stays u16::MAX
|
||||
assert_eq!(
|
||||
path_mtu,
|
||||
u16::MAX,
|
||||
"Two-node path_mtu should be u16::MAX (no transit nodes to clamp)"
|
||||
);
|
||||
|
||||
cleanup_nodes(&mut nodes).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_response_path_mtu_three_node_chain() {
|
||||
// Topology: node0 — node1 — node2
|
||||
// Node0 initiates lookup for node2. The response travels node2→node1→node0.
|
||||
// Node1 is a transit node and applies path_mtu = min(u16::MAX, link_mtu).
|
||||
// With test transport MTU of 1280, the final path_mtu at node0 should be 1280.
|
||||
let edges = vec![(0, 1), (1, 2)];
|
||||
let mut nodes = run_tree_test(3, &edges, false).await;
|
||||
|
||||
let node2_addr = *nodes[2].node.node_addr();
|
||||
let node2_pubkey = nodes[2].node.identity().pubkey_full();
|
||||
|
||||
nodes[0].node.register_identity(node2_addr, node2_pubkey);
|
||||
|
||||
nodes[0].node.initiate_lookup(&node2_addr, 8).await;
|
||||
|
||||
for _ in 0..10 {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
process_available_packets(&mut nodes).await;
|
||||
}
|
||||
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
|
||||
assert!(
|
||||
nodes[0].node.coord_cache().contains(&node2_addr, now_ms),
|
||||
"Node 0 should have cached node 2's route"
|
||||
);
|
||||
|
||||
// Node1 is transit and applies min(u16::MAX, 1280) = 1280
|
||||
let entry = nodes[0].node.coord_cache().get_entry(&node2_addr).unwrap();
|
||||
let path_mtu = entry.path_mtu().expect("path_mtu should be set from discovery");
|
||||
assert_eq!(
|
||||
path_mtu, 1280,
|
||||
"Three-node chain path_mtu should reflect transit node's transport MTU (1280)"
|
||||
);
|
||||
|
||||
cleanup_nodes(&mut nodes).await;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Unit Tests — Cache Entry path_mtu
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cache_entry_path_mtu_stored() {
|
||||
// Verify that insert_with_path_mtu stores the path_mtu in the cache entry
|
||||
let mut node = make_node();
|
||||
let target = make_node_addr(0xBB);
|
||||
|
||||
let coords = TreeCoordinate::from_addrs(vec![target, make_node_addr(0)]).unwrap();
|
||||
|
||||
let now_ms = 1000u64;
|
||||
node.coord_cache_mut().insert_with_path_mtu(
|
||||
target,
|
||||
coords,
|
||||
now_ms,
|
||||
1280,
|
||||
);
|
||||
|
||||
let entry = node.coord_cache().get_entry(&target).unwrap();
|
||||
assert_eq!(entry.path_mtu(), Some(1280));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cache_entry_no_path_mtu_from_regular_insert() {
|
||||
// Verify that regular insert() does not set path_mtu
|
||||
let mut node = make_node();
|
||||
let target = make_node_addr(0xBB);
|
||||
|
||||
let coords = TreeCoordinate::from_addrs(vec![target, make_node_addr(0)]).unwrap();
|
||||
|
||||
let now_ms = 1000u64;
|
||||
node.coord_cache_mut().insert(target, coords, now_ms);
|
||||
|
||||
let entry = node.coord_cache().get_entry(&target).unwrap();
|
||||
assert_eq!(entry.path_mtu(), None);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Unit Tests — LookupRequest min_mtu field
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_min_mtu_preserved_through_encode_decode() {
|
||||
// Verify min_mtu survives encode/decode in the handler test context
|
||||
let target = make_node_addr(0xBB);
|
||||
let origin = make_node_addr(0xCC);
|
||||
let coords = TreeCoordinate::from_addrs(vec![origin, make_node_addr(0)]).unwrap();
|
||||
|
||||
let request = LookupRequest::new(100, target, origin, coords, 5, 1386);
|
||||
let encoded = request.encode();
|
||||
let decoded = LookupRequest::decode(&encoded[1..]).unwrap();
|
||||
assert_eq!(decoded.min_mtu, 1386);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Unit Tests — LookupResponse path_mtu in originator handling
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_originator_stores_path_mtu_in_cache() {
|
||||
// Verify that the originator stores path_mtu from the response in coord_cache
|
||||
let mut node = make_node();
|
||||
let from = make_node_addr(0xAA);
|
||||
|
||||
let target_identity = Identity::generate();
|
||||
let target = *target_identity.node_addr();
|
||||
let root = make_node_addr(0xF0);
|
||||
let coords = TreeCoordinate::from_addrs(vec![target, root]).unwrap();
|
||||
|
||||
node.register_identity(target, target_identity.pubkey_full());
|
||||
|
||||
let proof_data = LookupResponse::proof_bytes(800, &target, &coords);
|
||||
let proof = target_identity.sign(&proof_data);
|
||||
|
||||
let mut response = LookupResponse::new(800, target, coords.clone(), proof);
|
||||
// Simulate transit having reduced path_mtu
|
||||
response.path_mtu = 1280;
|
||||
|
||||
let payload = &response.encode()[1..];
|
||||
|
||||
node.handle_lookup_response(&from, payload).await;
|
||||
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
|
||||
assert!(node.coord_cache().contains(&target, now_ms));
|
||||
|
||||
let entry = node.coord_cache().get_entry(&target).unwrap();
|
||||
assert_eq!(
|
||||
entry.path_mtu(),
|
||||
Some(1280),
|
||||
"Originator should store path_mtu from LookupResponse in cache"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ pub struct LookupRequest {
|
||||
pub origin_coords: TreeCoordinate,
|
||||
/// Remaining propagation hops.
|
||||
pub ttl: u8,
|
||||
/// Minimum transport MTU the origin requires for a viable route.
|
||||
/// 0 means no requirement.
|
||||
pub min_mtu: u16,
|
||||
/// Visited nodes filter (loop prevention).
|
||||
pub visited: BloomFilter,
|
||||
}
|
||||
@@ -35,6 +38,7 @@ impl LookupRequest {
|
||||
origin: NodeAddr,
|
||||
origin_coords: TreeCoordinate,
|
||||
ttl: u8,
|
||||
min_mtu: u16,
|
||||
) -> Self {
|
||||
// Small filter for visited tracking
|
||||
let visited = BloomFilter::with_params(256 * 8, 5).expect("valid params");
|
||||
@@ -44,6 +48,7 @@ impl LookupRequest {
|
||||
origin,
|
||||
origin_coords,
|
||||
ttl,
|
||||
min_mtu,
|
||||
visited,
|
||||
}
|
||||
}
|
||||
@@ -54,10 +59,11 @@ impl LookupRequest {
|
||||
origin: NodeAddr,
|
||||
origin_coords: TreeCoordinate,
|
||||
ttl: u8,
|
||||
min_mtu: u16,
|
||||
) -> Self {
|
||||
use rand::Rng;
|
||||
let request_id = rand::thread_rng().r#gen();
|
||||
Self::new(request_id, target, origin, origin_coords, ttl)
|
||||
Self::new(request_id, target, origin, origin_coords, ttl, min_mtu)
|
||||
}
|
||||
|
||||
/// Decrement TTL and add self to visited.
|
||||
@@ -84,18 +90,19 @@ impl LookupRequest {
|
||||
|
||||
/// Encode as wire format (includes msg_type byte).
|
||||
///
|
||||
/// Format: `[0x30][request_id:8][target:16][origin:16][ttl:1]`
|
||||
/// Format: `[0x30][request_id:8][target:16][origin:16][ttl:1][min_mtu:2]`
|
||||
/// `[origin_coords_cnt:2][origin_coords:16×n]`
|
||||
/// `[visited_hash_cnt:1][visited_bits:256]`
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
let visited_bytes = self.visited.as_bytes();
|
||||
let mut buf = Vec::with_capacity(44 + self.origin_coords.depth() * 16 + 1 + visited_bytes.len());
|
||||
let mut buf = Vec::with_capacity(46 + self.origin_coords.depth() * 16 + 1 + visited_bytes.len());
|
||||
|
||||
buf.push(0x30); // msg_type
|
||||
buf.extend_from_slice(&self.request_id.to_le_bytes());
|
||||
buf.extend_from_slice(self.target.as_bytes());
|
||||
buf.extend_from_slice(self.origin.as_bytes());
|
||||
buf.push(self.ttl);
|
||||
buf.extend_from_slice(&self.min_mtu.to_le_bytes());
|
||||
encode_coords(&self.origin_coords, &mut buf);
|
||||
buf.push(self.visited.hash_count());
|
||||
buf.extend_from_slice(visited_bytes);
|
||||
@@ -105,11 +112,11 @@ impl LookupRequest {
|
||||
|
||||
/// Decode from wire format (after msg_type byte has been consumed).
|
||||
pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
|
||||
// Minimum: request_id(8) + target(16) + origin(16) + ttl(1)
|
||||
// + coords_count(2) + hash_count(1) = 44 bytes
|
||||
if payload.len() < 44 {
|
||||
// Minimum: request_id(8) + target(16) + origin(16) + ttl(1) + min_mtu(2)
|
||||
// + coords_count(2) + hash_count(1) = 46 bytes
|
||||
if payload.len() < 46 {
|
||||
return Err(ProtocolError::MessageTooShort {
|
||||
expected: 44,
|
||||
expected: 46,
|
||||
got: payload.len(),
|
||||
});
|
||||
}
|
||||
@@ -136,6 +143,13 @@ impl LookupRequest {
|
||||
let ttl = payload[pos];
|
||||
pos += 1;
|
||||
|
||||
let min_mtu = u16::from_le_bytes(
|
||||
payload[pos..pos + 2]
|
||||
.try_into()
|
||||
.map_err(|_| ProtocolError::Malformed("bad min_mtu".into()))?,
|
||||
);
|
||||
pos += 2;
|
||||
|
||||
let (origin_coords, consumed) = decode_coords(&payload[pos..])?;
|
||||
pos += consumed;
|
||||
|
||||
@@ -162,6 +176,7 @@ impl LookupRequest {
|
||||
origin,
|
||||
origin_coords,
|
||||
ttl,
|
||||
min_mtu,
|
||||
visited,
|
||||
})
|
||||
}
|
||||
@@ -176,6 +191,12 @@ pub struct LookupResponse {
|
||||
pub request_id: u64,
|
||||
/// The target node.
|
||||
pub target: NodeAddr,
|
||||
/// Minimum transport MTU along the response path.
|
||||
///
|
||||
/// Initialized to `u16::MAX` by the target. Each transit node applies
|
||||
/// `path_mtu = path_mtu.min(outgoing_link_mtu)` when forwarding.
|
||||
/// NOT included in the proof signature (transit annotation).
|
||||
pub path_mtu: u16,
|
||||
/// Target's coordinates in the tree.
|
||||
pub target_coords: TreeCoordinate,
|
||||
/// Proof that target authorized this response (signature over request).
|
||||
@@ -184,6 +205,9 @@ pub struct LookupResponse {
|
||||
|
||||
impl LookupResponse {
|
||||
/// Create a new lookup response.
|
||||
///
|
||||
/// `path_mtu` is initialized to `u16::MAX` by the target; transit
|
||||
/// nodes reduce it as they forward.
|
||||
pub fn new(
|
||||
request_id: u64,
|
||||
target: NodeAddr,
|
||||
@@ -193,6 +217,7 @@ impl LookupResponse {
|
||||
Self {
|
||||
request_id,
|
||||
target,
|
||||
path_mtu: u16::MAX,
|
||||
target_coords,
|
||||
proof,
|
||||
}
|
||||
@@ -212,13 +237,14 @@ impl LookupResponse {
|
||||
|
||||
/// Encode as wire format (includes msg_type byte).
|
||||
///
|
||||
/// Format: `[0x31][request_id:8][target:16][target_coords_cnt:2][target_coords:16×n][proof:64]`
|
||||
/// Format: `[0x31][request_id:8][target:16][path_mtu:2][target_coords_cnt:2][target_coords:16×n][proof:64]`
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(91 + self.target_coords.depth() * 16);
|
||||
let mut buf = Vec::with_capacity(93 + self.target_coords.depth() * 16);
|
||||
|
||||
buf.push(0x31); // msg_type
|
||||
buf.extend_from_slice(&self.request_id.to_le_bytes());
|
||||
buf.extend_from_slice(self.target.as_bytes());
|
||||
buf.extend_from_slice(&self.path_mtu.to_le_bytes());
|
||||
encode_coords(&self.target_coords, &mut buf);
|
||||
buf.extend_from_slice(self.proof.as_ref());
|
||||
|
||||
@@ -227,10 +253,10 @@ impl LookupResponse {
|
||||
|
||||
/// Decode from wire format (after msg_type byte has been consumed).
|
||||
pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
|
||||
// Minimum: request_id(8) + target(16) + coords_count(2) + proof(64) = 90
|
||||
if payload.len() < 90 {
|
||||
// Minimum: request_id(8) + target(16) + path_mtu(2) + coords_count(2) + proof(64) = 92
|
||||
if payload.len() < 92 {
|
||||
return Err(ProtocolError::MessageTooShort {
|
||||
expected: 90,
|
||||
expected: 92,
|
||||
got: payload.len(),
|
||||
});
|
||||
}
|
||||
@@ -249,6 +275,13 @@ impl LookupResponse {
|
||||
let target = NodeAddr::from_bytes(target_bytes);
|
||||
pos += 16;
|
||||
|
||||
let path_mtu = u16::from_le_bytes(
|
||||
payload[pos..pos + 2]
|
||||
.try_into()
|
||||
.map_err(|_| ProtocolError::Malformed("bad path_mtu".into()))?,
|
||||
);
|
||||
pos += 2;
|
||||
|
||||
let (target_coords, consumed) = decode_coords(&payload[pos..])?;
|
||||
pos += consumed;
|
||||
|
||||
@@ -264,6 +297,7 @@ impl LookupResponse {
|
||||
Ok(Self {
|
||||
request_id,
|
||||
target,
|
||||
path_mtu,
|
||||
target_coords,
|
||||
proof,
|
||||
})
|
||||
@@ -291,7 +325,7 @@ mod tests {
|
||||
let coords = make_coords(&[2, 0]);
|
||||
let forwarder = make_node_addr(3);
|
||||
|
||||
let mut request = LookupRequest::new(123, target, origin, coords, 5);
|
||||
let mut request = LookupRequest::new(123, target, origin, coords, 5, 0);
|
||||
|
||||
assert!(request.can_forward());
|
||||
assert!(!request.was_visited(&forwarder));
|
||||
@@ -308,7 +342,7 @@ mod tests {
|
||||
let origin = make_node_addr(2);
|
||||
let coords = make_coords(&[2, 0]);
|
||||
|
||||
let mut request = LookupRequest::new(123, target, origin, coords, 1);
|
||||
let mut request = LookupRequest::new(123, target, origin, coords, 1, 0);
|
||||
|
||||
assert!(request.forward(&make_node_addr(3)));
|
||||
assert!(!request.can_forward());
|
||||
@@ -321,8 +355,8 @@ mod tests {
|
||||
let origin = make_node_addr(2);
|
||||
let coords = make_coords(&[2, 0]);
|
||||
|
||||
let req1 = LookupRequest::generate(target, origin, coords.clone(), 5);
|
||||
let req2 = LookupRequest::generate(target, origin, coords, 5);
|
||||
let req1 = LookupRequest::generate(target, origin, coords.clone(), 5, 0);
|
||||
let req2 = LookupRequest::generate(target, origin, coords, 5, 0);
|
||||
|
||||
// Random IDs should differ
|
||||
assert_ne!(req1.request_id, req2.request_id);
|
||||
@@ -350,7 +384,7 @@ mod tests {
|
||||
let origin = make_node_addr(20);
|
||||
let coords = make_coords(&[20, 0]);
|
||||
|
||||
let mut request = LookupRequest::new(12345, target, origin, coords.clone(), 8);
|
||||
let mut request = LookupRequest::new(12345, target, origin, coords.clone(), 8, 1386);
|
||||
request.forward(&make_node_addr(30));
|
||||
|
||||
let encoded = request.encode();
|
||||
@@ -361,13 +395,28 @@ mod tests {
|
||||
assert_eq!(decoded.target, target);
|
||||
assert_eq!(decoded.origin, origin);
|
||||
assert_eq!(decoded.ttl, 7); // decremented by forward()
|
||||
assert_eq!(decoded.min_mtu, 1386);
|
||||
assert!(decoded.was_visited(&make_node_addr(30)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookup_request_decode_too_short() {
|
||||
assert!(LookupRequest::decode(&[]).is_err());
|
||||
assert!(LookupRequest::decode(&[0u8; 40]).is_err());
|
||||
assert!(LookupRequest::decode(&[0u8; 42]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookup_request_min_mtu_boundary_values() {
|
||||
let target = make_node_addr(10);
|
||||
let origin = make_node_addr(20);
|
||||
let coords = make_coords(&[20, 0]);
|
||||
|
||||
for mtu_val in [0u16, 1386, u16::MAX] {
|
||||
let request = LookupRequest::new(100, target, origin, coords.clone(), 5, mtu_val);
|
||||
let encoded = request.encode();
|
||||
let decoded = LookupRequest::decode(&encoded[1..]).unwrap();
|
||||
assert_eq!(decoded.min_mtu, mtu_val);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -387,15 +436,56 @@ mod tests {
|
||||
|
||||
let response = LookupResponse::new(999, target, coords.clone(), sig);
|
||||
|
||||
// Default path_mtu should be u16::MAX
|
||||
assert_eq!(response.path_mtu, u16::MAX);
|
||||
|
||||
let encoded = response.encode();
|
||||
assert_eq!(encoded[0], 0x31);
|
||||
|
||||
let decoded = LookupResponse::decode(&encoded[1..]).unwrap();
|
||||
assert_eq!(decoded.request_id, 999);
|
||||
assert_eq!(decoded.target, target);
|
||||
assert_eq!(decoded.path_mtu, u16::MAX);
|
||||
assert_eq!(decoded.proof, sig);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookup_response_path_mtu_roundtrip() {
|
||||
use secp256k1::Secp256k1;
|
||||
|
||||
let target = make_node_addr(42);
|
||||
let coords = make_coords(&[42, 1, 0]);
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
let keypair = secp256k1::Keypair::new(&secp, &mut rand::thread_rng());
|
||||
let proof_data = LookupResponse::proof_bytes(999, &target, &coords);
|
||||
use sha2::Digest;
|
||||
let digest: [u8; 32] = sha2::Sha256::digest(&proof_data).into();
|
||||
let sig = secp.sign_schnorr(&digest, &keypair);
|
||||
|
||||
for mtu_val in [0u16, 1280, 1386, 9000, u16::MAX] {
|
||||
let mut response = LookupResponse::new(999, target, coords.clone(), sig);
|
||||
response.path_mtu = mtu_val;
|
||||
|
||||
let encoded = response.encode();
|
||||
let decoded = LookupResponse::decode(&encoded[1..]).unwrap();
|
||||
assert_eq!(decoded.path_mtu, mtu_val);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookup_response_path_mtu_not_in_proof_bytes() {
|
||||
// Verify that proof_bytes does NOT include path_mtu
|
||||
let target = make_node_addr(42);
|
||||
let coords = make_coords(&[42, 1, 0]);
|
||||
|
||||
let bytes = LookupResponse::proof_bytes(12345, &target, &coords);
|
||||
|
||||
// proof_bytes format: request_id(8) + target(16) + coords_encoding(2 + 3*16) = 74
|
||||
// No path_mtu(2) in here
|
||||
assert_eq!(bytes.len(), 74);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookup_response_decode_too_short() {
|
||||
assert!(LookupResponse::decode(&[]).is_err());
|
||||
|
||||
Reference in New Issue
Block a user