aboutsummaryrefslogtreecommitdiffstats
path: root/src/msg.rs
diff options
context:
space:
mode:
authormurilo ijanc2026-03-24 15:04:03 -0300
committermurilo ijanc2026-03-24 15:04:03 -0300
commit9821aabf0b50d2487b07502d3d2cd89e7d62bdbe (patch)
tree53da095ff90cc755bac3d4bf699172b5e8cd07d6 /src/msg.rs
downloadtesseras-dht-0.1.0.tar.gz
Initial commitv0.1.0
NAT-aware Kademlia DHT library for peer-to-peer networks. Features: - Distributed key-value storage (iterative FIND_NODE, FIND_VALUE, STORE) - NAT traversal via DTUN hole-punching and proxy relay - Reliable Datagram Protocol (RDP) with 7-state connection machine - Datagram transport with automatic fragmentation/reassembly - Ed25519 packet authentication - 256-bit node IDs (Ed25519 public keys) - Rate limiting, ban list, and eclipse attack mitigation - Persistence and metrics - OpenBSD and Linux support
Diffstat (limited to 'src/msg.rs')
-rw-r--r--src/msg.rs830
1 files changed, 830 insertions, 0 deletions
diff --git a/src/msg.rs b/src/msg.rs
new file mode 100644
index 0000000..95c1d4c
--- /dev/null
+++ b/src/msg.rs
@@ -0,0 +1,830 @@
+//! Message body parsing and writing.
+//!
+//! Each protocol message has a fixed header (parsed by
+//! `wire::MsgHeader`) followed by a variable body. This
+//! module provides parse/write for all body types.
+//!
+//! All multi-byte fields are big-endian (network order).
+
+use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
+
+use crate::error::Error;
+use crate::id::{ID_LEN, NodeId};
+use crate::peers::PeerInfo;
+use crate::wire::{DOMAIN_INET, HEADER_SIZE};
+
+// ── Helpers ─────────────────────────────────────────
+
+// Callers MUST validate `off + 2 <= buf.len()` before calling.
+fn read_u16(buf: &[u8], off: usize) -> u16 {
+ u16::from_be_bytes([buf[off], buf[off + 1]])
+}
+
+fn read_u32(buf: &[u8], off: usize) -> u32 {
+ u32::from_be_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
+}
+
+fn write_u16(buf: &mut [u8], off: usize, v: u16) {
+ buf[off..off + 2].copy_from_slice(&v.to_be_bytes());
+}
+
+fn write_u32(buf: &mut [u8], off: usize, v: u32) {
+ buf[off..off + 4].copy_from_slice(&v.to_be_bytes());
+}
+
+// ── Ping (DHT + DTUN) ──────────────────────────────
+
+/// Body: just a nonce (4 bytes after header).
+/// Used by: DhtPing, DhtPingReply, DtunPing,
+/// DtunPingReply, DtunRequestReply.
+pub fn parse_ping(buf: &[u8]) -> Result<u32, Error> {
+ if buf.len() < HEADER_SIZE + 4 {
+ return Err(Error::BufferTooSmall);
+ }
+ Ok(read_u32(buf, HEADER_SIZE))
+}
+
+pub fn write_ping(buf: &mut [u8], nonce: u32) {
+ write_u32(buf, HEADER_SIZE, nonce);
+}
+
+/// Total size of a ping message.
+pub const PING_MSG_SIZE: usize = HEADER_SIZE + 4;
+
+// ── NAT Echo ────────────────────────────────────────
+
+/// Parse NatEcho: nonce only.
+pub fn parse_nat_echo(buf: &[u8]) -> Result<u32, Error> {
+ parse_ping(buf) // same layout
+}
+
+/// NatEchoReply body: nonce(4) + domain(2) + port(2) + addr(16).
+pub const NAT_ECHO_REPLY_BODY: usize = 4 + 2 + 2 + 16;
+
+#[derive(Debug, Clone)]
+pub struct NatEchoReply {
+ pub nonce: u32,
+ pub domain: u16,
+ pub port: u16,
+ pub addr: [u8; 16],
+}
+
+pub fn parse_nat_echo_reply(buf: &[u8]) -> Result<NatEchoReply, Error> {
+ let off = HEADER_SIZE;
+ if buf.len() < off + NAT_ECHO_REPLY_BODY {
+ return Err(Error::BufferTooSmall);
+ }
+ Ok(NatEchoReply {
+ nonce: read_u32(buf, off),
+ domain: read_u16(buf, off + 4),
+ port: read_u16(buf, off + 6),
+ addr: {
+ let mut a = [0u8; 16];
+ a.copy_from_slice(&buf[off + 8..off + 24]);
+ a
+ },
+ })
+}
+
+pub fn write_nat_echo_reply(buf: &mut [u8], reply: &NatEchoReply) {
+ let off = HEADER_SIZE;
+ write_u32(buf, off, reply.nonce);
+ write_u16(buf, off + 4, reply.domain);
+ write_u16(buf, off + 6, reply.port);
+ buf[off + 8..off + 24].copy_from_slice(&reply.addr);
+}
+
+/// NatEchoRedirect body: nonce(4) + port(2) + padding(2).
+pub fn parse_nat_echo_redirect(buf: &[u8]) -> Result<(u32, u16), Error> {
+ let off = HEADER_SIZE;
+ if buf.len() < off + 8 {
+ return Err(Error::BufferTooSmall);
+ }
+ Ok((read_u32(buf, off), read_u16(buf, off + 4)))
+}
+
+/// Write NatEchoRedirect body.
+pub fn write_nat_echo_redirect(buf: &mut [u8], nonce: u32, port: u16) {
+ let off = HEADER_SIZE;
+ write_u32(buf, off, nonce);
+ write_u16(buf, off + 4, port);
+ write_u16(buf, off + 6, 0); // padding
+}
+
+/// Size of a NatEchoRedirect message.
+pub const NAT_ECHO_REDIRECT_SIZE: usize = HEADER_SIZE + 8;
+
+// ── FindNode (DHT + DTUN) ───────────────────────────
+
+/// FindNode body: nonce(4) + id(20) + domain(2) + state_or_pad(2).
+pub const FIND_NODE_BODY: usize = 4 + ID_LEN + 2 + 2;
+
+#[derive(Debug, Clone)]
+pub struct FindNodeMsg {
+ pub nonce: u32,
+ pub target: NodeId,
+ pub domain: u16,
+ pub state: u16,
+}
+
+pub fn parse_find_node(buf: &[u8]) -> Result<FindNodeMsg, Error> {
+ let off = HEADER_SIZE;
+ if buf.len() < off + FIND_NODE_BODY {
+ return Err(Error::BufferTooSmall);
+ }
+ Ok(FindNodeMsg {
+ nonce: read_u32(buf, off),
+ target: NodeId::read_from(&buf[off + 4..off + 4 + ID_LEN]),
+ domain: read_u16(buf, off + 4 + ID_LEN),
+ state: read_u16(buf, off + 6 + ID_LEN),
+ })
+}
+
+pub fn write_find_node(buf: &mut [u8], msg: &FindNodeMsg) {
+ let off = HEADER_SIZE;
+ write_u32(buf, off, msg.nonce);
+ msg.target.write_to(&mut buf[off + 4..off + 4 + ID_LEN]);
+ write_u16(buf, off + 4 + ID_LEN, msg.domain);
+ write_u16(buf, off + 6 + ID_LEN, msg.state);
+}
+
+pub const FIND_NODE_MSG_SIZE: usize = HEADER_SIZE + FIND_NODE_BODY;
+
+// ── FindNodeReply (DHT + DTUN) ──────────────────────
+
+/// FindNodeReply fixed part: nonce(4) + id(20) +
+/// domain(2) + num(1) + padding(1).
+pub const FIND_NODE_REPLY_FIXED: usize = 4 + ID_LEN + 4;
+
+#[derive(Debug, Clone)]
+pub struct FindNodeReplyMsg {
+ pub nonce: u32,
+ pub id: NodeId,
+ pub domain: u16,
+ pub nodes: Vec<PeerInfo>,
+}
+
+/// Size of an IPv4 node entry: port(2) + reserved(2) +
+/// addr(4) + id(ID_LEN).
+pub const INET_NODE_SIZE: usize = 8 + ID_LEN;
+
+/// Size of an IPv6 node entry: port(2) + reserved(2) +
+/// addr(16) + id(ID_LEN).
+pub const INET6_NODE_SIZE: usize = 20 + ID_LEN;
+
+pub fn parse_find_node_reply(buf: &[u8]) -> Result<FindNodeReplyMsg, Error> {
+ let off = HEADER_SIZE;
+ if buf.len() < off + FIND_NODE_REPLY_FIXED {
+ return Err(Error::BufferTooSmall);
+ }
+
+ let nonce = read_u32(buf, off);
+ let id = NodeId::read_from(&buf[off + 4..off + 4 + ID_LEN]);
+ let domain = read_u16(buf, off + 4 + ID_LEN);
+ let num = buf[off + 4 + ID_LEN + 2] as usize;
+
+ let nodes_off = off + FIND_NODE_REPLY_FIXED;
+ let nodes = if domain == DOMAIN_INET {
+ read_nodes_inet(&buf[nodes_off..], num)
+ } else {
+ read_nodes_inet6(&buf[nodes_off..], num)
+ };
+
+ Ok(FindNodeReplyMsg {
+ nonce,
+ id,
+ domain,
+ nodes,
+ })
+}
+
+pub fn write_find_node_reply(buf: &mut [u8], msg: &FindNodeReplyMsg) -> usize {
+ let off = HEADER_SIZE;
+ write_u32(buf, off, msg.nonce);
+ msg.id.write_to(&mut buf[off + 4..off + 4 + ID_LEN]);
+ write_u16(buf, off + 4 + ID_LEN, msg.domain);
+ let num = msg.nodes.len().min(MAX_NODES_PER_REPLY);
+ buf[off + 4 + ID_LEN + 2] = num as u8;
+ buf[off + 4 + ID_LEN + 3] = 0; // padding
+
+ let nodes_off = off + FIND_NODE_REPLY_FIXED;
+ let nodes_len = if msg.domain == DOMAIN_INET {
+ write_nodes_inet(&mut buf[nodes_off..], &msg.nodes)
+ } else {
+ write_nodes_inet6(&mut buf[nodes_off..], &msg.nodes)
+ };
+
+ HEADER_SIZE + FIND_NODE_REPLY_FIXED + nodes_len
+}
+
+// ── Store (DHT) ─────────────────────────────────────
+
+/// Store fixed part: id(20) + from(20) + keylen(2) +
+/// valuelen(2) + ttl(2) + flags(1) + reserved(1) = 48.
+pub const STORE_FIXED: usize = ID_LEN * 2 + 8;
+
+#[derive(Debug, Clone)]
+pub struct StoreMsg {
+ pub id: NodeId,
+ pub from: NodeId,
+ pub key: Vec<u8>,
+ pub value: Vec<u8>,
+ pub ttl: u16,
+ pub is_unique: bool,
+}
+
+pub fn parse_store(buf: &[u8]) -> Result<StoreMsg, Error> {
+ let off = HEADER_SIZE;
+ if buf.len() < off + STORE_FIXED {
+ return Err(Error::BufferTooSmall);
+ }
+
+ let id = NodeId::read_from(&buf[off..off + ID_LEN]);
+ let from = NodeId::read_from(&buf[off + ID_LEN..off + ID_LEN * 2]);
+ let keylen = read_u16(buf, off + ID_LEN * 2) as usize;
+ let valuelen = read_u16(buf, off + ID_LEN * 2 + 2) as usize;
+ let ttl = read_u16(buf, off + ID_LEN * 2 + 4);
+ let flags = buf[off + ID_LEN * 2 + 6];
+
+ let data_off = off + STORE_FIXED;
+ let total = data_off
+ .checked_add(keylen)
+ .and_then(|v| v.checked_add(valuelen))
+ .ok_or(Error::InvalidMessage)?;
+
+ if buf.len() < total {
+ return Err(Error::BufferTooSmall);
+ }
+
+ let key = buf[data_off..data_off + keylen].to_vec();
+ let value = buf[data_off + keylen..data_off + keylen + valuelen].to_vec();
+
+ Ok(StoreMsg {
+ id,
+ from,
+ key,
+ value,
+ ttl,
+ is_unique: flags & crate::wire::DHT_FLAG_UNIQUE != 0,
+ })
+}
+
+pub fn write_store(buf: &mut [u8], msg: &StoreMsg) -> Result<usize, Error> {
+ let off = HEADER_SIZE;
+ let total = off + STORE_FIXED + msg.key.len() + msg.value.len();
+ if buf.len() < total {
+ return Err(Error::BufferTooSmall);
+ }
+
+ msg.id.write_to(&mut buf[off..off + ID_LEN]);
+ msg.from.write_to(&mut buf[off + ID_LEN..off + ID_LEN * 2]);
+ let keylen =
+ u16::try_from(msg.key.len()).map_err(|_| Error::BufferTooSmall)?;
+ let valuelen =
+ u16::try_from(msg.value.len()).map_err(|_| Error::BufferTooSmall)?;
+ write_u16(buf, off + ID_LEN * 2, keylen);
+ write_u16(buf, off + ID_LEN * 2 + 2, valuelen);
+ write_u16(buf, off + ID_LEN * 2 + 4, msg.ttl);
+ buf[off + ID_LEN * 2 + 6] = if msg.is_unique {
+ crate::wire::DHT_FLAG_UNIQUE
+ } else {
+ 0
+ };
+ buf[off + ID_LEN * 2 + 7] = 0; // reserved
+
+ let data_off = off + STORE_FIXED;
+ buf[data_off..data_off + msg.key.len()].copy_from_slice(&msg.key);
+ buf[data_off + msg.key.len()..data_off + msg.key.len() + msg.value.len()]
+ .copy_from_slice(&msg.value);
+
+ Ok(total)
+}
+
+// ── FindValue (DHT) ─────────────────────────────────
+
+/// FindValue fixed: nonce(4) + id(20) + domain(2) +
+/// keylen(2) + flag(1) + padding(3) = 32.
+pub const FIND_VALUE_FIXED: usize = 4 + ID_LEN + 8;
+
+#[derive(Debug, Clone)]
+pub struct FindValueMsg {
+ pub nonce: u32,
+ pub target: NodeId,
+ pub domain: u16,
+ pub key: Vec<u8>,
+ pub use_rdp: bool,
+}
+
+pub fn parse_find_value(buf: &[u8]) -> Result<FindValueMsg, Error> {
+ let off = HEADER_SIZE;
+ if buf.len() < off + FIND_VALUE_FIXED {
+ return Err(Error::BufferTooSmall);
+ }
+
+ let nonce = read_u32(buf, off);
+ let target = NodeId::read_from(&buf[off + 4..off + 4 + ID_LEN]);
+ let domain = read_u16(buf, off + 4 + ID_LEN);
+ let keylen = read_u16(buf, off + 6 + ID_LEN) as usize;
+ let flag = buf[off + 8 + ID_LEN];
+
+ let key_off = off + FIND_VALUE_FIXED;
+ if buf.len() < key_off + keylen {
+ return Err(Error::BufferTooSmall);
+ }
+
+ Ok(FindValueMsg {
+ nonce,
+ target,
+ domain,
+ key: buf[key_off..key_off + keylen].to_vec(),
+ use_rdp: flag == 1,
+ })
+}
+
+pub fn write_find_value(
+ buf: &mut [u8],
+ msg: &FindValueMsg,
+) -> Result<usize, Error> {
+ let off = HEADER_SIZE;
+ let total = off + FIND_VALUE_FIXED + msg.key.len();
+ if buf.len() < total {
+ return Err(Error::BufferTooSmall);
+ }
+
+ write_u32(buf, off, msg.nonce);
+ msg.target.write_to(&mut buf[off + 4..off + 4 + ID_LEN]);
+ write_u16(buf, off + 4 + ID_LEN, msg.domain);
+ let keylen =
+ u16::try_from(msg.key.len()).map_err(|_| Error::BufferTooSmall)?;
+ write_u16(buf, off + 6 + ID_LEN, keylen);
+ buf[off + 8 + ID_LEN] = if msg.use_rdp { 1 } else { 0 };
+ buf[off + 9 + ID_LEN] = 0;
+ buf[off + 10 + ID_LEN] = 0;
+ buf[off + 11 + ID_LEN] = 0;
+
+ let key_off = off + FIND_VALUE_FIXED;
+ buf[key_off..key_off + msg.key.len()].copy_from_slice(&msg.key);
+
+ Ok(total)
+}
+
+// ── FindValueReply (DHT) ────────────────────────────
+
+/// Fixed: nonce(4) + id(20) + index(2) + total(2) +
+/// flag(1) + padding(3) = 32.
+pub const FIND_VALUE_REPLY_FIXED: usize = 4 + ID_LEN + 8;
+
+#[derive(Debug, Clone)]
+pub enum FindValueReplyData {
+ /// flag=0xa0: node list
+ Nodes { domain: u16, nodes: Vec<PeerInfo> },
+
+ /// flag=0xa1: a value chunk
+ Value {
+ index: u16,
+ total: u16,
+ data: Vec<u8>,
+ },
+
+ /// flag=0xa2: no data
+ Nul,
+}
+
+#[derive(Debug, Clone)]
+pub struct FindValueReplyMsg {
+ pub nonce: u32,
+ pub id: NodeId,
+ pub data: FindValueReplyData,
+}
+
+pub fn parse_find_value_reply(buf: &[u8]) -> Result<FindValueReplyMsg, Error> {
+ let off = HEADER_SIZE;
+ if buf.len() < off + FIND_VALUE_REPLY_FIXED {
+ return Err(Error::BufferTooSmall);
+ }
+
+ let nonce = read_u32(buf, off);
+ let id = NodeId::read_from(&buf[off + 4..off + 4 + ID_LEN]);
+ let index = read_u16(buf, off + 4 + ID_LEN);
+ let total = read_u16(buf, off + 6 + ID_LEN);
+ let flag = buf[off + 8 + ID_LEN];
+
+ let data_off = off + FIND_VALUE_REPLY_FIXED;
+
+ let data = match flag {
+ crate::wire::DATA_ARE_NODES => {
+ if buf.len() < data_off + 4 {
+ return Err(Error::BufferTooSmall);
+ }
+ let domain = read_u16(buf, data_off);
+ let num = buf[data_off + 2] as usize;
+ let nodes_off = data_off + 4;
+ let nodes = if domain == DOMAIN_INET {
+ read_nodes_inet(&buf[nodes_off..], num)
+ } else {
+ read_nodes_inet6(&buf[nodes_off..], num)
+ };
+ FindValueReplyData::Nodes { domain, nodes }
+ }
+ crate::wire::DATA_ARE_VALUES => {
+ let payload = buf[data_off..].to_vec();
+ FindValueReplyData::Value {
+ index,
+ total,
+ data: payload,
+ }
+ }
+ crate::wire::DATA_ARE_NUL => FindValueReplyData::Nul,
+ _ => return Err(Error::InvalidMessage),
+ };
+
+ Ok(FindValueReplyMsg { nonce, id, data })
+}
+
+// ── DtunRegister ────────────────────────────────────
+
+pub fn parse_dtun_register(buf: &[u8]) -> Result<u32, Error> {
+ if buf.len() < HEADER_SIZE + 4 {
+ return Err(Error::BufferTooSmall);
+ }
+ Ok(read_u32(buf, HEADER_SIZE)) // session
+}
+
+// ── DtunRequest ─────────────────────────────────────
+
+pub fn parse_dtun_request(buf: &[u8]) -> Result<(u32, NodeId), Error> {
+ let off = HEADER_SIZE;
+ if buf.len() < off + 4 + ID_LEN {
+ return Err(Error::BufferTooSmall);
+ }
+ let nonce = read_u32(buf, off);
+ let target = NodeId::read_from(&buf[off + 4..off + 4 + ID_LEN]);
+ Ok((nonce, target))
+}
+
+// ── Node list serialization (IPv4 / IPv6) ───────────
+
+/// Maximum nodes per reply (prevents OOM from malicious num).
+const MAX_NODES_PER_REPLY: usize = 20;
+
+/// Read `num` IPv4 node entries from `buf`.
+pub fn read_nodes_inet(buf: &[u8], num: usize) -> Vec<PeerInfo> {
+ let num = num.min(MAX_NODES_PER_REPLY);
+ let mut nodes = Vec::with_capacity(num);
+ for i in 0..num {
+ let off = i * INET_NODE_SIZE;
+ if off + INET_NODE_SIZE > buf.len() {
+ break;
+ }
+ let port = read_u16(buf, off);
+ let ip = Ipv4Addr::new(
+ buf[off + 4],
+ buf[off + 5],
+ buf[off + 6],
+ buf[off + 7],
+ );
+ let id = NodeId::read_from(&buf[off + 8..off + 8 + ID_LEN]);
+ let addr = SocketAddr::V4(SocketAddrV4::new(ip, port));
+ nodes.push(PeerInfo::new(id, addr));
+ }
+ nodes
+}
+
+/// Read `num` IPv6 node entries from `buf`.
+pub fn read_nodes_inet6(buf: &[u8], num: usize) -> Vec<PeerInfo> {
+ let num = num.min(MAX_NODES_PER_REPLY);
+ let mut nodes = Vec::with_capacity(num);
+ for i in 0..num {
+ let off = i * INET6_NODE_SIZE;
+ if off + INET6_NODE_SIZE > buf.len() {
+ break;
+ }
+ let port = read_u16(buf, off);
+ let mut octets = [0u8; 16];
+ octets.copy_from_slice(&buf[off + 4..off + 20]);
+ let ip = Ipv6Addr::from(octets);
+ let id = NodeId::read_from(&buf[off + 20..off + 20 + ID_LEN]);
+ let addr = SocketAddr::V6(SocketAddrV6::new(ip, port, 0, 0));
+ nodes.push(PeerInfo::new(id, addr));
+ }
+ nodes
+}
+
+/// Write IPv4 node entries. Returns bytes written.
+pub fn write_nodes_inet(buf: &mut [u8], nodes: &[PeerInfo]) -> usize {
+ let mut written = 0;
+ for node in nodes {
+ if written + INET_NODE_SIZE > buf.len() {
+ break;
+ }
+ let off = written;
+ write_u16(buf, off, node.addr.port());
+ write_u16(buf, off + 2, 0); // reserved
+
+ if let SocketAddr::V4(v4) = node.addr {
+ let octets = v4.ip().octets();
+ buf[off + 4..off + 8].copy_from_slice(&octets);
+ } else {
+ buf[off + 4..off + 8].fill(0);
+ }
+
+ node.id.write_to(&mut buf[off + 8..off + 8 + ID_LEN]);
+ written += INET_NODE_SIZE;
+ }
+ written
+}
+
+/// Write IPv6 node entries. Returns bytes written.
+pub fn write_nodes_inet6(buf: &mut [u8], nodes: &[PeerInfo]) -> usize {
+ let mut written = 0;
+ for node in nodes {
+ if written + INET6_NODE_SIZE > buf.len() {
+ break;
+ }
+ let off = written;
+ write_u16(buf, off, node.addr.port());
+ write_u16(buf, off + 2, 0); // reserved
+
+ if let SocketAddr::V6(v6) = node.addr {
+ let octets = v6.ip().octets();
+ buf[off + 4..off + 20].copy_from_slice(&octets);
+ } else {
+ buf[off + 4..off + 20].fill(0);
+ }
+
+ node.id.write_to(&mut buf[off + 20..off + 20 + ID_LEN]);
+ written += INET6_NODE_SIZE;
+ }
+ written
+}
+
+/// Create a PeerInfo from a message header and source
+/// address.
+pub fn peer_from_header(src_id: NodeId, from: SocketAddr) -> PeerInfo {
+ PeerInfo::new(src_id, from)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::wire::{MsgHeader, MsgType};
+
+ fn make_buf(msg_type: MsgType, body_len: usize) -> Vec<u8> {
+ let total = HEADER_SIZE + body_len;
+ let mut buf = vec![0u8; total];
+ let hdr = MsgHeader::new(
+ msg_type,
+ total as u16,
+ NodeId::from_bytes([0xAA; 32]),
+ NodeId::from_bytes([0xBB; 32]),
+ );
+ hdr.write(&mut buf).unwrap();
+ buf
+ }
+
+ // ── Ping ────────────────────────────────────────
+
+ #[test]
+ fn ping_roundtrip() {
+ let mut buf = make_buf(MsgType::DhtPing, 4);
+ write_ping(&mut buf, 0xDEADBEEF);
+ let nonce = parse_ping(&buf).unwrap();
+ assert_eq!(nonce, 0xDEADBEEF);
+ }
+
+ // ── NatEchoReply ────────────────────────────────
+
+ #[test]
+ fn nat_echo_reply_roundtrip() {
+ let mut buf = make_buf(MsgType::NatEchoReply, NAT_ECHO_REPLY_BODY);
+ let reply = NatEchoReply {
+ nonce: 42,
+ domain: DOMAIN_INET,
+ port: 3000,
+ addr: {
+ let mut a = [0u8; 16];
+ a[0..4].copy_from_slice(&[192, 168, 1, 1]);
+ a
+ },
+ };
+ write_nat_echo_reply(&mut buf, &reply);
+ let parsed = parse_nat_echo_reply(&buf).unwrap();
+ assert_eq!(parsed.nonce, 42);
+ assert_eq!(parsed.domain, DOMAIN_INET);
+ assert_eq!(parsed.port, 3000);
+ assert_eq!(parsed.addr[0..4], [192, 168, 1, 1]);
+ }
+
+ // ── FindNode ────────────────────────────────────
+
+ #[test]
+ fn find_node_roundtrip() {
+ let mut buf = make_buf(MsgType::DhtFindNode, FIND_NODE_BODY);
+ let msg = FindNodeMsg {
+ nonce: 99,
+ target: NodeId::from_bytes([0x42; 32]),
+ domain: DOMAIN_INET,
+ state: 1,
+ };
+ write_find_node(&mut buf, &msg);
+ let parsed = parse_find_node(&buf).unwrap();
+ assert_eq!(parsed.nonce, 99);
+ assert_eq!(parsed.target, msg.target);
+ assert_eq!(parsed.domain, DOMAIN_INET);
+ }
+
+ // ── FindNodeReply ───────────────────────────────
+
+ #[test]
+ fn find_node_reply_roundtrip() {
+ let nodes = vec![
+ PeerInfo::new(
+ NodeId::from_bytes([0x01; 32]),
+ "127.0.0.1:3000".parse().unwrap(),
+ ),
+ PeerInfo::new(
+ NodeId::from_bytes([0x02; 32]),
+ "127.0.0.1:3001".parse().unwrap(),
+ ),
+ ];
+ let msg = FindNodeReplyMsg {
+ nonce: 55,
+ id: NodeId::from_bytes([0x42; 32]),
+ domain: DOMAIN_INET,
+ nodes: nodes.clone(),
+ };
+
+ let mut buf = vec![0u8; 1024];
+ let hdr = MsgHeader::new(
+ MsgType::DhtFindNodeReply,
+ 0, // will fix
+ NodeId::from_bytes([0xAA; 32]),
+ NodeId::from_bytes([0xBB; 32]),
+ );
+ hdr.write(&mut buf).unwrap();
+ let _total = write_find_node_reply(&mut buf, &msg);
+
+ let parsed = parse_find_node_reply(&buf).unwrap();
+ assert_eq!(parsed.nonce, 55);
+ assert_eq!(parsed.nodes.len(), 2);
+ assert_eq!(parsed.nodes[0].id, nodes[0].id);
+ assert_eq!(parsed.nodes[0].addr.port(), 3000);
+ assert_eq!(parsed.nodes[1].id, nodes[1].id);
+ }
+
+ // ── Store ───────────────────────────────────────
+
+ #[test]
+ fn store_roundtrip() {
+ let msg = StoreMsg {
+ id: NodeId::from_bytes([0x10; 32]),
+ from: NodeId::from_bytes([0x20; 32]),
+ key: b"mykey".to_vec(),
+ value: b"myvalue".to_vec(),
+ ttl: 300,
+ is_unique: true,
+ };
+ let total = HEADER_SIZE + STORE_FIXED + msg.key.len() + msg.value.len();
+ let mut buf = vec![0u8; total];
+ let hdr = MsgHeader::new(
+ MsgType::DhtStore,
+ total as u16,
+ NodeId::from_bytes([0xAA; 32]),
+ NodeId::from_bytes([0xBB; 32]),
+ );
+ hdr.write(&mut buf).unwrap();
+ write_store(&mut buf, &msg).unwrap();
+
+ let parsed = parse_store(&buf).unwrap();
+ assert_eq!(parsed.id, msg.id);
+ assert_eq!(parsed.from, msg.from);
+ assert_eq!(parsed.key, b"mykey");
+ assert_eq!(parsed.value, b"myvalue");
+ assert_eq!(parsed.ttl, 300);
+ assert!(parsed.is_unique);
+ }
+
+ #[test]
+ fn store_not_unique() {
+ let msg = StoreMsg {
+ id: NodeId::from_bytes([0x10; 32]),
+ from: NodeId::from_bytes([0x20; 32]),
+ key: b"k".to_vec(),
+ value: b"v".to_vec(),
+ ttl: 60,
+ is_unique: false,
+ };
+ let total = HEADER_SIZE + STORE_FIXED + msg.key.len() + msg.value.len();
+ let mut buf = vec![0u8; total];
+ let hdr = MsgHeader::new(
+ MsgType::DhtStore,
+ total as u16,
+ NodeId::from_bytes([0xAA; 32]),
+ NodeId::from_bytes([0xBB; 32]),
+ );
+ hdr.write(&mut buf).unwrap();
+ write_store(&mut buf, &msg).unwrap();
+
+ let parsed = parse_store(&buf).unwrap();
+ assert!(!parsed.is_unique);
+ }
+
+ // ── FindValue ───────────────────────────────────
+
+ #[test]
+ fn find_value_roundtrip() {
+ let msg = FindValueMsg {
+ nonce: 77,
+ target: NodeId::from_bytes([0x33; 32]),
+ domain: DOMAIN_INET,
+ key: b"lookup-key".to_vec(),
+ use_rdp: false,
+ };
+ let total = HEADER_SIZE + FIND_VALUE_FIXED + msg.key.len();
+ let mut buf = vec![0u8; total];
+ let hdr = MsgHeader::new(
+ MsgType::DhtFindValue,
+ total as u16,
+ NodeId::from_bytes([0xAA; 32]),
+ NodeId::from_bytes([0xBB; 32]),
+ );
+ hdr.write(&mut buf).unwrap();
+ write_find_value(&mut buf, &msg).unwrap();
+
+ let parsed = parse_find_value(&buf).unwrap();
+ assert_eq!(parsed.nonce, 77);
+ assert_eq!(parsed.target, msg.target);
+ assert_eq!(parsed.key, b"lookup-key");
+ assert!(!parsed.use_rdp);
+ }
+
+ // ── DtunRegister ────────────────────────────────
+
+ #[test]
+ fn dtun_register_roundtrip() {
+ let mut buf = make_buf(MsgType::DtunRegister, 4);
+ write_u32(&mut buf, HEADER_SIZE, 12345);
+ let session = parse_dtun_register(&buf).unwrap();
+ assert_eq!(session, 12345);
+ }
+
+ // ── DtunRequest ─────────────────────────────────
+
+ #[test]
+ fn dtun_request_roundtrip() {
+ let mut buf = make_buf(MsgType::DtunRequest, 4 + ID_LEN);
+ let nonce = 88u32;
+ let target = NodeId::from_bytes([0x55; 32]);
+ write_u32(&mut buf, HEADER_SIZE, nonce);
+ target.write_to(&mut buf[HEADER_SIZE + 4..HEADER_SIZE + 4 + ID_LEN]);
+
+ let (n, t) = parse_dtun_request(&buf).unwrap();
+ assert_eq!(n, 88);
+ assert_eq!(t, target);
+ }
+
+ // ── Node list ───────────────────────────────────
+
+ #[test]
+ fn inet_nodes_roundtrip() {
+ let nodes = vec![
+ PeerInfo::new(
+ NodeId::from_bytes([0x01; 32]),
+ "10.0.0.1:8000".parse().unwrap(),
+ ),
+ PeerInfo::new(
+ NodeId::from_bytes([0x02; 32]),
+ "10.0.0.2:9000".parse().unwrap(),
+ ),
+ ];
+ let mut buf = vec![0u8; INET_NODE_SIZE * 2];
+ let written = write_nodes_inet(&mut buf, &nodes);
+ assert_eq!(written, INET_NODE_SIZE * 2);
+
+ let parsed = read_nodes_inet(&buf, 2);
+ assert_eq!(parsed.len(), 2);
+ assert_eq!(parsed[0].id, nodes[0].id);
+ assert_eq!(parsed[0].addr.port(), 8000);
+ assert_eq!(parsed[1].addr.port(), 9000);
+ }
+
+ // ── Truncated inputs ────────────────────────────
+
+ #[test]
+ fn parse_store_truncated() {
+ let buf = make_buf(MsgType::DhtStore, 2); // too small
+ assert!(matches!(parse_store(&buf), Err(Error::BufferTooSmall)));
+ }
+
+ #[test]
+ fn parse_find_node_truncated() {
+ let buf = make_buf(MsgType::DhtFindNode, 2);
+ assert!(matches!(parse_find_node(&buf), Err(Error::BufferTooSmall)));
+ }
+
+ #[test]
+ fn parse_find_value_truncated() {
+ let buf = make_buf(MsgType::DhtFindValue, 2);
+ assert!(matches!(parse_find_value(&buf), Err(Error::BufferTooSmall)));
+ }
+}