aboutsummaryrefslogtreecommitdiffstats
path: root/src/wire.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/wire.rs
downloadtesseras-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.rs368
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}");
+ }
+ }
+}