use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use bytes::{BufMut, BytesMut};
pub const PEAVEIL_MAGIC: u8 = 0x50;
pub const PEAVEIL_VERSION: u8 = 0x01;
pub const IPV4_ENTRY_SIZE: usize = 1 + 4 + 2 + 4;
pub const IPV6_ENTRY_SIZE: usize = 1 + 16 + 2 + 4;
pub const SAMPLE_HEADER_SIZE: usize = 3;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct PeerEntry {
pub addr: SocketAddr,
pub age_secs: u32,
}
impl PeerEntry {
fn encoded_size(&self) -> usize {
match self.addr.ip() {
IpAddr::V4(_) => IPV4_ENTRY_SIZE,
IpAddr::V6(_) => IPV6_ENTRY_SIZE,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PeerSample {
entries: Vec<PeerEntry>,
}
impl PeerSample {
pub fn empty() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn from_entries(entries: Vec<PeerEntry>) -> Self {
Self { entries }
}
pub fn entries(&self) -> &[PeerEntry] {
&self.entries
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn encoded_size(&self) -> usize {
SAMPLE_HEADER_SIZE
+ self
.entries
.iter()
.map(PeerEntry::encoded_size)
.sum::<usize>()
}
pub const MAX_ENTRIES: usize = u8::MAX as usize;
pub fn encode(&self) -> BytesMut {
assert!(
self.entries.len() <= Self::MAX_ENTRIES,
"peer sample cannot carry more than {} entries (got {})",
Self::MAX_ENTRIES,
self.entries.len(),
);
let mut out = BytesMut::with_capacity(self.encoded_size());
out.put_u8(PEAVEIL_MAGIC);
out.put_u8(PEAVEIL_VERSION);
out.put_u8(self.entries.len() as u8);
for entry in &self.entries {
encode_entry(&mut out, entry);
}
debug_assert_eq!(out.len(), self.encoded_size());
out
}
pub fn decode(buf: &[u8]) -> Result<Self, DecodeError> {
if buf.len() < SAMPLE_HEADER_SIZE {
return Err(DecodeError::Truncated);
}
if buf[0] != PEAVEIL_MAGIC {
return Err(DecodeError::BadMagic(buf[0]));
}
if buf[1] != PEAVEIL_VERSION {
return Err(DecodeError::BadVersion(buf[1]));
}
let count = buf[2] as usize;
let mut entries = Vec::with_capacity(count);
let mut cursor = SAMPLE_HEADER_SIZE;
for _ in 0..count {
let family = *buf.get(cursor).ok_or(DecodeError::Truncated)?;
let (entry_size, entry) = match family {
0x04 => {
if buf.len() < cursor + IPV4_ENTRY_SIZE {
return Err(DecodeError::Truncated);
}
let ip = Ipv4Addr::new(
buf[cursor + 1],
buf[cursor + 2],
buf[cursor + 3],
buf[cursor + 4],
);
let port = u16::from_be_bytes([buf[cursor + 5], buf[cursor + 6]]);
let age = u32::from_be_bytes([
buf[cursor + 7],
buf[cursor + 8],
buf[cursor + 9],
buf[cursor + 10],
]);
(
IPV4_ENTRY_SIZE,
PeerEntry {
addr: SocketAddr::new(IpAddr::V4(ip), port),
age_secs: age,
},
)
}
0x06 => {
if buf.len() < cursor + IPV6_ENTRY_SIZE {
return Err(DecodeError::Truncated);
}
let mut octets = [0u8; 16];
octets.copy_from_slice(&buf[cursor + 1..cursor + 17]);
let port = u16::from_be_bytes([buf[cursor + 17], buf[cursor + 18]]);
let age = u32::from_be_bytes([
buf[cursor + 19],
buf[cursor + 20],
buf[cursor + 21],
buf[cursor + 22],
]);
(
IPV6_ENTRY_SIZE,
PeerEntry {
addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::from(octets)), port),
age_secs: age,
},
)
}
other => return Err(DecodeError::BadFamily(other)),
};
cursor += entry_size;
entries.push(entry);
}
Ok(Self { entries })
}
}
fn encode_entry(out: &mut BytesMut, entry: &PeerEntry) {
match entry.addr.ip() {
IpAddr::V4(v4) => {
out.put_u8(0x04);
out.put_slice(&v4.octets());
}
IpAddr::V6(v6) => {
out.put_u8(0x06);
out.put_slice(&v6.octets());
}
}
out.put_u16(entry.addr.port());
out.put_u32(entry.age_secs);
}
#[derive(Debug)]
#[non_exhaustive]
pub enum DecodeError {
Truncated,
BadMagic(u8),
BadVersion(u8),
BadFamily(u8),
}
impl std::fmt::Display for DecodeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DecodeError::Truncated => f.write_str("truncated buffer"),
DecodeError::BadMagic(b) => write!(f, "bad magic 0x{b:02x}"),
DecodeError::BadVersion(b) => write!(f, "bad version 0x{b:02x}"),
DecodeError::BadFamily(b) => write!(f, "bad address family 0x{b:02x}"),
}
}
}
impl std::error::Error for DecodeError {}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> PeerSample {
PeerSample::from_entries(vec![
PeerEntry {
addr: "10.0.0.1:9000".parse().unwrap(),
age_secs: 5,
},
PeerEntry {
addr: "[2001:db8::1]:9000".parse().unwrap(),
age_secs: 12,
},
PeerEntry {
addr: "10.0.0.2:9000".parse().unwrap(),
age_secs: 0,
},
])
}
#[test]
fn roundtrip() {
let s = sample();
let bytes = s.encode();
let decoded = PeerSample::decode(&bytes).expect("decode");
assert_eq!(s, decoded);
}
#[test]
fn roundtrip_empty() {
let s = PeerSample::empty();
let bytes = s.encode();
assert_eq!(bytes.len(), SAMPLE_HEADER_SIZE);
let decoded = PeerSample::decode(&bytes).expect("decode");
assert_eq!(s, decoded);
}
#[test]
fn bad_magic_rejected() {
let mut bytes = sample().encode();
bytes[0] = 0xAB;
assert!(matches!(
PeerSample::decode(&bytes),
Err(DecodeError::BadMagic(0xAB))
));
}
#[test]
fn bad_version_rejected() {
let mut bytes = sample().encode();
bytes[1] = 0x99;
assert!(matches!(
PeerSample::decode(&bytes),
Err(DecodeError::BadVersion(0x99))
));
}
#[test]
fn truncated_rejected() {
let bytes = sample().encode();
let truncated = &bytes[..bytes.len() - 3];
assert!(matches!(
PeerSample::decode(truncated),
Err(DecodeError::Truncated)
));
}
#[test]
fn trailing_padding_ignored() {
let mut bytes = sample().encode();
for _ in 0..5 {
bytes.put_u8(0x42);
}
let decoded = PeerSample::decode(&bytes).expect("decode");
assert_eq!(decoded, sample());
}
#[test]
#[should_panic(expected = "cannot carry more than")]
fn encode_panics_on_overflow() {
let entries: Vec<PeerEntry> = (0..PeerSample::MAX_ENTRIES + 1)
.map(|i| PeerEntry {
addr: format!("10.0.0.{}:80", i % 256).parse().unwrap(),
age_secs: 0,
})
.collect();
let sample = PeerSample::from_entries(entries);
sample.encode();
}
#[test]
fn max_ipv4_per_frame_is_at_least_20() {
let payload = peashape::ID_SIZE;
let frame = 256;
let available = frame - payload - SAMPLE_HEADER_SIZE;
let max_per_frame = available / IPV4_ENTRY_SIZE;
assert!(
max_per_frame >= 20,
"a 256-byte frame should fit at least 20 IPv4 peers; fits {max_per_frame}",
);
}
}