//! On-the-wire binary protocol. //! //! Defines message header, message types, and //! serialization for all protocol messages. Maintains //! the same field layout and byte order for //! structural reference. use crate::error::Error; use crate::id::{ID_LEN, NodeId}; // ── Protocol constants ────────────────────────────── /// Protocol magic number: 0x7E55 ("TESS"). pub const MAGIC_NUMBER: u16 = 0x7E55; /// Protocol version. pub const TESSERAS_DHT_VERSION: u8 = 0; /// Size of the message header in bytes. /// /// magic(2) + ver(1) + type(1) + len(2) + reserved(2) /// + src(32) + dst(32) = 72 pub const HEADER_SIZE: usize = 8 + ID_LEN * 2; /// Ed25519 signature appended to authenticated packets. pub const SIGNATURE_SIZE: usize = crate::crypto::SIGNATURE_SIZE; // ── Address domains ───────────────────────────────── pub const DOMAIN_LOOPBACK: u16 = 0; pub const DOMAIN_INET: u16 = 1; pub const DOMAIN_INET6: u16 = 2; // ── Node state on the wire ────────────────────────── pub const STATE_GLOBAL: u16 = 1; pub const STATE_NAT: u16 = 2; // ── Message types ─────────────────────────────────── #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum MsgType { // Datagram Dgram = 0x01, // Advertise Advertise = 0x02, AdvertiseReply = 0x03, // NAT detection NatEcho = 0x11, NatEchoReply = 0x12, NatEchoRedirect = 0x13, NatEchoRedirectReply = 0x14, // DTUN DtunPing = 0x21, DtunPingReply = 0x22, DtunFindNode = 0x23, DtunFindNodeReply = 0x24, DtunFindValue = 0x25, DtunFindValueReply = 0x26, DtunRegister = 0x27, DtunRequest = 0x28, DtunRequestBy = 0x29, DtunRequestReply = 0x2A, // DHT DhtPing = 0x41, DhtPingReply = 0x42, DhtFindNode = 0x43, DhtFindNodeReply = 0x44, DhtFindValue = 0x45, DhtFindValueReply = 0x46, DhtStore = 0x47, // Proxy ProxyRegister = 0x81, ProxyRegisterReply = 0x82, ProxyStore = 0x83, ProxyGet = 0x84, ProxyGetReply = 0x85, ProxyDgram = 0x86, ProxyDgramForwarded = 0x87, ProxyRdp = 0x88, ProxyRdpForwarded = 0x89, // RDP Rdp = 0x90, } impl MsgType { pub fn from_u8(v: u8) -> Result { match v { 0x01 => Ok(MsgType::Dgram), 0x02 => Ok(MsgType::Advertise), 0x03 => Ok(MsgType::AdvertiseReply), 0x11 => Ok(MsgType::NatEcho), 0x12 => Ok(MsgType::NatEchoReply), 0x13 => Ok(MsgType::NatEchoRedirect), 0x14 => Ok(MsgType::NatEchoRedirectReply), 0x21 => Ok(MsgType::DtunPing), 0x22 => Ok(MsgType::DtunPingReply), 0x23 => Ok(MsgType::DtunFindNode), 0x24 => Ok(MsgType::DtunFindNodeReply), 0x25 => Ok(MsgType::DtunFindValue), 0x26 => Ok(MsgType::DtunFindValueReply), 0x27 => Ok(MsgType::DtunRegister), 0x28 => Ok(MsgType::DtunRequest), 0x29 => Ok(MsgType::DtunRequestBy), 0x2A => Ok(MsgType::DtunRequestReply), 0x41 => Ok(MsgType::DhtPing), 0x42 => Ok(MsgType::DhtPingReply), 0x43 => Ok(MsgType::DhtFindNode), 0x44 => Ok(MsgType::DhtFindNodeReply), 0x45 => Ok(MsgType::DhtFindValue), 0x46 => Ok(MsgType::DhtFindValueReply), 0x47 => Ok(MsgType::DhtStore), 0x81 => Ok(MsgType::ProxyRegister), 0x82 => Ok(MsgType::ProxyRegisterReply), 0x83 => Ok(MsgType::ProxyStore), 0x84 => Ok(MsgType::ProxyGet), 0x85 => Ok(MsgType::ProxyGetReply), 0x86 => Ok(MsgType::ProxyDgram), 0x87 => Ok(MsgType::ProxyDgramForwarded), 0x88 => Ok(MsgType::ProxyRdp), 0x89 => Ok(MsgType::ProxyRdpForwarded), 0x90 => Ok(MsgType::Rdp), _ => Err(Error::UnknownMessageType(v)), } } } // ── Response flags ────────────────────────────────── pub const DATA_ARE_NODES: u8 = 0xa0; pub const DATA_ARE_VALUES: u8 = 0xa1; pub const DATA_ARE_NUL: u8 = 0xa2; pub const GET_BY_UDP: u8 = 0xb0; pub const GET_BY_RDP: u8 = 0xb1; pub const DHT_FLAG_UNIQUE: u8 = 0x01; pub const DHT_GET_NEXT: u8 = 0xc0; pub const PROXY_GET_SUCCESS: u8 = 0xd0; pub const PROXY_GET_FAIL: u8 = 0xd1; pub const PROXY_GET_NEXT: u8 = 0xd2; // ── Message header ────────────────────────────────── /// Parsed message header (48 bytes on the wire). #[derive(Debug, Clone)] pub struct MsgHeader { pub magic: u16, pub ver: u8, pub msg_type: MsgType, pub len: u16, pub src: NodeId, pub dst: NodeId, } impl MsgHeader { /// Parse a header from a byte buffer. pub fn parse(buf: &[u8]) -> Result { if buf.len() < HEADER_SIZE { return Err(Error::BufferTooSmall); } let magic = u16::from_be_bytes([buf[0], buf[1]]); if magic != MAGIC_NUMBER { return Err(Error::BadMagic(magic)); } let ver = buf[2]; if ver != TESSERAS_DHT_VERSION { return Err(Error::UnsupportedVersion(ver)); } let msg_type = MsgType::from_u8(buf[3])?; let len = u16::from_be_bytes([buf[4], buf[5]]); // buf[6..8] reserved let src = NodeId::read_from(&buf[8..8 + ID_LEN]); let dst = NodeId::read_from(&buf[8 + ID_LEN..8 + ID_LEN * 2]); Ok(Self { magic, ver, msg_type, len, src, dst, }) } /// Write header to a byte buffer. Returns bytes written /// (always HEADER_SIZE). pub fn write(&self, buf: &mut [u8]) -> Result { if buf.len() < HEADER_SIZE { return Err(Error::BufferTooSmall); } buf[0..2].copy_from_slice(&MAGIC_NUMBER.to_be_bytes()); buf[2] = TESSERAS_DHT_VERSION; buf[3] = self.msg_type as u8; buf[4..6].copy_from_slice(&self.len.to_be_bytes()); buf[6] = 0; // reserved buf[7] = 0; self.src.write_to(&mut buf[8..8 + ID_LEN]); self.dst.write_to(&mut buf[8 + ID_LEN..8 + ID_LEN * 2]); Ok(HEADER_SIZE) } /// Create a new header for sending. pub fn new( msg_type: MsgType, total_len: u16, src: NodeId, dst: NodeId, ) -> Self { Self { magic: MAGIC_NUMBER, ver: TESSERAS_DHT_VERSION, msg_type, len: total_len, src, dst, } } } /// Append Ed25519 signature to a packet buffer. /// /// Signs the entire buffer (header + body) using the /// sender's private key. Appends 64-byte signature. pub fn sign_packet(buf: &mut Vec, identity: &crate::crypto::Identity) { let sig = identity.sign(buf); buf.extend_from_slice(&sig); } /// Verify Ed25519 signature on a received packet. /// /// The last 64 bytes of `buf` are the signature. /// `sender_pubkey` is the sender's 32-byte Ed25519 /// public key. /// /// Returns `true` if the signature is valid. pub fn verify_packet( buf: &[u8], sender_pubkey: &[u8; crate::crypto::PUBLIC_KEY_SIZE], ) -> bool { if buf.len() < HEADER_SIZE + SIGNATURE_SIZE { return false; } let (data, sig) = buf.split_at(buf.len() - SIGNATURE_SIZE); crate::crypto::Identity::verify(sender_pubkey, data, sig) } #[cfg(test)] mod tests { use super::*; fn make_header() -> MsgHeader { MsgHeader::new( MsgType::DhtPing, 52, NodeId::from_bytes([0xAA; ID_LEN]), NodeId::from_bytes([0xBB; ID_LEN]), ) } #[test] fn header_roundtrip() { let hdr = make_header(); let mut buf = [0u8; HEADER_SIZE]; hdr.write(&mut buf).unwrap(); let parsed = MsgHeader::parse(&buf).unwrap(); assert_eq!(parsed.magic, MAGIC_NUMBER); assert_eq!(parsed.ver, TESSERAS_DHT_VERSION); assert_eq!(parsed.msg_type, MsgType::DhtPing); assert_eq!(parsed.len, 52); assert_eq!(parsed.src, hdr.src); assert_eq!(parsed.dst, hdr.dst); } #[test] fn reject_bad_magic() { let mut buf = [0u8; HEADER_SIZE]; buf[0] = 0xBA; buf[1] = 0xBE; // wrong magic let err = MsgHeader::parse(&buf); assert!(matches!(err, Err(Error::BadMagic(0xBABE)))); } #[test] fn reject_bad_version() { let hdr = make_header(); let mut buf = [0u8; HEADER_SIZE]; hdr.write(&mut buf).unwrap(); buf[2] = 99; // bad version let err = MsgHeader::parse(&buf); assert!(matches!(err, Err(Error::UnsupportedVersion(99)))); } #[test] fn reject_unknown_type() { let hdr = make_header(); let mut buf = [0u8; HEADER_SIZE]; hdr.write(&mut buf).unwrap(); buf[3] = 0xFF; // unknown type let err = MsgHeader::parse(&buf); assert!(matches!(err, Err(Error::UnknownMessageType(0xFF)))); } #[test] fn reject_truncated() { let err = MsgHeader::parse(&[0u8; 10]); assert!(matches!(err, Err(Error::BufferTooSmall))); } #[test] fn all_msg_types_roundtrip() { let types = [ MsgType::Dgram, MsgType::Advertise, MsgType::AdvertiseReply, MsgType::NatEcho, MsgType::NatEchoReply, MsgType::NatEchoRedirect, MsgType::NatEchoRedirectReply, MsgType::DtunPing, MsgType::DtunPingReply, MsgType::DtunFindNode, MsgType::DtunFindNodeReply, MsgType::DtunFindValue, MsgType::DtunFindValueReply, MsgType::DtunRegister, MsgType::DtunRequest, MsgType::DtunRequestBy, MsgType::DtunRequestReply, MsgType::DhtPing, MsgType::DhtPingReply, MsgType::DhtFindNode, MsgType::DhtFindNodeReply, MsgType::DhtFindValue, MsgType::DhtFindValueReply, MsgType::DhtStore, MsgType::ProxyRegister, MsgType::ProxyRegisterReply, MsgType::ProxyStore, MsgType::ProxyGet, MsgType::ProxyGetReply, MsgType::ProxyDgram, MsgType::ProxyDgramForwarded, MsgType::ProxyRdp, MsgType::ProxyRdpForwarded, MsgType::Rdp, ]; for &t in &types { let val = t as u8; let parsed = MsgType::from_u8(val).unwrap(); assert_eq!(parsed, t, "roundtrip failed for 0x{val:02x}"); } } }