diff --git a/peeroxide-dht/src/blind_relay.rs b/peeroxide-dht/src/blind_relay.rs index 76f6449..b93b641 100644 --- a/peeroxide-dht/src/blind_relay.rs +++ b/peeroxide-dht/src/blind_relay.rs @@ -29,15 +29,20 @@ use tracing::debug; /// Pair message — requests relay pairing with a 32-byte token. #[derive(Debug, Clone, PartialEq)] pub struct PairMessage { + /// Indicates whether this peer initiated the pair request. pub is_initiator: bool, + /// Relay token used to match the pair and unpair messages. pub token: [u8; 32], + /// Stream identifier assigned by the local peer. pub id: u64, + /// Sequence number for the pair request. pub seq: u64, } /// Unpair message — cancels a relay pairing. #[derive(Debug, Clone, PartialEq)] pub struct UnpairMessage { + /// Relay token used to cancel a pending pair request. pub token: [u8; 32], } @@ -50,6 +55,7 @@ pub const MSG_TYPE_PAIR: u32 = 0; /// Protomux message type index for unpair. pub const MSG_TYPE_UNPAIR: u32 = 1; +/// Pre-encodes a [`PairMessage`], advancing the state cursor. pub fn preencode_pair(state: &mut State, msg: &PairMessage) { state.end += 1; // bitfield(7) = 1 byte state.end += 32; // fixed32 token @@ -57,6 +63,7 @@ pub fn preencode_pair(state: &mut State, msg: &PairMessage) { c::preencode_uint(state, msg.seq); } +/// Encodes a [`PairMessage`] into the state buffer. pub fn encode_pair(state: &mut State, msg: &PairMessage) { let flags: u8 = if msg.is_initiator { 1 } else { 0 }; c::encode_uint8(state, flags); @@ -65,6 +72,7 @@ pub fn encode_pair(state: &mut State, msg: &PairMessage) { c::encode_uint(state, msg.seq); } +/// Decodes a [`PairMessage`] from the state buffer. pub fn decode_pair(state: &mut State) -> c::Result { let flags = c::decode_uint8(state)?; let is_initiator = flags & 1 != 0; @@ -79,16 +87,19 @@ pub fn decode_pair(state: &mut State) -> c::Result { }) } +/// Pre-encodes a [`UnpairMessage`], advancing the state cursor. pub fn preencode_unpair(state: &mut State, _msg: &UnpairMessage) { state.end += 1; // bitfield(7) = 1 byte state.end += 32; // fixed32 token } +/// Encodes a [`UnpairMessage`] into the state buffer. pub fn encode_unpair(state: &mut State, msg: &UnpairMessage) { c::encode_uint8(state, 0); // flags = 0 c::encode_fixed32(state, &msg.token); } +/// Decodes a [`UnpairMessage`] from the state buffer. pub fn decode_unpair(state: &mut State) -> c::Result { let _flags = c::decode_uint8(state)?; let token = c::decode_fixed32(state)?; @@ -127,20 +138,26 @@ pub fn decode_unpair_from_slice(data: &[u8]) -> c::Result { // ── Client ─────────────────────────────────────────────────────────────────── +/// Errors that can occur while using the blind relay client. #[derive(Debug, Error)] pub enum RelayError { + /// A Protomux operation failed while opening, sending, or receiving. #[error("protomux error: {0}")] Protomux(#[from] protomux::ProtomuxError), + /// Encoding or decoding of relay messages failed. #[error("encoding error: {0}")] Encoding(#[from] c::EncodingError), + /// The channel closed before a matching pair response arrived. #[error("channel closed before pair response")] ChannelClosed, + /// The client was destroyed before the operation could complete. #[error("relay client destroyed")] Destroyed, + /// A pair request with this token is already in flight. #[error("already pairing with this token")] AlreadyPairing, } @@ -148,6 +165,7 @@ pub enum RelayError { /// Response from a successful relay pairing. #[derive(Debug, Clone)] pub struct PairResponse { + /// Relay-assigned remote stream identifier. pub remote_id: u64, } diff --git a/peeroxide-dht/src/compact_encoding.rs b/peeroxide-dht/src/compact_encoding.rs index 8cfea6f..4fe78a2 100644 --- a/peeroxide-dht/src/compact_encoding.rs +++ b/peeroxide-dht/src/compact_encoding.rs @@ -1,31 +1,54 @@ use thiserror::Error; +/// Errors returned by compact encoding operations #[derive(Error, Debug)] pub enum EncodingError { #[error("out of bounds: need {need} bytes, have {have}")] - OutOfBounds { need: usize, have: usize }, + /// The buffer did not contain enough bytes + OutOfBounds { + /// The number of bytes needed + need: usize, + /// The number of bytes available + have: usize, + }, #[error("incorrect buffer size: expected {expected}, got {got}")] - IncorrectBufferSize { expected: usize, got: usize }, + /// The input buffer did not match the expected size + IncorrectBufferSize { + /// The expected buffer length + expected: usize, + /// The actual buffer length + got: usize, + }, #[error("array too large: {0} elements (max 1048576)")] - ArrayTooLarge(usize), + /// The array length exceeds the supported maximum + ArrayTooLarge(#[doc = "The number of elements requested"] usize), #[error("invalid IP family: {0}")] - InvalidIpFamily(u8), + /// The encoded IP family tag was not recognized + InvalidIpFamily(#[doc = "The invalid family tag value"] u8), #[error("invalid IPv4 address: {0}")] - InvalidIpv4(String), + /// The provided IPv4 address was invalid + InvalidIpv4(#[doc = "The invalid IPv4 address string"] String), #[error("invalid IPv6 address: {0}")] - InvalidIpv6(String), + /// The provided IPv6 address was invalid + InvalidIpv6(#[doc = "The invalid IPv6 address string"] String), } +/// Result type used by compact encoding operations pub type Result = std::result::Result; +/// Encoding cursor and backing buffer state #[derive(Debug, Clone)] pub struct State { + /// The current read or write position pub start: usize, + /// The allocated end position for pre-encoded data pub end: usize, + /// The backing byte buffer pub buffer: Vec, } impl State { + /// Creates a new empty state pub fn new() -> Self { Self { start: 0, @@ -34,6 +57,7 @@ impl State { } } + /// Creates a state from an existing buffer pub fn from_buffer(buffer: &[u8]) -> Self { Self { start: 0, @@ -42,6 +66,7 @@ impl State { } } + /// Allocates a buffer based on pre-encoded size pub fn alloc(&mut self) { if self.buffer.len() < self.end { self.buffer.resize(self.end, 0); @@ -69,15 +94,18 @@ impl Default for State { } } +/// Pre-encodes a uint8 value, advancing the state cursor pub fn preencode_uint8(state: &mut State, _val: u8) { state.end += 1; } +/// Encodes a uint8 value into the state buffer pub fn encode_uint8(state: &mut State, val: u8) { state.buffer[state.start] = val; state.start += 1; } +/// Decodes a uint8 value from the state buffer pub fn decode_uint8(state: &mut State) -> Result { state.check_remaining(1)?; let val = state.buffer[state.start]; @@ -85,10 +113,12 @@ pub fn decode_uint8(state: &mut State) -> Result { Ok(val) } +/// Pre-encodes a uint16 value, advancing the state cursor pub fn preencode_uint16(state: &mut State, _val: u16) { state.end += 2; } +/// Encodes a uint16 value into the state buffer pub fn encode_uint16(state: &mut State, val: u16) { let bytes = val.to_le_bytes(); state.buffer[state.start] = bytes[0]; @@ -96,6 +126,7 @@ pub fn encode_uint16(state: &mut State, val: u16) { state.start += 2; } +/// Decodes a uint16 value from the state buffer pub fn decode_uint16(state: &mut State) -> Result { state.check_remaining(2)?; let val = u16::from_le_bytes([state.buffer[state.start], state.buffer[state.start + 1]]); @@ -103,10 +134,12 @@ pub fn decode_uint16(state: &mut State) -> Result { Ok(val) } +/// Pre-encodes a uint24 value, advancing the state cursor pub fn preencode_uint24(state: &mut State, _val: u32) { state.end += 3; } +/// Encodes a uint24 value into the state buffer pub fn encode_uint24(state: &mut State, val: u32) { state.buffer[state.start] = val as u8; state.buffer[state.start + 1] = (val >> 8) as u8; @@ -114,6 +147,7 @@ pub fn encode_uint24(state: &mut State, val: u32) { state.start += 3; } +/// Decodes a uint24 value from the state buffer pub fn decode_uint24(state: &mut State) -> Result { state.check_remaining(3)?; let val = state.buffer[state.start] as u32 @@ -123,16 +157,19 @@ pub fn decode_uint24(state: &mut State) -> Result { Ok(val) } +/// Pre-encodes a uint32 value, advancing the state cursor pub fn preencode_uint32(state: &mut State, _val: u32) { state.end += 4; } +/// Encodes a uint32 value into the state buffer pub fn encode_uint32(state: &mut State, val: u32) { let bytes = val.to_le_bytes(); state.buffer[state.start..state.start + 4].copy_from_slice(&bytes); state.start += 4; } +/// Decodes a uint32 value from the state buffer pub fn decode_uint32(state: &mut State) -> Result { state.check_remaining(4)?; let val = u32::from_le_bytes([ @@ -145,15 +182,18 @@ pub fn decode_uint32(state: &mut State) -> Result { Ok(val) } +/// Pre-encodes a uint40 value, advancing the state cursor pub fn preencode_uint40(state: &mut State, _val: u64) { state.end += 5; } +/// Encodes a uint40 value into the state buffer pub fn encode_uint40(state: &mut State, val: u64) { encode_uint8(state, val as u8); encode_uint32(state, (val >> 8) as u32); } +/// Decodes a uint40 value from the state buffer pub fn decode_uint40(state: &mut State) -> Result { state.check_remaining(5)?; let lo = decode_uint8(state)? as u64; @@ -161,15 +201,18 @@ pub fn decode_uint40(state: &mut State) -> Result { Ok(lo | (hi << 8)) } +/// Pre-encodes a uint48 value, advancing the state cursor pub fn preencode_uint48(state: &mut State, _val: u64) { state.end += 6; } +/// Encodes a uint48 value into the state buffer pub fn encode_uint48(state: &mut State, val: u64) { encode_uint16(state, val as u16); encode_uint32(state, (val >> 16) as u32); } +/// Decodes a uint48 value from the state buffer pub fn decode_uint48(state: &mut State) -> Result { state.check_remaining(6)?; let lo = decode_uint16(state)? as u64; @@ -177,15 +220,18 @@ pub fn decode_uint48(state: &mut State) -> Result { Ok(lo | (hi << 16)) } +/// Pre-encodes a uint56 value, advancing the state cursor pub fn preencode_uint56(state: &mut State, _val: u64) { state.end += 7; } +/// Encodes a uint56 value into the state buffer pub fn encode_uint56(state: &mut State, val: u64) { encode_uint24(state, val as u32 & 0xFFFFFF); encode_uint32(state, (val >> 24) as u32); } +/// Decodes a uint56 value from the state buffer pub fn decode_uint56(state: &mut State) -> Result { state.check_remaining(7)?; let lo = decode_uint24(state)? as u64; @@ -193,10 +239,12 @@ pub fn decode_uint56(state: &mut State) -> Result { Ok(lo | (hi << 24)) } +/// Pre-encodes a uint64 value, advancing the state cursor pub fn preencode_uint64(state: &mut State, _val: u64) { state.end += 8; } +/// Encodes a uint64 value into the state buffer pub fn encode_uint64(state: &mut State, val: u64) { let lo = val as u32; let hi = (val >> 32) as u32; @@ -204,6 +252,7 @@ pub fn encode_uint64(state: &mut State, val: u64) { encode_uint32(state, hi); } +/// Decodes a uint64 value from the state buffer pub fn decode_uint64(state: &mut State) -> Result { state.check_remaining(8)?; let lo = decode_uint32(state)? as u64; @@ -211,6 +260,7 @@ pub fn decode_uint64(state: &mut State) -> Result { Ok(lo | (hi << 32)) } +/// Pre-encodes a uint value, advancing the state cursor pub fn preencode_uint(state: &mut State, n: u64) { if n <= 0xfc { state.end += 1; @@ -223,6 +273,7 @@ pub fn preencode_uint(state: &mut State, n: u64) { } } +/// Encodes a uint value into the state buffer pub fn encode_uint(state: &mut State, n: u64) { if n <= 0xfc { encode_uint8(state, n as u8); @@ -241,6 +292,7 @@ pub fn encode_uint(state: &mut State, n: u64) { } } +/// Decodes a uint value from the state buffer pub fn decode_uint(state: &mut State) -> Result { let a = decode_uint8(state)?; if a <= 0xfc { @@ -263,80 +315,98 @@ fn zigzag_decode(n: u64) -> i64 { ((n >> 1) as i64) ^ -((n & 1) as i64) } +/// Pre-encodes an int value, advancing the state cursor pub fn preencode_int(state: &mut State, n: i64) { preencode_uint(state, zigzag_encode(n)); } +/// Encodes an int value into the state buffer pub fn encode_int(state: &mut State, n: i64) { encode_uint(state, zigzag_encode(n)); } +/// Decodes an int value from the state buffer pub fn decode_int(state: &mut State) -> Result { Ok(zigzag_decode(decode_uint(state)?)) } +/// Pre-encodes an int8 value, advancing the state cursor pub fn preencode_int8(state: &mut State, _val: i8) { state.end += 1; } +/// Encodes an int8 value into the state buffer pub fn encode_int8(state: &mut State, val: i8) { let z = zigzag_encode(val as i64); encode_uint8(state, z as u8); } +/// Decodes an int8 value from the state buffer pub fn decode_int8(state: &mut State) -> Result { Ok(zigzag_decode(decode_uint8(state)? as u64) as i8) } +/// Pre-encodes an int16 value, advancing the state cursor pub fn preencode_int16(state: &mut State, _val: i16) { state.end += 2; } +/// Encodes an int16 value into the state buffer pub fn encode_int16(state: &mut State, val: i16) { let z = zigzag_encode(val as i64); encode_uint16(state, z as u16); } +/// Decodes an int16 value from the state buffer pub fn decode_int16(state: &mut State) -> Result { Ok(zigzag_decode(decode_uint16(state)? as u64) as i16) } +/// Pre-encodes an int32 value, advancing the state cursor pub fn preencode_int32(state: &mut State, _val: i32) { state.end += 4; } +/// Encodes an int32 value into the state buffer pub fn encode_int32(state: &mut State, val: i32) { let z = zigzag_encode(val as i64); encode_uint32(state, z as u32); } +/// Decodes an int32 value from the state buffer pub fn decode_int32(state: &mut State) -> Result { Ok(zigzag_decode(decode_uint32(state)? as u64) as i32) } +/// Pre-encodes an int64 value, advancing the state cursor pub fn preencode_int64(state: &mut State, _val: i64) { state.end += 8; } +/// Encodes an int64 value into the state buffer pub fn encode_int64(state: &mut State, val: i64) { let z = zigzag_encode(val); encode_uint64(state, z); } +/// Decodes an int64 value from the state buffer pub fn decode_int64(state: &mut State) -> Result { Ok(zigzag_decode(decode_uint64(state)?)) } +/// Pre-encodes a float32 value, advancing the state cursor pub fn preencode_float32(state: &mut State, _val: f32) { state.end += 4; } +/// Encodes a float32 value into the state buffer pub fn encode_float32(state: &mut State, val: f32) { let bytes = val.to_le_bytes(); state.buffer[state.start..state.start + 4].copy_from_slice(&bytes); state.start += 4; } +/// Decodes a float32 value from the state buffer pub fn decode_float32(state: &mut State) -> Result { state.check_remaining(4)?; let val = f32::from_le_bytes([ @@ -349,16 +419,19 @@ pub fn decode_float32(state: &mut State) -> Result { Ok(val) } +/// Pre-encodes a float64 value, advancing the state cursor pub fn preencode_float64(state: &mut State, _val: f64) { state.end += 8; } +/// Encodes a float64 value into the state buffer pub fn encode_float64(state: &mut State, val: f64) { let bytes = val.to_le_bytes(); state.buffer[state.start..state.start + 8].copy_from_slice(&bytes); state.start += 8; } +/// Decodes a float64 value from the state buffer pub fn decode_float64(state: &mut State) -> Result { state.check_remaining(8)?; let val = f64::from_le_bytes([ @@ -375,18 +448,22 @@ pub fn decode_float64(state: &mut State) -> Result { Ok(val) } +/// Pre-encodes a bool value, advancing the state cursor pub fn preencode_bool(state: &mut State, _val: bool) { state.end += 1; } +/// Encodes a bool value into the state buffer pub fn encode_bool(state: &mut State, val: bool) { encode_uint8(state, if val { 1 } else { 0 }); } +/// Decodes a bool value from the state buffer pub fn decode_bool(state: &mut State) -> Result { Ok(decode_uint8(state)? != 0) } +/// Pre-encodes an optional buffer, advancing the state cursor pub fn preencode_buffer(state: &mut State, buf: Option<&[u8]>) { match buf { Some(b) => { @@ -397,6 +474,7 @@ pub fn preencode_buffer(state: &mut State, buf: Option<&[u8]>) { } } +/// Encodes an optional buffer into the state buffer pub fn encode_buffer(state: &mut State, buf: Option<&[u8]>) { match buf { Some(b) => { @@ -411,6 +489,7 @@ pub fn encode_buffer(state: &mut State, buf: Option<&[u8]>) { } } +/// Decodes an optional buffer from the state buffer pub fn decode_buffer(state: &mut State) -> Result>> { let len = decode_uint(state)? as usize; if len == 0 { @@ -422,12 +501,14 @@ pub fn decode_buffer(state: &mut State) -> Result>> { Ok(Some(val)) } +/// Pre-encodes a string value, advancing the state cursor pub fn preencode_string(state: &mut State, s: &str) { let len = s.len(); preencode_uint(state, len as u64); state.end += len; } +/// Encodes a string value into the state buffer pub fn encode_string(state: &mut State, s: &str) { let bytes = s.as_bytes(); encode_uint(state, bytes.len() as u64); @@ -435,6 +516,7 @@ pub fn encode_string(state: &mut State, s: &str) { state.start += bytes.len(); } +/// Decodes a string value from the state buffer pub fn decode_string(state: &mut State) -> Result { let len = decode_uint(state)? as usize; state.check_remaining(len)?; @@ -443,6 +525,7 @@ pub fn decode_string(state: &mut State) -> Result { Ok(val) } +/// Pre-encodes a fixed-length buffer, advancing the state cursor pub fn preencode_fixed(state: &mut State, n: usize, buf: &[u8]) -> Result<()> { if buf.len() != n { return Err(EncodingError::IncorrectBufferSize { @@ -454,11 +537,13 @@ pub fn preencode_fixed(state: &mut State, n: usize, buf: &[u8]) -> Result<()> { Ok(()) } +/// Encodes a fixed-length buffer into the state buffer pub fn encode_fixed(state: &mut State, buf: &[u8]) { state.buffer[state.start..state.start + buf.len()].copy_from_slice(buf); state.start += buf.len(); } +/// Decodes a fixed-length buffer from the state buffer pub fn decode_fixed(state: &mut State, n: usize) -> Result> { state.check_remaining(n)?; let val = state.buffer[state.start..state.start + n].to_vec(); @@ -466,14 +551,17 @@ pub fn decode_fixed(state: &mut State, n: usize) -> Result> { Ok(val) } +/// Pre-encodes a 32-byte fixed buffer, advancing the state cursor pub fn preencode_fixed32(state: &mut State, buf: &[u8; 32]) -> Result<()> { preencode_fixed(state, 32, buf) } +/// Encodes a 32-byte fixed buffer into the state buffer pub fn encode_fixed32(state: &mut State, buf: &[u8; 32]) { encode_fixed(state, buf); } +/// Decodes a 32-byte fixed buffer from the state buffer pub fn decode_fixed32(state: &mut State) -> Result<[u8; 32]> { state.check_remaining(32)?; let mut val = [0u8; 32]; @@ -482,14 +570,17 @@ pub fn decode_fixed32(state: &mut State) -> Result<[u8; 32]> { Ok(val) } +/// Pre-encodes a 64-byte fixed buffer, advancing the state cursor pub fn preencode_fixed64(state: &mut State, buf: &[u8; 64]) -> Result<()> { preencode_fixed(state, 64, buf) } +/// Encodes a 64-byte fixed buffer into the state buffer pub fn encode_fixed64(state: &mut State, buf: &[u8; 64]) { encode_fixed(state, buf); } +/// Decodes a 64-byte fixed buffer from the state buffer pub fn decode_fixed64(state: &mut State) -> Result<[u8; 64]> { state.check_remaining(64)?; let mut val = [0u8; 64]; @@ -498,10 +589,12 @@ pub fn decode_fixed64(state: &mut State) -> Result<[u8; 64]> { Ok(val) } +/// Pre-encodes an IPv4 address, advancing the state cursor pub fn preencode_ipv4(state: &mut State, _addr: &str) { state.end += 4; } +/// Encodes an IPv4 address into the state buffer pub fn encode_ipv4(state: &mut State, addr: &str) -> Result<()> { let parts: Vec<&str> = addr.split('.').collect(); if parts.len() != 4 { @@ -517,6 +610,7 @@ pub fn encode_ipv4(state: &mut State, addr: &str) -> Result<()> { Ok(()) } +/// Decodes an IPv4 address from the state buffer pub fn decode_ipv4(state: &mut State) -> Result { state.check_remaining(4)?; let addr = format!( @@ -530,10 +624,12 @@ pub fn decode_ipv4(state: &mut State) -> Result { Ok(addr) } +/// Pre-encodes an IPv6 address, advancing the state cursor pub fn preencode_ipv6(state: &mut State, _addr: &str) { state.end += 16; } +/// Encodes an IPv6 address into the state buffer pub fn encode_ipv6(state: &mut State, addr: &str) -> Result<()> { let parsed: std::net::Ipv6Addr = addr .parse() @@ -544,6 +640,7 @@ pub fn encode_ipv6(state: &mut State, addr: &str) -> Result<()> { Ok(()) } +/// Decodes an IPv6 address from the state buffer pub fn decode_ipv6(state: &mut State) -> Result { state.check_remaining(16)?; let mut octets = [0u8; 16]; @@ -556,6 +653,7 @@ pub fn decode_ipv6(state: &mut State) -> Result { const IP_FAMILY_V4: u8 = 4; const IP_FAMILY_V6: u8 = 6; +/// Pre-encodes an IP address, advancing the state cursor pub fn preencode_ip(state: &mut State, addr: &str) { if addr.contains(':') { preencode_uint8(state, IP_FAMILY_V6); @@ -566,6 +664,7 @@ pub fn preencode_ip(state: &mut State, addr: &str) { } } +/// Encodes an IP address into the state buffer pub fn encode_ip(state: &mut State, addr: &str) -> Result<()> { if addr.contains(':') { encode_uint8(state, IP_FAMILY_V6); @@ -576,6 +675,7 @@ pub fn encode_ip(state: &mut State, addr: &str) -> Result<()> { } } +/// Decodes an IP address from the state buffer pub fn decode_ip(state: &mut State) -> Result { let family = decode_uint8(state)?; match family { @@ -585,34 +685,40 @@ pub fn decode_ip(state: &mut State) -> Result { } } +/// Pre-encodes an IPv4 address and port, advancing the state cursor pub fn preencode_ipv4_address(state: &mut State, addr: &str, _port: u16) { preencode_ipv4(state, addr); state.end += 2; } +/// Encodes an IPv4 address and port into the state buffer pub fn encode_ipv4_address(state: &mut State, addr: &str, port: u16) -> Result<()> { encode_ipv4(state, addr)?; encode_uint16(state, port); Ok(()) } +/// Decodes an IPv4 address and port from the state buffer pub fn decode_ipv4_address(state: &mut State) -> Result<(String, u16)> { let addr = decode_ipv4(state)?; let port = decode_uint16(state)?; Ok((addr, port)) } +/// Pre-encodes an IPv6 address and port, advancing the state cursor pub fn preencode_ipv6_address(state: &mut State, addr: &str, _port: u16) { preencode_ipv6(state, addr); state.end += 2; } +/// Encodes an IPv6 address and port into the state buffer pub fn encode_ipv6_address(state: &mut State, addr: &str, port: u16) -> Result<()> { encode_ipv6(state, addr)?; encode_uint16(state, port); Ok(()) } +/// Decodes an IPv6 address and port from the state buffer pub fn decode_ipv6_address(state: &mut State) -> Result<(String, u16)> { let addr = decode_ipv6(state)?; let port = decode_uint16(state)?; @@ -621,6 +727,7 @@ pub fn decode_ipv6_address(state: &mut State) -> Result<(String, u16)> { const MAX_ARRAY_LENGTH: usize = 0x100000; +/// Pre-encodes a uint array, advancing the state cursor pub fn preencode_uint_array(state: &mut State, arr: &[u64]) { preencode_uint(state, arr.len() as u64); for &val in arr { @@ -628,6 +735,7 @@ pub fn preencode_uint_array(state: &mut State, arr: &[u64]) { } } +/// Encodes a uint array into the state buffer pub fn encode_uint_array(state: &mut State, arr: &[u64]) { encode_uint(state, arr.len() as u64); for &val in arr { @@ -635,6 +743,7 @@ pub fn encode_uint_array(state: &mut State, arr: &[u64]) { } } +/// Decodes a uint array from the state buffer pub fn decode_uint_array(state: &mut State) -> Result> { let len = decode_uint(state)? as usize; if len > MAX_ARRAY_LENGTH { @@ -647,6 +756,7 @@ pub fn decode_uint_array(state: &mut State) -> Result> { Ok(arr) } +/// Pre-encodes a buffer array, advancing the state cursor pub fn preencode_buffer_array(state: &mut State, arr: &[Option<&[u8]>]) { preencode_uint(state, arr.len() as u64); for buf in arr { @@ -654,6 +764,7 @@ pub fn preencode_buffer_array(state: &mut State, arr: &[Option<&[u8]>]) { } } +/// Encodes a buffer array into the state buffer pub fn encode_buffer_array(state: &mut State, arr: &[Option<&[u8]>]) { encode_uint(state, arr.len() as u64); for buf in arr { @@ -661,6 +772,7 @@ pub fn encode_buffer_array(state: &mut State, arr: &[Option<&[u8]>]) { } } +/// Decodes a buffer array from the state buffer pub fn decode_buffer_array(state: &mut State) -> Result>>> { let len = decode_uint(state)? as usize; if len > MAX_ARRAY_LENGTH { @@ -673,6 +785,7 @@ pub fn decode_buffer_array(state: &mut State) -> Result>>> { Ok(arr) } +/// Pre-encodes a string array, advancing the state cursor pub fn preencode_string_array(state: &mut State, arr: &[&str]) { preencode_uint(state, arr.len() as u64); for s in arr { @@ -680,6 +793,7 @@ pub fn preencode_string_array(state: &mut State, arr: &[&str]) { } } +/// Encodes a string array into the state buffer pub fn encode_string_array(state: &mut State, arr: &[&str]) { encode_uint(state, arr.len() as u64); for s in arr { @@ -687,6 +801,7 @@ pub fn encode_string_array(state: &mut State, arr: &[&str]) { } } +/// Decodes a string array from the state buffer pub fn decode_string_array(state: &mut State) -> Result> { let len = decode_uint(state)? as usize; if len > MAX_ARRAY_LENGTH { diff --git a/peeroxide-dht/src/crypto.rs b/peeroxide-dht/src/crypto.rs index 6bdc55e..4ce1243 100644 --- a/peeroxide-dht/src/crypto.rs +++ b/peeroxide-dht/src/crypto.rs @@ -9,6 +9,7 @@ type Blake2bMac256 = Blake2bMac; // ── BLAKE2b primitives ────────────────────────────────────────────────────── +/// Computes a BLAKE2b-256 hash of the given data. pub fn hash(data: &[u8]) -> [u8; 32] { let output = Blake2b256::digest(data); let mut result = [0u8; 32]; @@ -16,6 +17,7 @@ pub fn hash(data: &[u8]) -> [u8; 32] { result } +/// Computes a BLAKE2b-256 hash over multiple byte slices concatenated together. pub fn hash_batch(parts: &[&[u8]]) -> [u8; 32] { let mut h = Blake2b256::new(); for part in parts { @@ -60,14 +62,19 @@ pub fn namespace(name: &str, ids: &[u8]) -> Vec<[u8; 32]> { result } +/// Precomputed namespace hash for announce operations. pub static NS_ANNOUNCE: LazyLock<[u8; 32]> = LazyLock::new(|| namespace("hyperswarm/dht", &[4])[0]); +/// Precomputed namespace hash for unannounce operations. pub static NS_UNANNOUNCE: LazyLock<[u8; 32]> = LazyLock::new(|| namespace("hyperswarm/dht", &[5])[0]); +/// Precomputed namespace hash for mutable put operations. pub static NS_MUTABLE_PUT: LazyLock<[u8; 32]> = LazyLock::new(|| namespace("hyperswarm/dht", &[6])[0]); +/// Precomputed namespace hash for peer handshake operations. pub static NS_PEER_HANDSHAKE: LazyLock<[u8; 32]> = LazyLock::new(|| namespace("hyperswarm/dht", &[0])[0]); +/// Precomputed namespace hash for peer holepunch operations. pub static NS_PEER_HOLEPUNCH: LazyLock<[u8; 32]> = LazyLock::new(|| namespace("hyperswarm/dht", &[1])[0]); @@ -81,6 +88,7 @@ pub fn sign_detached(message: &[u8], secret_key: &[u8; 64]) -> [u8; 64] { signing_key.sign(message).to_bytes() } +/// Verifies an Ed25519 detached signature against the given message and public key. pub fn verify_detached(signature: &[u8; 64], message: &[u8], public_key: &[u8; 32]) -> bool { let Ok(verifying_key) = VerifyingKey::from_bytes(public_key) else { return false; diff --git a/peeroxide-dht/src/hyperdht.rs b/peeroxide-dht/src/hyperdht.rs index 644bac9..398bffd 100644 --- a/peeroxide-dht/src/hyperdht.rs +++ b/peeroxide-dht/src/hyperdht.rs @@ -67,45 +67,66 @@ fn is_addr_private(host: &str) -> bool { } #[derive(Debug, Error)] +/// Errors returned by HyperDHT operations. pub enum HyperDhtError { + /// Error propagated from the underlying DHT client. #[error("DHT error: {0}")] Dht(#[from] DhtError), + /// Error while encoding or decoding protocol data. #[error("encoding error: {0}")] Encoding(#[from] crate::compact_encoding::EncodingError), + /// Error from Noise handshake or session setup. #[error("noise error: {0}")] Noise(#[from] crate::noise::NoiseError), + /// Error from the Noise wrapper layer. #[error("noise wrap error: {0}")] NoiseWrap(#[from] crate::noise_wrap::NoiseWrapError), + /// Error from the router state machine. #[error("router error: {0}")] Router(#[from] crate::router::RouterError), + /// Error while wrapping or unwrapping secure payloads. #[error("secure payload error: {0}")] SecurePayload(#[from] crate::secure_payload::SecurePayloadError), + /// This DHT instance has been destroyed. #[error("node destroyed")] Destroyed, + /// A signature did not verify. #[error("invalid signature")] InvalidSignature, + /// A content hash did not match. #[error("invalid hash")] InvalidHash, + /// The internal channel was closed. #[error("channel closed")] ChannelClosed, + /// No peer was found for the requested target. #[error("peer not found")] PeerNotFound, + /// No relay nodes were available for the operation. #[error("no relay nodes available")] NoRelayNodes, + /// The handshake failed with the given message. #[error("handshake failed: {0}")] HandshakeFailed(String), + /// Hole punching did not succeed. #[error("holepunch failed")] HolepunchFailed, + /// Hole punching was aborted by the remote side. #[error("holepunch aborted")] HolepunchAborted, + /// The remote firewall rejected the connection. #[error("firewall rejected")] FirewallRejected, + /// Error from the UDX transport layer. #[error("UDX error: {0}")] Udx(#[from] libudx::UdxError), + /// Error from the secret stream layer. #[error("secret stream error: {0}")] SecretStream(#[from] SecretStreamError), + /// Failed to establish a UDX stream. #[error("stream establishment failed: {0}")] StreamEstablishment(String), + /// Error from the relay subsystem. #[error("relay error: {0}")] Relay(#[from] RelayError), } @@ -113,37 +134,53 @@ pub enum HyperDhtError { // ── Server events (forwarded to listen() subscribers) ──────────────────────── #[derive(Debug)] +/// Events forwarded to server-side listeners. pub enum ServerEvent { + /// A peer handshake request that may need local server handling. PeerHandshake { + /// The decoded handshake message. msg: HandshakeMessage, + /// Address of the peer that sent the request. from: Ipv4Peer, + /// Optional DHT target associated with the request. target: Option, + /// Reply channel for the generated response. reply_tx: oneshot::Sender>>, }, + /// A peer holepunch request that may need local server handling. PeerHolepunch { + /// The decoded holepunch message. msg: HolepunchMessage, + /// Address of the peer that sent the request. from: Ipv4Peer, + /// Address of the peer we should punch toward. peer_address: Ipv4Peer, + /// Optional DHT target associated with the request. target: Option, + /// Reply channel for the generated response. reply_tx: oneshot::Sender>>, }, } // ── KeyPair ─────────────────────────────────────────────────────────────────── -/// An Ed25519 key pair (libsodium layout: seed‖public_key). #[derive(Clone)] +/// An Ed25519 key pair (libsodium layout: seed‖public_key). pub struct KeyPair { + /// The 32-byte public key. pub public_key: [u8; 32], + /// The 64-byte secret key in libsodium layout. pub secret_key: [u8; 64], } impl KeyPair { + /// Generate a new random key pair. pub fn generate() -> Self { let seed: [u8; 32] = random(); Self::from_seed(seed) } + /// Derive a deterministic key pair from a 32-byte seed. pub fn from_seed(seed: [u8; 32]) -> Self { let signing_key = SigningKey::from_bytes(&seed); let pk: [u8; 32] = signing_key.verifying_key().to_bytes(); @@ -177,47 +214,74 @@ impl KeyPair { // ── Result types ───────────────────────────────────────────────────────────── #[derive(Debug, Clone)] +/// Result from a LOOKUP query. pub struct LookupResult { + /// Node that returned the lookup result. pub from: Ipv4Peer, + /// Optional intermediate hop used to reach the node. pub to: Option, + /// Peers advertised by the node. pub peers: Vec, } #[derive(Debug, Clone)] +/// Result from an ANNOUNCE operation. pub struct AnnounceResult { + /// Closest nodes contacted during the announce. pub closest_nodes: Vec, } #[derive(Debug, Clone)] +/// Result from an immutable put operation. pub struct ImmutablePutResult { + /// Content hash used as the target key. pub hash: [u8; 32], + /// Closest nodes contacted during the write. pub closest_nodes: Vec, } #[derive(Debug, Clone)] +/// Result from a mutable put operation. pub struct MutablePutResult { + /// Public key used as the mutable record key. pub public_key: [u8; 32], + /// Closest nodes contacted during the write. pub closest_nodes: Vec, + /// Record sequence number that was written. pub seq: u64, + /// Signature over the stored value. pub signature: [u8; 64], } #[derive(Debug, Clone)] +/// Result from a mutable get operation. pub struct MutableGetResult { + /// Retrieved value bytes. pub value: Vec, + /// Sequence number attached to the value. pub seq: u64, + /// Signature verifying the value. pub signature: [u8; 64], + /// Node that returned the value. pub from: Ipv4Peer, } #[derive(Debug, Clone)] +/// Metadata needed to establish a peer connection. pub struct ConnectResult { + /// Remote peer's public key. pub remote_public_key: [u8; 32], + /// Address used to reach the server during handshake. pub server_address: Ipv4Peer, + /// Address of the client-side peer endpoint. pub client_address: Ipv4Peer, + /// Whether the connection was relayed through a third party. pub is_relayed: bool, + /// Final Noise state and negotiated keys. pub noise: NoiseWrapResult, + /// Local UDX stream id to use for the connection. pub local_stream_id: u32, + /// Remote UDX metadata advertised by the peer. pub remote_udx: Option, } @@ -226,7 +290,9 @@ pub struct ConnectResult { /// Wraps a [`SecretStream`] over a UDX transport, keeping the underlying /// socket alive for the connection's lifetime. pub struct PeerConnection { + /// Encrypted bidirectional stream to the peer. pub stream: SecretStream, + /// Remote peer's public key. pub remote_public_key: [u8; 32], /// Remote peer's network address (used by server-side relay to connect data streams). pub remote_addr: Option, @@ -282,8 +348,11 @@ impl fmt::Debug for PeerConnection { } } +/// Configuration used by the server-side handshake and holepunch handler. pub struct ServerConfig { + /// Server identity key pair. pub key_pair: KeyPair, + /// Firewall mode advertised to connecting peers. pub firewall: u64, } @@ -302,8 +371,11 @@ pub const DEFAULT_BOOTSTRAP: [&str; 3] = [ // ── Config ──────────────────────────────────────────────────────────────────── #[derive(Debug, Clone, Default)] +/// Configuration for a HyperDHT instance. pub struct HyperDhtConfig { + /// DHT transport and bootstrap settings. pub dht: DhtConfig, + /// Persistent storage settings for stored records. pub persistent: PersistentConfig, } @@ -327,6 +399,7 @@ impl HyperDhtConfig { // ── HyperDhtHandle ──────────────────────────────────────────────────────────── #[derive(Clone)] +/// Main public HyperDHT API handle. pub struct HyperDhtHandle { dht: DhtHandle, router: Arc>, @@ -336,6 +409,7 @@ pub struct HyperDhtHandle { impl HyperDhtHandle { // ── LOOKUP ──────────────────────────────────────────────────────────────── + /// Query the DHT for peers advertising the target. pub async fn lookup(&self, target: [u8; 32]) -> Result, HyperDhtError> { let replies = self .dht @@ -367,6 +441,7 @@ impl HyperDhtHandle { // ── ANNOUNCE ───────────────────────────────────────────────────────────── + /// Announce this peer under the given target. pub async fn announce( &self, target: [u8; 32], @@ -440,6 +515,7 @@ impl HyperDhtHandle { // ── FIND_PEER ───────────────────────────────────────────────────────────── + /// Return the first peer record found for the target. pub async fn find_peer( &self, target: [u8; 32], @@ -488,6 +564,7 @@ impl HyperDhtHandle { // ── UNANNOUNCE ──────────────────────────────────────────────────────────── + /// Remove a previously announced peer record. pub async fn unannounce( &self, target: [u8; 32], @@ -557,6 +634,7 @@ impl HyperDhtHandle { // ── IMMUTABLE_PUT ──────────────────────────────────────────────────────── + /// Store immutable content under its content hash. pub async fn immutable_put( &self, value: &[u8], @@ -607,6 +685,7 @@ impl HyperDhtHandle { // ── IMMUTABLE_GET ──────────────────────────────────────────────────────── + /// Fetch immutable content by content hash. pub async fn immutable_get( &self, target: [u8; 32], @@ -634,6 +713,7 @@ impl HyperDhtHandle { // ── MUTABLE_PUT ─────────────────────────────────────────────────────────── + /// Store a signed mutable record for the given key pair. pub async fn mutable_put( &self, key_pair: &KeyPair, @@ -700,6 +780,7 @@ impl HyperDhtHandle { // ── MUTABLE_GET ─────────────────────────────────────────────────────────── + /// Fetch and verify a mutable record for the given public key. pub async fn mutable_get( &self, public_key: &[u8; 32], @@ -740,10 +821,12 @@ impl HyperDhtHandle { Ok(None) } + /// Wait until the DHT is bootstrapped. pub async fn bootstrapped(&self) -> Result<(), HyperDhtError> { self.dht.bootstrapped().await.map_err(HyperDhtError::Dht) } + /// Destroy the underlying DHT instance. pub async fn destroy(&self) -> Result<(), HyperDhtError> { self.dht.destroy().await.map_err(HyperDhtError::Dht) } @@ -758,14 +841,17 @@ impl HyperDhtHandle { self.dht.local_port().await.map_err(HyperDhtError::Dht) } + /// Access the shared router state. pub fn router(&self) -> &Arc> { &self.router } + /// Access the underlying DHT handle. pub fn dht(&self) -> &DhtHandle { &self.dht } + /// Mark a target as having a local server available. pub fn register_server(&self, target: &[u8; 32]) { if let Ok(mut router) = self.router.lock() { router.set( @@ -779,18 +865,21 @@ impl HyperDhtHandle { } } + /// Remove the local-server marker for a target. pub fn unregister_server(&self, target: &[u8; 32]) { if let Ok(mut router) = self.router.lock() { router.delete(target); } } + /// Access the server event sender. pub fn server_sender(&self) -> &mpsc::UnboundedSender { &self.server_tx } // ── CONNECT (client-side holepunch orchestration) ───────────────────── + /// Connect to a remote peer using the DHT and relay fallback. pub async fn connect( &self, key_pair: &KeyPair, @@ -1411,7 +1500,9 @@ pub async fn establish_stream( // ── Server-side event handler ───────────────────────────────────────────────── +/// Per-server state for pending handshake and holepunch exchanges. pub struct ServerSession { + /// Cached holepunch secrets indexed by remote public key. holepunch_secrets: std::collections::HashMap<[u8; 32], ServerPeerState>, } @@ -1424,6 +1515,7 @@ struct ServerPeerState { remote_udx: Option, } +/// Run the server-side request loop for peer handshakes and holepunches. pub async fn run_server( mut event_rx: mpsc::UnboundedReceiver, config: ServerConfig, @@ -1622,6 +1714,7 @@ async fn handle_server_holepunch( // ── Spawn ───────────────────────────────────────────────────────────────────── +/// Create a HyperDHT instance and start its background tasks. pub async fn spawn( runtime: &UdxRuntime, config: HyperDhtConfig, diff --git a/peeroxide-dht/src/hyperdht_messages.rs b/peeroxide-dht/src/hyperdht_messages.rs index 2afa195..4bc7585 100644 --- a/peeroxide-dht/src/hyperdht_messages.rs +++ b/peeroxide-dht/src/hyperdht_messages.rs @@ -4,63 +4,95 @@ use crate::compact_encoding::{ }; use crate::messages::Ipv4Peer; +/// Type alias for results returned by encoding and decoding functions in this module. pub type Result = std::result::Result; // ── HyperDHT command IDs ──────────────────────────────────────────────────── +/// Command ID for peer handshake messages. pub const PEER_HANDSHAKE: u64 = 0; +/// Command ID for peer hole-punch messages. pub const PEER_HOLEPUNCH: u64 = 1; +/// Command ID for find-peer queries. pub const FIND_PEER: u64 = 2; +/// Command ID for DHT lookup queries. pub const LOOKUP: u64 = 3; +/// Command ID for announce messages. pub const ANNOUNCE: u64 = 4; +/// Command ID for unannounce messages. pub const UNANNOUNCE: u64 = 5; +/// Command ID for mutable put requests. pub const MUTABLE_PUT: u64 = 6; +/// Command ID for mutable get requests. pub const MUTABLE_GET: u64 = 7; +/// Command ID for immutable put requests. pub const IMMUTABLE_PUT: u64 = 8; +/// Command ID for immutable get requests. pub const IMMUTABLE_GET: u64 = 9; // ── Handshake routing modes ───────────────────────────────────────────────── +/// Routing mode indicating the message originates from the client. pub const MODE_FROM_CLIENT: u64 = 0; +/// Routing mode indicating the message originates from the server. pub const MODE_FROM_SERVER: u64 = 1; +/// Routing mode indicating the message originates from a relay node. pub const MODE_FROM_RELAY: u64 = 2; +/// Routing mode indicating the message originates from a second relay node. pub const MODE_FROM_SECOND_RELAY: u64 = 3; +/// Routing mode indicating this is a reply message. pub const MODE_REPLY: u64 = 4; // ── Firewall constants ────────────────────────────────────────────────────── +/// Firewall state is unknown. pub const FIREWALL_UNKNOWN: u64 = 0; +/// Firewall is open — NAT maps all connections to the same public address and port. pub const FIREWALL_OPEN: u64 = 1; +/// Firewall uses a consistent port mapping for the same destination IP. pub const FIREWALL_CONSISTENT: u64 = 2; +/// Firewall uses random port mapping for each new destination. pub const FIREWALL_RANDOM: u64 = 3; // ── Error constants ───────────────────────────────────────────────────────── +/// No error. pub const ERROR_NONE: u64 = 0; +/// The operation was aborted. pub const ERROR_ABORTED: u64 = 1; +/// Protocol version mismatch between peers. pub const ERROR_VERSION_MISMATCH: u64 = 2; +/// The remote is busy; try again later. pub const ERROR_TRY_LATER: u64 = 3; +/// The sequence number has already been used. pub const ERROR_SEQ_REUSED: u64 = 16; +/// The sequence number is too low. pub const ERROR_SEQ_TOO_LOW: u64 = 17; // ── HyperPeer ─────────────────────────────────────────────────────────────── #[derive(Debug, Clone)] +/// A HyperDHT peer identified by its public key and optional relay addresses. pub struct HyperPeer { + /// The 32-byte Ed25519 public key identifying this peer. pub public_key: [u8; 32], + /// IPv4 relay addresses through which this peer can be reached. pub relay_addresses: Vec, } +/// Pre-encodes a [`HyperPeer`] to calculate the required buffer size. pub fn preencode_hyper_peer(state: &mut State, peer: &HyperPeer) { state.end += 32; preencode_ipv4_peer_array(state, &peer.relay_addresses); } +/// Encodes a [`HyperPeer`] into the compact-encoding buffer. pub fn encode_hyper_peer(state: &mut State, peer: &HyperPeer) -> Result<()> { encode_fixed32(state, &peer.public_key); encode_ipv4_peer_array(state, &peer.relay_addresses) } +/// Decodes a [`HyperPeer`] from the compact-encoding buffer. pub fn decode_hyper_peer(state: &mut State) -> Result { let public_key = decode_fixed32(state)?; let relay_addresses = decode_ipv4_peer_array(state)?; @@ -70,6 +102,7 @@ pub fn decode_hyper_peer(state: &mut State) -> Result { }) } +/// Serializes a [`HyperPeer`] to a byte vector. pub fn encode_hyper_peer_to_bytes(peer: &HyperPeer) -> Result> { let mut state = State::new(); preencode_hyper_peer(&mut state, peer); @@ -78,6 +111,7 @@ pub fn encode_hyper_peer_to_bytes(peer: &HyperPeer) -> Result> { Ok(state.buffer) } +/// Deserializes a [`HyperPeer`] from bytes. pub fn decode_hyper_peer_from_bytes(buf: &[u8]) -> Result { let mut state = State::from_buffer(buf); decode_hyper_peer(&mut state) @@ -91,13 +125,19 @@ const FLAG_SIGNATURE: u64 = 0x4; const FLAG_BUMP: u64 = 0x8; #[derive(Debug, Clone)] +/// Wire message for announcing a peer on the DHT. pub struct AnnounceMessage { + /// The peer being announced, if present. pub peer: Option, + /// A 32-byte refresh token, if present. pub refresh: Option<[u8; 32]>, + /// A 64-byte signature over the announcement, if present. pub signature: Option<[u8; 64]>, + /// Bump counter used to force re-announcement. pub bump: u64, } +/// Pre-encodes an [`AnnounceMessage`] to calculate the required buffer size. pub fn preencode_announce(state: &mut State, m: &AnnounceMessage) { state.end += 1; // flags byte (always fits in 1 byte, max 15) if let Some(peer) = &m.peer { @@ -114,6 +154,7 @@ pub fn preencode_announce(state: &mut State, m: &AnnounceMessage) { } } +/// Encodes an [`AnnounceMessage`] into the compact-encoding buffer. pub fn encode_announce(state: &mut State, m: &AnnounceMessage) -> Result<()> { let flags = (if m.peer.is_some() { FLAG_PEER } else { 0 }) | (if m.refresh.is_some() { FLAG_REFRESH } else { 0 }) @@ -135,6 +176,7 @@ pub fn encode_announce(state: &mut State, m: &AnnounceMessage) -> Result<()> { Ok(()) } +/// Decodes an [`AnnounceMessage`] from the compact-encoding buffer. pub fn decode_announce(state: &mut State) -> Result { let flags = decode_uint(state)?; let peer = if flags & FLAG_PEER != 0 { @@ -165,6 +207,7 @@ pub fn decode_announce(state: &mut State) -> Result { }) } +/// Serializes an [`AnnounceMessage`] to a byte vector. pub fn encode_announce_to_bytes(m: &AnnounceMessage) -> Result> { let mut state = State::new(); preencode_announce(&mut state, m); @@ -173,6 +216,7 @@ pub fn encode_announce_to_bytes(m: &AnnounceMessage) -> Result> { Ok(state.buffer) } +/// Deserializes an [`AnnounceMessage`] from bytes. pub fn decode_announce_from_bytes(buf: &[u8]) -> Result { let mut state = State::from_buffer(buf); decode_announce(&mut state) @@ -181,11 +225,15 @@ pub fn decode_announce_from_bytes(buf: &[u8]) -> Result { // ── LookupRawReply ────────────────────────────────────────────────────────── #[derive(Debug, Clone)] +/// Raw reply message returned from a DHT lookup containing matching peers. pub struct LookupRawReply { + /// The list of peers returned by the lookup. pub peers: Vec, + /// Bump counter echoed from the corresponding announce. pub bump: u64, } +/// Pre-encodes a [`LookupRawReply`] to calculate the required buffer size. pub fn preencode_lookup_raw_reply(state: &mut State, m: &LookupRawReply) { preencode_uint(state, m.peers.len() as u64); for peer in &m.peers { @@ -194,6 +242,7 @@ pub fn preencode_lookup_raw_reply(state: &mut State, m: &LookupRawReply) { preencode_uint(state, m.bump); } +/// Encodes a [`LookupRawReply`] into the compact-encoding buffer. pub fn encode_lookup_raw_reply(state: &mut State, m: &LookupRawReply) -> Result<()> { encode_uint(state, m.peers.len() as u64); for peer in &m.peers { @@ -203,6 +252,7 @@ pub fn encode_lookup_raw_reply(state: &mut State, m: &LookupRawReply) -> Result< Ok(()) } +/// Decodes a [`LookupRawReply`] from the compact-encoding buffer. pub fn decode_lookup_raw_reply(state: &mut State) -> Result { let count = decode_uint(state)? as usize; let mut peers = Vec::with_capacity(count); @@ -217,6 +267,7 @@ pub fn decode_lookup_raw_reply(state: &mut State) -> Result { Ok(LookupRawReply { peers, bump }) } +/// Serializes a [`LookupRawReply`] to a byte vector. pub fn encode_lookup_raw_reply_to_bytes(m: &LookupRawReply) -> Result> { let mut state = State::new(); preencode_lookup_raw_reply(&mut state, m); @@ -225,6 +276,7 @@ pub fn encode_lookup_raw_reply_to_bytes(m: &LookupRawReply) -> Result> { Ok(state.buffer) } +/// Deserializes a [`LookupRawReply`] from bytes. pub fn decode_lookup_raw_reply_from_bytes(buf: &[u8]) -> Result { let mut state = State::from_buffer(buf); decode_lookup_raw_reply(&mut state) @@ -233,13 +285,19 @@ pub fn decode_lookup_raw_reply_from_bytes(buf: &[u8]) -> Result // ── MutablePutRequest ─────────────────────────────────────────────────────── #[derive(Debug, Clone, PartialEq, Eq)] +/// Request to store a mutable value on the DHT. pub struct MutablePutRequest { + /// The 32-byte public key of the value's owner. pub public_key: [u8; 32], + /// Monotonically increasing sequence number for this value. pub seq: u64, + /// The value payload to store. pub value: Vec, + /// A 64-byte Ed25519 signature over `(seq, value)`. pub signature: [u8; 64], } +/// Pre-encodes a [`MutablePutRequest`] to calculate the required buffer size. pub fn preencode_mutable_put_request(state: &mut State, m: &MutablePutRequest) { state.end += 32; preencode_uint(state, m.seq); @@ -247,6 +305,7 @@ pub fn preencode_mutable_put_request(state: &mut State, m: &MutablePutRequest) { state.end += 64; } +/// Encodes a [`MutablePutRequest`] into the compact-encoding buffer. pub fn encode_mutable_put_request(state: &mut State, m: &MutablePutRequest) -> Result<()> { encode_fixed32(state, &m.public_key); encode_uint(state, m.seq); @@ -255,6 +314,7 @@ pub fn encode_mutable_put_request(state: &mut State, m: &MutablePutRequest) -> R Ok(()) } +/// Decodes a [`MutablePutRequest`] from the compact-encoding buffer. pub fn decode_mutable_put_request(state: &mut State) -> Result { let public_key = decode_fixed32(state)?; let seq = decode_uint(state)?; @@ -268,6 +328,7 @@ pub fn decode_mutable_put_request(state: &mut State) -> Result Result> { let mut state = State::new(); preencode_mutable_put_request(&mut state, m); @@ -276,6 +337,7 @@ pub fn encode_mutable_put_request_to_bytes(m: &MutablePutRequest) -> Result Result { let mut state = State::from_buffer(buf); decode_mutable_put_request(&mut state) @@ -284,18 +346,24 @@ pub fn decode_mutable_put_request_from_bytes(buf: &[u8]) -> Result, + /// A 64-byte Ed25519 signature over `(seq, value)`. pub signature: [u8; 64], } +/// Pre-encodes a [`MutableGetResponse`] to calculate the required buffer size. pub fn preencode_mutable_get_response(state: &mut State, m: &MutableGetResponse) { preencode_uint(state, m.seq); compact_encoding::preencode_buffer(state, Some(&m.value)); state.end += 64; } +/// Encodes a [`MutableGetResponse`] into the compact-encoding buffer. pub fn encode_mutable_get_response(state: &mut State, m: &MutableGetResponse) -> Result<()> { encode_uint(state, m.seq); compact_encoding::encode_buffer(state, Some(&m.value)); @@ -303,6 +371,7 @@ pub fn encode_mutable_get_response(state: &mut State, m: &MutableGetResponse) -> Ok(()) } +/// Decodes a [`MutableGetResponse`] from the compact-encoding buffer. pub fn decode_mutable_get_response(state: &mut State) -> Result { let seq = decode_uint(state)?; let value = compact_encoding::decode_buffer(state)?.unwrap_or_default(); @@ -314,6 +383,7 @@ pub fn decode_mutable_get_response(state: &mut State) -> Result Result> { let mut state = State::new(); preencode_mutable_get_response(&mut state, m); @@ -322,6 +392,7 @@ pub fn encode_mutable_get_response_to_bytes(m: &MutableGetResponse) -> Result Result { let mut state = State::from_buffer(buf); decode_mutable_get_response(&mut state) @@ -330,28 +401,35 @@ pub fn decode_mutable_get_response_from_bytes(buf: &[u8]) -> Result, } +/// Pre-encodes a [`MutableSignable`] to calculate the required buffer size. pub fn preencode_mutable_signable(state: &mut State, m: &MutableSignable) { preencode_uint(state, m.seq); compact_encoding::preencode_buffer(state, Some(&m.value)); } +/// Encodes a [`MutableSignable`] into the compact-encoding buffer. pub fn encode_mutable_signable(state: &mut State, m: &MutableSignable) -> Result<()> { encode_uint(state, m.seq); compact_encoding::encode_buffer(state, Some(&m.value)); Ok(()) } +/// Decodes a [`MutableSignable`] from the compact-encoding buffer. pub fn decode_mutable_signable(state: &mut State) -> Result { let seq = decode_uint(state)?; let value = compact_encoding::decode_buffer(state)?.unwrap_or_default(); Ok(MutableSignable { seq, value }) } +/// Serializes a [`MutableSignable`] to a byte vector. pub fn encode_mutable_signable_to_bytes(m: &MutableSignable) -> Result> { let mut state = State::new(); preencode_mutable_signable(&mut state, m); @@ -360,6 +438,7 @@ pub fn encode_mutable_signable_to_bytes(m: &MutableSignable) -> Result> Ok(state.buffer) } +/// Deserializes a [`MutableSignable`] from bytes. pub fn decode_mutable_signable_from_bytes(buf: &[u8]) -> Result { let mut state = State::from_buffer(buf); decode_mutable_signable(&mut state) @@ -422,13 +501,19 @@ fn decode_ipv6_peer_array(state: &mut State) -> Result> { // ── HandshakeMessage (PEER_HANDSHAKE wire format) ─────────────────────────── #[derive(Debug, Clone, PartialEq, Eq)] +/// Wire message exchanged during the PEER_HANDSHAKE phase. pub struct HandshakeMessage { + /// Routing mode for this handshake (one of the `MODE_*` constants). pub mode: u64, + /// Raw Noise protocol handshake bytes. pub noise: Vec, + /// Optional IPv4 address of the connecting peer. pub peer_address: Option, + /// Optional IPv4 address of the relay through which this message is routed. pub relay_address: Option, } +/// Pre-encodes a [`HandshakeMessage`] to calculate the required buffer size. pub fn preencode_handshake(state: &mut State, m: &HandshakeMessage) { preencode_uint(state, 0); // flags preencode_uint(state, m.mode); @@ -441,6 +526,7 @@ pub fn preencode_handshake(state: &mut State, m: &HandshakeMessage) { } } +/// Encodes a [`HandshakeMessage`] into the compact-encoding buffer. pub fn encode_handshake(state: &mut State, m: &HandshakeMessage) -> Result<()> { let flags = (if m.peer_address.is_some() { 1u64 } else { 0 }) | (if m.relay_address.is_some() { 2 } else { 0 }); @@ -456,6 +542,7 @@ pub fn encode_handshake(state: &mut State, m: &HandshakeMessage) -> Result<()> { Ok(()) } +/// Decodes a [`HandshakeMessage`] from the compact-encoding buffer. pub fn decode_handshake(state: &mut State) -> Result { let flags = decode_uint(state)?; let mode = decode_uint(state)?; @@ -480,6 +567,7 @@ pub fn decode_handshake(state: &mut State) -> Result { }) } +/// Serializes a [`HandshakeMessage`] to a byte vector. pub fn encode_handshake_to_bytes(m: &HandshakeMessage) -> Result> { let mut state = State::new(); preencode_handshake(&mut state, m); @@ -488,6 +576,7 @@ pub fn encode_handshake_to_bytes(m: &HandshakeMessage) -> Result> { Ok(state.buffer) } +/// Deserializes a [`HandshakeMessage`] from bytes. pub fn decode_handshake_from_bytes(buf: &[u8]) -> Result { let mut state = State::from_buffer(buf); decode_handshake(&mut state) @@ -496,13 +585,19 @@ pub fn decode_handshake_from_bytes(buf: &[u8]) -> Result { // ── HolepunchMessage (PEER_HOLEPUNCH wire format) ─────────────────────────── #[derive(Debug, Clone, PartialEq, Eq)] +/// Wire message exchanged during the PEER_HOLEPUNCH phase. pub struct HolepunchMessage { + /// Routing mode for this holepunch message (one of the `MODE_*` constants). pub mode: u64, + /// Session identifier for this holepunch attempt. pub id: u64, + /// Encrypted holepunch payload bytes. pub payload: Vec, + /// Optional IPv4 address of the peer involved in the holepunch. pub peer_address: Option, } +/// Pre-encodes a [`HolepunchMessage`] to calculate the required buffer size. pub fn preencode_holepunch_msg(state: &mut State, m: &HolepunchMessage) { preencode_uint(state, 0); // flags preencode_uint(state, m.mode); @@ -513,6 +608,7 @@ pub fn preencode_holepunch_msg(state: &mut State, m: &HolepunchMessage) { } } +/// Encodes a [`HolepunchMessage`] into the compact-encoding buffer. pub fn encode_holepunch_msg(state: &mut State, m: &HolepunchMessage) -> Result<()> { let flags: u64 = if m.peer_address.is_some() { 1 } else { 0 }; encode_uint(state, flags); @@ -525,6 +621,7 @@ pub fn encode_holepunch_msg(state: &mut State, m: &HolepunchMessage) -> Result<( Ok(()) } +/// Decodes a [`HolepunchMessage`] from the compact-encoding buffer. pub fn decode_holepunch_msg(state: &mut State) -> Result { let flags = decode_uint(state)?; let mode = decode_uint(state)?; @@ -544,6 +641,7 @@ pub fn decode_holepunch_msg(state: &mut State) -> Result { }) } +/// Serializes a [`HolepunchMessage`] to a byte vector. pub fn encode_holepunch_msg_to_bytes(m: &HolepunchMessage) -> Result> { let mut state = State::new(); preencode_holepunch_msg(&mut state, m); @@ -552,6 +650,7 @@ pub fn encode_holepunch_msg_to_bytes(m: &HolepunchMessage) -> Result> { Ok(state.buffer) } +/// Deserializes a [`HolepunchMessage`] from bytes. pub fn decode_holepunch_msg_from_bytes(buf: &[u8]) -> Result { let mut state = State::from_buffer(buf); decode_holepunch_msg(&mut state) @@ -560,8 +659,11 @@ pub fn decode_holepunch_msg_from_bytes(buf: &[u8]) -> Result { // ── RelayInfo ─────────────────────────────────────────────────────────────── #[derive(Debug, Clone, PartialEq, Eq)] +/// A pair of addresses describing a relay-assisted path between two peers. pub struct RelayInfo { + /// The IPv4 address of the relay node. pub relay_address: Ipv4Peer, + /// The IPv4 address of the remote peer as seen by the relay. pub peer_address: Ipv4Peer, } @@ -620,8 +722,11 @@ fn decode_relay_info_array(state: &mut State) -> Result> { // ── HolepunchInfo ─────────────────────────────────────────────────────────── #[derive(Debug, Clone, PartialEq, Eq)] +/// Metadata for an ongoing hole-punch session, including relay candidates. pub struct HolepunchInfo { + /// Unique session identifier for this hole-punch attempt. pub id: u64, + /// List of relay-peer address pairs available for this hole-punch. pub relays: Vec, } @@ -645,10 +750,15 @@ fn decode_holepunch_info(state: &mut State) -> Result { // ── UdxInfo ───────────────────────────────────────────────────────────────── #[derive(Debug, Clone, PartialEq, Eq)] +/// UDX transport parameters exchanged during the Noise handshake. pub struct UdxInfo { + /// UDX protocol version advertised by the sender. pub version: u64, + /// Whether the sender's UDP socket can be reused across connections. pub reusable_socket: bool, + /// Local UDX stream identifier chosen by the sender. pub id: u64, + /// Initial sequence number for the UDX stream. pub seq: u64, } @@ -682,7 +792,9 @@ fn decode_udx_info(state: &mut State) -> Result { // ── SecretStreamInfo ──────────────────────────────────────────────────────── #[derive(Debug, Clone, PartialEq, Eq)] +/// Secret stream parameters exchanged during the Noise handshake. pub struct SecretStreamInfo { + /// Secret stream protocol version advertised by the sender. pub version: u64, } @@ -703,9 +815,13 @@ fn decode_secret_stream_info(state: &mut State) -> Result { // ── RelayThroughInfo ──────────────────────────────────────────────────────── #[derive(Debug, Clone, PartialEq, Eq)] +/// Information about a relay node through which a connection should be routed. pub struct RelayThroughInfo { + /// Protocol version for the relay-through capability. pub version: u64, + /// 32-byte public key of the relay node. pub public_key: [u8; 32], + /// 32-byte authorization token for connecting through the relay. pub token: [u8; 32], } @@ -738,16 +854,27 @@ fn decode_relay_through_info(state: &mut State) -> Result { // ── NoisePayload (exchanged inside Noise handshake) ───────────────────────── #[derive(Debug, Clone, PartialEq, Eq)] +/// Payload carried inside the Noise protocol handshake messages. pub struct NoisePayload { + /// Protocol version of this payload (must be `1` for valid messages). pub version: u64, + /// Error code from the sender (one of the `ERROR_*` constants). pub error: u64, + /// Firewall type reported by the sender (one of the `FIREWALL_*` constants). pub firewall: u64, + /// Optional hole-punch session info, present when hole-punching is in progress. pub holepunch: Option, + /// IPv4 addresses the sender is reachable on. pub addresses4: Vec, + /// IPv6 addresses the sender is reachable on. pub addresses6: Vec, + /// Optional UDX transport parameters. pub udx: Option, + /// Optional secret stream parameters. pub secret_stream: Option, + /// Optional relay-through info for relayed connections. pub relay_through: Option, + /// Optional list of relay IPv4 addresses available for this peer. pub relay_addresses: Option>, } @@ -759,6 +886,7 @@ const NP_FLAG_SECRET_STREAM: u64 = 16; const NP_FLAG_RELAY_THROUGH: u64 = 32; const NP_FLAG_RELAY_ADDRESSES: u64 = 64; +/// Pre-encodes a [`NoisePayload`] to calculate the required buffer size. pub fn preencode_noise_payload(state: &mut State, m: &NoisePayload) { state.end += 4; // version + flags + error + firewall (each 1 byte for small values) if let Some(hp) = &m.holepunch { @@ -784,6 +912,7 @@ pub fn preencode_noise_payload(state: &mut State, m: &NoisePayload) { } } +/// Encodes a [`NoisePayload`] into the compact-encoding buffer. pub fn encode_noise_payload(state: &mut State, m: &NoisePayload) -> Result<()> { let mut flags = 0u64; if m.holepunch.is_some() { @@ -837,6 +966,7 @@ pub fn encode_noise_payload(state: &mut State, m: &NoisePayload) -> Result<()> { Ok(()) } +/// Decodes a [`NoisePayload`] from the compact-encoding buffer. pub fn decode_noise_payload(state: &mut State) -> Result { let version = decode_uint(state)?; if version != 1 { @@ -907,6 +1037,7 @@ pub fn decode_noise_payload(state: &mut State) -> Result { }) } +/// Serializes a [`NoisePayload`] to a byte vector. pub fn encode_noise_payload_to_bytes(m: &NoisePayload) -> Result> { let mut state = State::new(); preencode_noise_payload(&mut state, m); @@ -915,6 +1046,7 @@ pub fn encode_noise_payload_to_bytes(m: &NoisePayload) -> Result> { Ok(state.buffer) } +/// Deserializes a [`NoisePayload`] from bytes. pub fn decode_noise_payload_from_bytes(buf: &[u8]) -> Result { let mut state = State::from_buffer(buf); decode_noise_payload(&mut state) @@ -923,15 +1055,25 @@ pub fn decode_noise_payload_from_bytes(buf: &[u8]) -> Result { // ── HolepunchPayload (encrypted, exchanged during hole-punch rounds) ──────── #[derive(Debug, Clone, PartialEq, Eq)] +/// Encrypted payload exchanged between peers during each hole-punch round. pub struct HolepunchPayload { + /// Error code from the sender (one of the `ERROR_*` constants). pub error: u64, + /// Firewall type reported by the sender (one of the `FIREWALL_*` constants). pub firewall: u64, + /// Current hole-punch round number. pub round: u64, + /// Whether the sender has already established a direct connection. pub connected: bool, + /// Whether the sender is actively sending hole-punch packets. pub punching: bool, + /// List of local IPv4 addresses the sender is reachable on, if present. pub addresses: Option>, + /// The remote address the sender is trying to punch to, if known. pub remote_address: Option, + /// A 32-byte token sent by the initiator to prove liveness. pub token: Option<[u8; 32]>, + /// A 32-byte token sent by the remote peer to prove liveness. pub remote_token: Option<[u8; 32]>, } @@ -942,6 +1084,7 @@ const HP_FLAG_REMOTE_ADDRESS: u64 = 8; const HP_FLAG_TOKEN: u64 = 16; const HP_FLAG_REMOTE_TOKEN: u64 = 32; +/// Pre-encodes a [`HolepunchPayload`] to calculate the required buffer size. pub fn preencode_holepunch_payload(state: &mut State, m: &HolepunchPayload) { state.end += 4; // flags + error + firewall + round if let Some(addrs) = &m.addresses { @@ -958,6 +1101,7 @@ pub fn preencode_holepunch_payload(state: &mut State, m: &HolepunchPayload) { } } +/// Encodes a [`HolepunchPayload`] into the compact-encoding buffer. pub fn encode_holepunch_payload(state: &mut State, m: &HolepunchPayload) -> Result<()> { let mut flags = 0u64; if m.connected { @@ -999,6 +1143,7 @@ pub fn encode_holepunch_payload(state: &mut State, m: &HolepunchPayload) -> Resu Ok(()) } +/// Decodes a [`HolepunchPayload`] from the compact-encoding buffer. pub fn decode_holepunch_payload(state: &mut State) -> Result { let flags = decode_uint(state)?; let error = decode_uint(state)?; @@ -1040,6 +1185,7 @@ pub fn decode_holepunch_payload(state: &mut State) -> Result { }) } +/// Serializes a [`HolepunchPayload`] to a byte vector. pub fn encode_holepunch_payload_to_bytes(m: &HolepunchPayload) -> Result> { let mut state = State::new(); preencode_holepunch_payload(&mut state, m); @@ -1048,6 +1194,7 @@ pub fn encode_holepunch_payload_to_bytes(m: &HolepunchPayload) -> Result Ok(state.buffer) } +/// Deserializes a [`HolepunchPayload`] from bytes. pub fn decode_holepunch_payload_from_bytes(buf: &[u8]) -> Result { let mut state = State::from_buffer(buf); decode_holepunch_payload(&mut state) diff --git a/peeroxide-dht/src/lib.rs b/peeroxide-dht/src/lib.rs index 086e0ca..6bb1f01 100644 --- a/peeroxide-dht/src/lib.rs +++ b/peeroxide-dht/src/lib.rs @@ -1,4 +1,5 @@ #![forbid(unsafe_code)] +#![deny(missing_docs)] //! Rust port of [HyperDHT](https://github.com/holepunchto/hyperdht) — a //! Kademlia distributed hash table with NAT hole-punching, Noise-encrypted @@ -56,16 +57,30 @@ #![deny(clippy::all)] +/// Blind relay for proxying encrypted traffic between peers behind restrictive NATs. pub mod blind_relay; +/// Compact binary encoding primitives compatible with the +/// [compact-encoding](https://github.com/holepunchto/compact-encoding) wire format. pub mod compact_encoding; +/// BLAKE2b hashing, Ed25519 signing, and namespace derivation helpers. pub mod crypto; +/// High-level HyperDHT node: peer discovery, announce/unannounce, mutable/immutable +/// storage, and Noise-encrypted connections. pub mod hyperdht; +/// Wire-format message types for HyperDHT peer handshake, holepunch, and relay +/// operations. pub mod hyperdht_messages; +/// DHT RPC request/response message encoding and decoding. pub mod messages; +/// Noise IK handshake for establishing shared secrets between peers. pub mod noise; +/// Noise handshake wrapper that adds framing and key splitting for stream encryption. pub mod noise_wrap; +/// Lightweight multiplexer for running multiple channels over a single connection. pub mod protomux; +/// DHT RPC transport layer: request dispatch, reply handling, and node communication. pub mod rpc; +/// Noise-encrypted bidirectional byte stream over any `AsyncRead + AsyncWrite` transport. pub mod secret_stream; // Internal protocol modules — public for advanced use but hidden from diff --git a/peeroxide-dht/src/messages.rs b/peeroxide-dht/src/messages.rs index c30750a..e41ec80 100644 --- a/peeroxide-dht/src/messages.rs +++ b/peeroxide-dht/src/messages.rs @@ -2,15 +2,22 @@ use crate::compact_encoding::{self, EncodingError, State}; use crate::peer::NodeId; #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] +/// DHT RPC command kinds. pub enum Command { + /// Pings a peer. Ping = 0, + /// Pings a peer over NAT. PingNat = 1, + /// Finds the closest nodes to a target. FindNode = 2, + /// Reports a down-hint for a peer. DownHint = 3, + /// Sends a delayed ping. DelayedPing = 4, } impl Command { + /// Converts a numeric code into a [`Command`]. pub fn from_u64(v: u64) -> Option { match v { 0 => Some(Self::Ping), @@ -34,37 +41,65 @@ const FLAG_TARGET_OR_ERROR: u8 = 0b0000_1000; const FLAG_VALUE: u8 = 0b0001_0000; #[derive(Debug, Clone, PartialEq, Eq)] +/// A peer network address (host and port). +/// +/// Despite the name, this type may carry either an IPv4 or IPv6 address +/// depending on context—for example, `addresses6` fields decode into +/// `Vec`. The name is inherited from the upstream JavaScript +/// implementation. pub struct Ipv4Peer { + /// The peer host (IPv4 or IPv6 address string). pub host: String, + /// The peer port. pub port: u16, } #[derive(Debug, Clone)] +/// A DHT request message. pub struct Request { + /// The transaction identifier. pub tid: u16, + /// The destination peer. pub to: Ipv4Peer, + /// The optional sender id. pub id: Option, + /// The optional request token. pub token: Option<[u8; 32]>, + /// Whether the request is internal. pub internal: bool, + /// The numeric command code. pub command: u64, + /// The optional target node id. pub target: Option, + /// The optional payload value. pub value: Option>, } #[derive(Debug, Clone)] +/// A DHT response message. pub struct Response { + /// The transaction identifier. pub tid: u16, + /// The destination peer. pub to: Ipv4Peer, + /// The optional sender id. pub id: Option, + /// The optional response token. pub token: Option<[u8; 32]>, + /// The closer peers returned by the lookup. pub closer_nodes: Vec, + /// The response error code. pub error: u64, + /// The optional payload value. pub value: Option>, } #[derive(Debug, Clone)] +/// A decoded DHT wire message. pub enum Message { + /// A request message. Request(Request), + /// A response message. Response(Response), } @@ -112,6 +147,7 @@ fn decode_ipv4_array(state: &mut State) -> Result, EncodingError> Ok(peers) } +/// Pre-encodes a [`Request`] to calculate the required buffer size. pub fn preencode_request(state: &mut State, req: &Request) { state.end += 1; // type_version state.end += 1; // flags @@ -133,6 +169,7 @@ pub fn preencode_request(state: &mut State, req: &Request) { } } +/// Encodes a [`Request`] into the compact-encoding buffer. pub fn encode_request(state: &mut State, req: &Request) -> Result<(), EncodingError> { compact_encoding::encode_uint8(state, REQUEST_ID); @@ -174,6 +211,7 @@ pub fn encode_request(state: &mut State, req: &Request) -> Result<(), EncodingEr Ok(()) } +/// Pre-encodes a [`Response`] to calculate the required buffer size. pub fn preencode_response(state: &mut State, res: &Response) { state.end += 1; // type_version state.end += 1; // flags @@ -197,6 +235,7 @@ pub fn preencode_response(state: &mut State, res: &Response) { } } +/// Encodes a [`Response`] into the compact-encoding buffer. pub fn encode_response(state: &mut State, res: &Response) -> Result<(), EncodingError> { compact_encoding::encode_uint8(state, RESPONSE_ID); @@ -240,6 +279,7 @@ pub fn encode_response(state: &mut State, res: &Response) -> Result<(), Encoding Ok(()) } +/// Decodes a [`Message`] from the compact-encoding buffer. pub fn decode_message(buf: &[u8]) -> Result { if buf.is_empty() { return Err(EncodingError::OutOfBounds { @@ -333,6 +373,7 @@ pub fn decode_message(buf: &[u8]) -> Result { } } +/// Serializes a [`Request`] to a byte vector. pub fn encode_request_to_bytes(req: &Request) -> Result, EncodingError> { let mut state = State::new(); preencode_request(&mut state, req); @@ -341,6 +382,7 @@ pub fn encode_request_to_bytes(req: &Request) -> Result, EncodingError> Ok(state.buffer) } +/// Serializes a [`Response`] to a byte vector. pub fn encode_response_to_bytes(res: &Response) -> Result, EncodingError> { let mut state = State::new(); preencode_response(&mut state, res); diff --git a/peeroxide-dht/src/noise.rs b/peeroxide-dht/src/noise.rs index 7e4f661..3e20669 100644 --- a/peeroxide-dht/src/noise.rs +++ b/peeroxide-dht/src/noise.rs @@ -31,14 +31,19 @@ const PROTOCOL_NAME_IK: &[u8] = b"Noise_IK_Ed25519_ChaChaPoly_BLAKE2b"; // ─── Error type ────────────────────────────────────────────────────────────── +/// Errors that can occur during a Noise protocol handshake. #[derive(Debug, thiserror::Error)] pub enum NoiseError { + /// The provided public key could not be decompressed to a valid curve point. #[error("invalid public key")] InvalidPublicKey, + /// AEAD decryption or authentication failed. #[error("decryption failed")] DecryptionFailed, + /// A send or receive was attempted after the handshake already completed. #[error("handshake already complete")] HandshakeComplete, + /// The handshake state machine received a message out of order. #[error("unexpected handshake state")] UnexpectedState, } @@ -298,6 +303,7 @@ impl SymmetricState { /// Ed25519 keypair in libsodium 64-byte format: `seed[32] || pubkey[32]`. #[derive(Clone)] pub struct Keypair { + /// Ed25519 public key (32-byte compressed Edwards Y point). pub public_key: [u8; 32], /// 64-byte libsodium format: first 32 bytes are the seed, last 32 are the /// compressed Edwards Y public key. @@ -669,14 +675,17 @@ impl HandshakeIK { self.e = Some(keypair); } + /// Returns true after the handshake has completed. pub fn complete(&self) -> bool { self.result.is_some() } + /// Access the handshake result (available once [`complete`](Self::complete) returns true). pub fn result(&self) -> Option<&HandshakeResult> { self.result.as_ref() } + /// Returns the remote peer's static public key, if known. pub fn remote_static_key(&self) -> Option<&[u8; 32]> { self.rs.as_ref() } diff --git a/peeroxide-dht/src/noise_wrap.rs b/peeroxide-dht/src/noise_wrap.rs index 44cf16f..b6c95a7 100644 --- a/peeroxide-dht/src/noise_wrap.rs +++ b/peeroxide-dht/src/noise_wrap.rs @@ -1,6 +1,6 @@ //! NoiseWrap — IK-pattern handshake with typed payload encoding. //! -//! Combines [`HandshakeIK`] with [`NoisePayload`] encoding/decoding, +//! Combines [`HandshakeIK`](crate::noise::HandshakeIK) with [`NoisePayload`](crate::hyperdht_messages::NoisePayload) encoding/decoding, //! and derives the `holepunch_secret` after finalisation. //! //! Reference: `hyperdht/lib/noise-wrap.js`. @@ -20,14 +20,18 @@ type Blake2bMac256 = Blake2bMac; // ─── Error type ────────────────────────────────────────────────────────────── +/// Errors from the [`NoiseWrap`] handshake layer. #[derive(Debug, thiserror::Error)] pub enum NoiseWrapError { + /// The underlying Noise IK handshake failed. #[error("noise handshake failed: {0}")] Noise(#[from] crate::noise::NoiseError), + /// Payload compact-encoding or decoding failed. #[error("payload encoding failed: {0}")] Encoding(#[from] crate::compact_encoding::EncodingError), + /// [`NoiseWrap::finalize`] was called before both messages were exchanged. #[error("handshake not yet complete")] NotComplete, } diff --git a/peeroxide-dht/src/protomux.rs b/peeroxide-dht/src/protomux.rs index e0a6398..8ee57c7 100644 --- a/peeroxide-dht/src/protomux.rs +++ b/peeroxide-dht/src/protomux.rs @@ -68,26 +68,34 @@ const MAX_BATCH: usize = 8 * 1024 * 1024; // ── Errors ─────────────────────────────────────────────────────────────────── #[derive(Debug, Error)] +/// Errors returned by the Protomux encoder, decoder, and channel runtime. pub enum ProtomuxError { + /// Encoding failed while serializing a frame. #[error("encoding error: {0}")] Encoding(#[from] c::EncodingError), + /// The underlying framed stream was closed. #[error("stream closed")] StreamClosed, + /// The incoming frame was malformed. #[error("invalid frame: {0}")] InvalidFrame(String), + /// The channel was already closed. #[error("channel closed")] ChannelClosed, + /// No local channel exists for the requested ID. #[error("channel not found: local_id={0}")] ChannelNotFound(u64), + /// An internal mux invariant was violated. #[error("internal error: {0}")] Internal(String), } +/// Result type used by Protomux operations. pub type Result = std::result::Result; // ── Frame encoding ─────────────────────────────────────────────────────────── @@ -218,16 +226,25 @@ pub fn encode_batch(entries: &[(u64, Vec)]) -> Vec { /// Decoded control frame. #[derive(Debug, Clone, PartialEq)] pub enum ControlFrame { + /// Opened channel metadata from the remote peer. Open { + /// Remote local channel ID. local_id: u64, + /// Protocol name. protocol: String, + /// Optional sub-channel identifier. id: Option>, + /// Optional handshake payload. handshake_state: Option>, }, + /// Remote requested channel closure. Close { + /// Remote local channel ID. local_id: u64, }, + /// Remote rejected the channel. Reject { + /// Remote channel ID. remote_id: u64, }, } @@ -235,7 +252,9 @@ pub enum ControlFrame { /// A single item from a decoded batch. #[derive(Debug, Clone, PartialEq)] pub struct BatchItem { + /// Channel ID that the batched message belongs to. pub channel_id: u64, + /// Raw inner batch payload. pub data: Vec, } @@ -248,8 +267,11 @@ pub enum DecodedFrame { Batch(Vec), /// A single data message on a channel. Message { + /// Channel ID. channel_id: u64, + /// Message type index. message_type: u64, + /// Message payload. payload: Vec, }, } @@ -361,15 +383,19 @@ pub trait FramedStream: Send + 'static { pub enum ChannelEvent { /// Remote opened the channel (with optional handshake data). Opened { + /// Optional handshake payload from the remote peer. handshake: Option>, }, /// Received a message on this channel. Message { + /// Message type index. message_type: u32, + /// Message payload. data: Vec, }, /// Channel was closed. Closed { + /// `true` if the remote peer initiated the close. is_remote: bool, }, } diff --git a/peeroxide-dht/src/rpc.rs b/peeroxide-dht/src/rpc.rs index aacc900..d77dc34 100644 --- a/peeroxide-dht/src/rpc.rs +++ b/peeroxide-dht/src/rpc.rs @@ -40,15 +40,21 @@ const ERR_UNKNOWN_COMMAND: u64 = 1; // ── Error ───────────────────────────────────────────────────────────────────── #[derive(Debug, Error)] +/// Errors returned by [`DhtHandle`] and [`spawn`]. pub enum DhtError { + /// Underlying I/O failed. #[error("IO error: {0}")] Io(#[from] crate::io::IoError), + /// The DHT node has been destroyed. #[error("node destroyed")] Destroyed, + /// The internal command channel is closed. #[error("command channel closed")] ChannelClosed, + /// Bootstrapping did not complete successfully. #[error("bootstrap failed")] BootstrapFailed, + /// A request failed with the given message. #[error("request failed: {0}")] RequestFailed(String), } @@ -58,12 +64,19 @@ pub enum DhtError { /// Configuration for creating a DHT node. #[derive(Debug, Clone)] pub struct DhtConfig { + /// Bootstrap node addresses. pub bootstrap: Vec, + /// Local port to bind. pub port: u16, + /// Local host to bind. pub host: String, + /// Whether to force ephemeral mode. pub ephemeral: Option, + /// Whether to advertise as firewalled. pub firewalled: bool, + /// Query concurrency limit. pub concurrency: usize, + /// Maximum query window size. pub max_window: usize, } @@ -82,57 +95,89 @@ impl Default for DhtConfig { } #[derive(Debug, Clone)] +/// Response to a ping request. pub struct PingResponse { + /// Remote peer that replied. pub from: Ipv4Peer, + /// Optional peer id reported by the remote node. pub id: Option, + /// Round-trip time for the ping. pub rtt: Duration, } #[derive(Debug, Clone)] +/// Data returned from a DHT request. pub struct ResponseData { + /// Remote peer that replied. pub from: Ipv4Peer, + /// Optional peer id reported by the remote node. pub id: Option, + /// Optional response token. pub token: Option<[u8; 32]>, + /// Nodes returned by the remote peer. pub closer_nodes: Vec, + /// Response error code. pub error: u64, + /// Optional response value. pub value: Option>, + /// Round-trip time for the request. pub rtt: Duration, } #[derive(Debug, Clone)] +/// Parameters for a user-driven DHT query. pub struct UserQueryParams { + /// Query target node id. pub target: NodeId, + /// RPC command to send. pub command: u64, + /// Optional query payload. pub value: Option>, + /// Whether the query is a commit. pub commit: bool, + /// Optional per-query concurrency override. pub concurrency: Option, } #[derive(Debug, Clone)] +/// Parameters for a user-driven DHT request. pub struct UserRequestParams { + /// Optional request token. pub token: Option<[u8; 32]>, + /// RPC command to send. pub command: u64, + /// Optional target node id. pub target: Option, + /// Optional request payload. pub value: Option>, } +/// An incoming user-facing request forwarded from the DHT. pub struct UserRequest { + /// Origin peer for the request. pub from: Ipv4Peer, + /// Optional origin peer id. pub id: Option, + /// Optional request token. pub token: Option<[u8; 32]>, + /// RPC command received. pub command: u64, + /// Optional target node id. pub target: Option, + /// Optional request payload. pub value: Option>, reply_tx: Option>)>>, } impl UserRequest { + /// Replies to the request with a value and success code. pub fn reply(&mut self, value: Option>) { if let Some(tx) = self.reply_tx.take() { let _ = tx.send((0, value)); } } + /// Replies to the request with an error code. pub fn error(&mut self, code: u64) { if let Some(tx) = self.reply_tx.take() { let _ = tx.send((code, None)); @@ -212,12 +257,14 @@ struct DeferredReply { // ── DhtHandle (user-facing, Send + Sync + Clone) ────────────────────────────── +/// Handle for interacting with a running DHT node. #[derive(Clone)] pub struct DhtHandle { cmd_tx: mpsc::UnboundedSender, } impl DhtHandle { + /// Waits until the node has finished bootstrapping. pub async fn bootstrapped(&self) -> Result<(), DhtError> { let (tx, rx) = oneshot::channel(); self.cmd_tx @@ -226,6 +273,7 @@ impl DhtHandle { rx.await.map_err(|_| DhtError::ChannelClosed)? } + /// Sends a ping to `host:port`. pub async fn ping(&self, host: &str, port: u16) -> Result { let (tx, rx) = oneshot::channel(); self.cmd_tx @@ -238,6 +286,7 @@ impl DhtHandle { rx.await.map_err(|_| DhtError::ChannelClosed)? } + /// Runs a `find_node` query for `target`. pub async fn find_node(&self, target: NodeId) -> Result, DhtError> { let (tx, rx) = oneshot::channel(); self.cmd_tx @@ -249,6 +298,7 @@ impl DhtHandle { rx.await.map_err(|_| DhtError::ChannelClosed)? } + /// Runs a custom DHT query. pub async fn query(&self, params: UserQueryParams) -> Result, DhtError> { let (tx, rx) = oneshot::channel(); self.cmd_tx @@ -260,6 +310,7 @@ impl DhtHandle { rx.await.map_err(|_| DhtError::ChannelClosed)? } + /// Sends a request to a remote peer. pub async fn request( &self, params: UserRequestParams, @@ -279,6 +330,7 @@ impl DhtHandle { } /// Fire-and-forget relay send (no response tracking). + /// Relays an RPC command to `to` without waiting for a reply. pub fn relay( &self, command: u64, @@ -296,6 +348,7 @@ impl DhtHandle { .map_err(|_| DhtError::ChannelClosed) } + /// Subscribes to forwarded user requests. pub async fn subscribe_requests(&self) -> Option> { let (tx, rx) = oneshot::channel(); self.cmd_tx @@ -304,6 +357,7 @@ impl DhtHandle { rx.await.ok() } + /// Returns the current routing table size. pub async fn table_size(&self) -> Result { let (tx, rx) = oneshot::channel(); self.cmd_tx @@ -312,6 +366,7 @@ impl DhtHandle { rx.await.map_err(|_| DhtError::ChannelClosed) } + /// Destroys the running DHT node. pub async fn destroy(&self) -> Result<(), DhtError> { let (tx, rx) = oneshot::channel(); self.cmd_tx @@ -320,6 +375,7 @@ impl DhtHandle { rx.await.map_err(|_| DhtError::ChannelClosed)? } + /// Returns the local bound port. pub async fn local_port(&self) -> Result { let (tx, rx) = oneshot::channel(); self.cmd_tx @@ -1311,6 +1367,7 @@ impl DhtNode { // ── Spawn ───────────────────────────────────────────────────────────────────── +/// Spawns a DHT node and returns its join handle and public handle. pub async fn spawn( runtime: &UdxRuntime, config: DhtConfig, diff --git a/peeroxide-dht/src/secret_stream.rs b/peeroxide-dht/src/secret_stream.rs index 8091ca6..941451f 100644 --- a/peeroxide-dht/src/secret_stream.rs +++ b/peeroxide-dht/src/secret_stream.rs @@ -57,30 +57,47 @@ fn ns_responder() -> &'static [u8; 32] { // ── Errors ─────────────────────────────────────────────────────────────────── #[derive(Debug, Error)] +/// Errors returned by `SecretStream` and its framing helpers. pub enum SecretStreamError { + /// An underlying I/O operation failed. #[error("I/O error: {0}")] Io(#[from] std::io::Error), + /// Noise handshake negotiation failed. #[error("noise handshake failed: {0}")] Noise(#[from] NoiseError), + /// Secretstream decryption failed. #[error("secretstream decryption failed: {0}")] Decrypt(#[from] secretstream::SecretstreamError), + /// The handshake ended before completion. #[error("handshake did not complete")] HandshakeIncomplete, + /// EOF was encountered while the handshake was still in progress. #[error("unexpected EOF during handshake")] UnexpectedEof, + /// The ID header length was invalid. #[error("invalid ID header: expected {expected} bytes, got {got}")] - InvalidIdHeader { expected: usize, got: usize }, - + InvalidIdHeader { + /// The expected header length in bytes. + expected: usize, + /// The actual header length received. + got: usize, + }, + + /// The remote stream ID did not match the expected identity. #[error("stream ID mismatch: remote peer sent wrong identity")] StreamIdMismatch, + /// The framed message exceeded the maximum allowed length. #[error("message too large: {len} bytes exceeds maximum {MAX_MESSAGE_LEN}")] - MessageTooLarge { len: usize }, + MessageTooLarge { + /// The length of the oversized message in bytes. + len: usize, + }, } // ── SecretStream ───────────────────────────────────────────────────────────── diff --git a/peeroxide/src/error.rs b/peeroxide/src/error.rs index 5263a16..3ae50ee 100644 --- a/peeroxide/src/error.rs +++ b/peeroxide/src/error.rs @@ -3,18 +3,23 @@ use thiserror::Error; /// Errors from the Hyperswarm layer. #[derive(Debug, Error)] pub enum SwarmError { + /// Error from the HyperDHT layer. #[error("DHT error: {0}")] Dht(#[from] peeroxide_dht::hyperdht::HyperDhtError), + /// Error from the DHT RPC transport. #[error("DHT RPC error: {0}")] DhtRpc(#[from] peeroxide_dht::rpc::DhtError), + /// Error from the UDX transport layer. #[error("UDX error: {0}")] Udx(#[from] libudx::UdxError), + /// The swarm has been destroyed and can no longer process commands. #[error("swarm destroyed")] Destroyed, + /// An internal channel was closed unexpectedly. #[error("channel closed")] ChannelClosed, } diff --git a/peeroxide/src/lib.rs b/peeroxide/src/lib.rs index 48f3a89..016c5e4 100644 --- a/peeroxide/src/lib.rs +++ b/peeroxide/src/lib.rs @@ -1,4 +1,5 @@ #![forbid(unsafe_code)] +#![deny(missing_docs)] //! Rust port of [Hyperswarm](https://github.com/holepunchto/hyperswarm) — //! topic-based P2P peer discovery and encrypted connections over the diff --git a/peeroxide/src/peer_info.rs b/peeroxide/src/peer_info.rs index 941cbdd..2262118 100644 --- a/peeroxide/src/peer_info.rs +++ b/peeroxide/src/peer_info.rs @@ -15,10 +15,15 @@ const PROVEN_THRESHOLD_SECS: u64 = 15; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(u8)] pub enum Priority { + /// Lowest priority (4+ failed attempts). VeryLow = 0, + /// Low priority (3 attempts, or 2 unproven). Low = 1, + /// Default priority (0 attempts, or 1 unproven). Normal = 2, + /// High priority (2 attempts, proven). High = 3, + /// Highest priority (1 attempt, proven). VeryHigh = 4, } @@ -26,22 +31,34 @@ pub enum Priority { /// /// Modelled after `lib/peer-info.js` in the Node.js Hyperswarm. pub struct PeerInfo { + /// The peer's Ed25519 public key. pub public_key: [u8; 32], + /// Relay addresses learned from DHT discovery. pub relay_addresses: Vec, + /// Whether we are reconnecting after a proven connection dropped. pub reconnecting: bool, + /// Whether this peer has had a successful connection lasting ≥ 15 seconds. pub proven: bool, + /// Whether this peer has been banned. pub banned: bool, + /// Whether this peer is queued for an outgoing connection attempt. pub queued: bool, + /// Whether a connection attempt is currently in progress. pub connecting: bool, + /// Whether this peer was explicitly added (not discovered via DHT). pub explicit: bool, + /// Topics this peer is associated with. pub topics: Vec<[u8; 32]>, + /// Number of connection attempts since the last successful connection. pub attempts: u32, + /// Current computed priority level for connection scheduling. pub priority: Priority, connected_at: Option, waiting: bool, } impl PeerInfo { + /// Create a new [`PeerInfo`] for the given public key and relay addresses. pub fn new(public_key: [u8; 32], relay_addresses: Vec) -> Self { Self { public_key, @@ -83,6 +100,7 @@ impl PeerInfo { } } + /// Record that a connection to this peer has been established. pub fn connected(&mut self) { self.reconnecting = false; self.connected_at = Some(Instant::now()); @@ -99,6 +117,7 @@ impl PeerInfo { } } + /// Returns `true` if this peer is eligible for garbage collection. pub fn should_gc(&self) -> bool { if self.banned || self.queued || self.explicit || self.waiting { return false; @@ -106,10 +125,12 @@ impl PeerInfo { self.topics.is_empty() } + /// Returns `true` if this peer is in a retry-backoff waiting period. pub fn is_waiting(&self) -> bool { self.waiting } + /// Set whether this peer is in a retry-backoff waiting period. pub fn set_waiting(&mut self, w: bool) { self.waiting = w; } diff --git a/peeroxide/src/swarm.rs b/peeroxide/src/swarm.rs index 4b4e924..57889d6 100644 --- a/peeroxide/src/swarm.rs +++ b/peeroxide/src/swarm.rs @@ -148,6 +148,7 @@ pub struct SwarmConnection { } impl SwarmConnection { + /// Returns the remote peer's static public key. pub fn remote_public_key(&self) -> &[u8; 32] { &self.peer.remote_public_key }