mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-10 00:26:59 +00:00
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.
495 lines
16 KiB
Rust
495 lines
16 KiB
Rust
//! Discovery messages: LookupRequest and LookupResponse.
|
||
|
||
use crate::bloom::BloomFilter;
|
||
use crate::protocol::error::ProtocolError;
|
||
use crate::protocol::session::{decode_coords, encode_coords};
|
||
use crate::tree::TreeCoordinate;
|
||
use crate::NodeAddr;
|
||
use secp256k1::schnorr::Signature;
|
||
|
||
/// Request to discover a node's coordinates.
|
||
///
|
||
/// Flooded through the network with TTL limiting scope. The visited
|
||
/// filter prevents routing loops.
|
||
#[derive(Clone, Debug)]
|
||
pub struct LookupRequest {
|
||
/// Unique request identifier.
|
||
pub request_id: u64,
|
||
/// Node we're looking for.
|
||
pub target: NodeAddr,
|
||
/// Who's asking (for response routing).
|
||
pub origin: NodeAddr,
|
||
/// Origin's coordinates (for return path).
|
||
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,
|
||
}
|
||
|
||
impl LookupRequest {
|
||
/// Create a new lookup request.
|
||
pub fn new(
|
||
request_id: u64,
|
||
target: NodeAddr,
|
||
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");
|
||
Self {
|
||
request_id,
|
||
target,
|
||
origin,
|
||
origin_coords,
|
||
ttl,
|
||
min_mtu,
|
||
visited,
|
||
}
|
||
}
|
||
|
||
/// Generate a new request with a random ID.
|
||
pub fn generate(
|
||
target: NodeAddr,
|
||
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, min_mtu)
|
||
}
|
||
|
||
/// Decrement TTL and add self to visited.
|
||
///
|
||
/// Returns false if TTL was already 0.
|
||
pub fn forward(&mut self, my_node_addr: &NodeAddr) -> bool {
|
||
if self.ttl == 0 {
|
||
return false;
|
||
}
|
||
self.ttl -= 1;
|
||
self.visited.insert(my_node_addr);
|
||
true
|
||
}
|
||
|
||
/// Check if this request can still be forwarded.
|
||
pub fn can_forward(&self) -> bool {
|
||
self.ttl > 0
|
||
}
|
||
|
||
/// Check if a node was already visited.
|
||
pub fn was_visited(&self, node_addr: &NodeAddr) -> bool {
|
||
self.visited.contains(node_addr)
|
||
}
|
||
|
||
/// Encode as wire format (includes msg_type byte).
|
||
///
|
||
/// 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(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);
|
||
|
||
buf
|
||
}
|
||
|
||
/// 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) + min_mtu(2)
|
||
// + coords_count(2) + hash_count(1) = 46 bytes
|
||
if payload.len() < 46 {
|
||
return Err(ProtocolError::MessageTooShort {
|
||
expected: 46,
|
||
got: payload.len(),
|
||
});
|
||
}
|
||
|
||
let mut pos = 0;
|
||
|
||
let request_id = u64::from_le_bytes(
|
||
payload[pos..pos + 8]
|
||
.try_into()
|
||
.map_err(|_| ProtocolError::Malformed("bad request_id".into()))?,
|
||
);
|
||
pos += 8;
|
||
|
||
let mut target_bytes = [0u8; 16];
|
||
target_bytes.copy_from_slice(&payload[pos..pos + 16]);
|
||
let target = NodeAddr::from_bytes(target_bytes);
|
||
pos += 16;
|
||
|
||
let mut origin_bytes = [0u8; 16];
|
||
origin_bytes.copy_from_slice(&payload[pos..pos + 16]);
|
||
let origin = NodeAddr::from_bytes(origin_bytes);
|
||
pos += 16;
|
||
|
||
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;
|
||
|
||
if payload.len() < pos + 1 {
|
||
return Err(ProtocolError::MessageTooShort {
|
||
expected: pos + 1,
|
||
got: payload.len(),
|
||
});
|
||
}
|
||
let hash_count = payload[pos];
|
||
pos += 1;
|
||
|
||
let filter_bytes = &payload[pos..];
|
||
if filter_bytes.is_empty() {
|
||
return Err(ProtocolError::Malformed("visited filter missing".into()));
|
||
}
|
||
|
||
let visited = BloomFilter::from_slice(filter_bytes, hash_count)
|
||
.map_err(|e| ProtocolError::Malformed(format!("bad visited filter: {e}")))?;
|
||
|
||
Ok(Self {
|
||
request_id,
|
||
target,
|
||
origin,
|
||
origin_coords,
|
||
ttl,
|
||
min_mtu,
|
||
visited,
|
||
})
|
||
}
|
||
}
|
||
|
||
/// Response to a lookup request with target's coordinates.
|
||
///
|
||
/// Routed back to the origin using the origin_coords from the request.
|
||
#[derive(Clone, Debug)]
|
||
pub struct LookupResponse {
|
||
/// Echoed request identifier.
|
||
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).
|
||
pub proof: Signature,
|
||
}
|
||
|
||
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,
|
||
target_coords: TreeCoordinate,
|
||
proof: Signature,
|
||
) -> Self {
|
||
Self {
|
||
request_id,
|
||
target,
|
||
path_mtu: u16::MAX,
|
||
target_coords,
|
||
proof,
|
||
}
|
||
}
|
||
|
||
/// Get the bytes that should be signed as proof.
|
||
///
|
||
/// Format: request_id (8) || target (16) || coords_encoding (2 + 16×n)
|
||
pub fn proof_bytes(request_id: u64, target: &NodeAddr, target_coords: &TreeCoordinate) -> Vec<u8> {
|
||
let coord_size = 2 + target_coords.entries().len() * 16;
|
||
let mut bytes = Vec::with_capacity(24 + coord_size);
|
||
bytes.extend_from_slice(&request_id.to_le_bytes());
|
||
bytes.extend_from_slice(target.as_bytes());
|
||
encode_coords(target_coords, &mut bytes);
|
||
bytes
|
||
}
|
||
|
||
/// Encode as wire format (includes msg_type byte).
|
||
///
|
||
/// 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(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());
|
||
|
||
buf
|
||
}
|
||
|
||
/// 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) + path_mtu(2) + coords_count(2) + proof(64) = 92
|
||
if payload.len() < 92 {
|
||
return Err(ProtocolError::MessageTooShort {
|
||
expected: 92,
|
||
got: payload.len(),
|
||
});
|
||
}
|
||
|
||
let mut pos = 0;
|
||
|
||
let request_id = u64::from_le_bytes(
|
||
payload[pos..pos + 8]
|
||
.try_into()
|
||
.map_err(|_| ProtocolError::Malformed("bad request_id".into()))?,
|
||
);
|
||
pos += 8;
|
||
|
||
let mut target_bytes = [0u8; 16];
|
||
target_bytes.copy_from_slice(&payload[pos..pos + 16]);
|
||
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;
|
||
|
||
if payload.len() < pos + 64 {
|
||
return Err(ProtocolError::MessageTooShort {
|
||
expected: pos + 64,
|
||
got: payload.len(),
|
||
});
|
||
}
|
||
let proof = Signature::from_slice(&payload[pos..pos + 64])
|
||
.map_err(|_| ProtocolError::Malformed("bad proof signature".into()))?;
|
||
|
||
Ok(Self {
|
||
request_id,
|
||
target,
|
||
path_mtu,
|
||
target_coords,
|
||
proof,
|
||
})
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn make_node_addr(val: u8) -> NodeAddr {
|
||
let mut bytes = [0u8; 16];
|
||
bytes[0] = val;
|
||
NodeAddr::from_bytes(bytes)
|
||
}
|
||
|
||
fn make_coords(ids: &[u8]) -> TreeCoordinate {
|
||
TreeCoordinate::from_addrs(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap()
|
||
}
|
||
|
||
#[test]
|
||
fn test_lookup_request_forward() {
|
||
let target = make_node_addr(1);
|
||
let origin = make_node_addr(2);
|
||
let coords = make_coords(&[2, 0]);
|
||
let forwarder = make_node_addr(3);
|
||
|
||
let mut request = LookupRequest::new(123, target, origin, coords, 5, 0);
|
||
|
||
assert!(request.can_forward());
|
||
assert!(!request.was_visited(&forwarder));
|
||
|
||
assert!(request.forward(&forwarder));
|
||
|
||
assert_eq!(request.ttl, 4);
|
||
assert!(request.was_visited(&forwarder));
|
||
}
|
||
|
||
#[test]
|
||
fn test_lookup_request_ttl_exhausted() {
|
||
let target = make_node_addr(1);
|
||
let origin = make_node_addr(2);
|
||
let coords = make_coords(&[2, 0]);
|
||
|
||
let mut request = LookupRequest::new(123, target, origin, coords, 1, 0);
|
||
|
||
assert!(request.forward(&make_node_addr(3)));
|
||
assert!(!request.can_forward());
|
||
assert!(!request.forward(&make_node_addr(4)));
|
||
}
|
||
|
||
#[test]
|
||
fn test_lookup_request_generate() {
|
||
let target = make_node_addr(1);
|
||
let origin = make_node_addr(2);
|
||
let coords = make_coords(&[2, 0]);
|
||
|
||
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);
|
||
}
|
||
|
||
#[test]
|
||
fn test_lookup_response_proof_bytes() {
|
||
let target = make_node_addr(42);
|
||
let coords = make_coords(&[42, 1, 0]);
|
||
let bytes = LookupResponse::proof_bytes(12345, &target, &coords);
|
||
|
||
// 8 (request_id) + 16 (target) + 2 (count) + 3*16 (coords) = 74
|
||
assert_eq!(bytes.len(), 74);
|
||
assert_eq!(&bytes[0..8], &12345u64.to_le_bytes());
|
||
assert_eq!(&bytes[8..24], target.as_bytes());
|
||
|
||
// Verify coordinate encoding is present
|
||
let count = u16::from_le_bytes([bytes[24], bytes[25]]);
|
||
assert_eq!(count, 3); // 3 entries in coords
|
||
}
|
||
|
||
#[test]
|
||
fn test_lookup_request_encode_decode_roundtrip() {
|
||
let target = make_node_addr(10);
|
||
let origin = make_node_addr(20);
|
||
let coords = make_coords(&[20, 0]);
|
||
|
||
let mut request = LookupRequest::new(12345, target, origin, coords.clone(), 8, 1386);
|
||
request.forward(&make_node_addr(30));
|
||
|
||
let encoded = request.encode();
|
||
assert_eq!(encoded[0], 0x30);
|
||
|
||
let decoded = LookupRequest::decode(&encoded[1..]).unwrap();
|
||
assert_eq!(decoded.request_id, 12345);
|
||
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; 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]
|
||
fn test_lookup_response_encode_decode_roundtrip() {
|
||
use secp256k1::Secp256k1;
|
||
|
||
let target = make_node_addr(42);
|
||
let coords = make_coords(&[42, 1, 0]);
|
||
|
||
// Create a dummy signature for testing
|
||
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);
|
||
|
||
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());
|
||
assert!(LookupResponse::decode(&[0u8; 50]).is_err());
|
||
}
|
||
}
|