diff options
| author | murilo ijanc | 2026-03-24 15:04:03 -0300 |
|---|---|---|
| committer | murilo ijanc | 2026-03-24 15:04:03 -0300 |
| commit | 9821aabf0b50d2487b07502d3d2cd89e7d62bdbe (patch) | |
| tree | 53da095ff90cc755bac3d4bf699172b5e8cd07d6 /src/wire.rs | |
| download | tesseras-dht-9821aabf0b50d2487b07502d3d2cd89e7d62bdbe.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/wire.rs')
| -rw-r--r-- | src/wire.rs | 368 |
1 files changed, 368 insertions, 0 deletions
diff --git a/src/wire.rs b/src/wire.rs new file mode 100644 index 0000000..3d80c3b --- /dev/null +++ b/src/wire.rs @@ -0,0 +1,368 @@ +//! 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<Self, Error> { + 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<Self, Error> { + 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<usize, Error> { + 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<u8>, 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}"); + } + } +} |