Files
tsunagi/src/dataplane/wireguard/packet.rs
T
tsunagiandClaude Opus 5 cfab38824d Make the overlay dual stack
Every member now also derives an IPv4 address, from the same inputs as its
IPv6 one, into 100.64.0.0/10 by default. The range is configurable and IPv4
can be turned off with --no-ipv4.

IPv4 is honestly weaker than IPv6 here and the code says so. A 64 bit
interface identifier makes an IPv6 collision impossible in practice; IPv4
has nothing like that room, and in a /10 with 50 members two will derive the
same address about 0.03% of the time. A mesh with no coordinator cannot
allocate around that, so a collision is detected and resolved instead: the
member whose public key sorts lower keeps the address, a rule every member
computes identically and therefore agrees on. The other keeps IPv6 and is
flagged in the status. IPv6 always works; IPv4 almost always works and
degrades predictably.

Routing and address-ownership enforcement now cover both families: a packet
goes to the peer that owns its destination, and a decrypted packet is
dropped unless its source is an address derived for the peer that sent it,
IPv4 included.

Six new tests, among them a real IPv4 packet crossing a tunnel next to an
IPv6 one, a spoofed IPv4 source being dropped, and an IPv6-only overlay.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 13:00:35 +01:00

142 lines
4.8 KiB
Rust

//! The little bit of IP parsing the data plane needs.
//!
//! Two questions only: which peer should carry this packet, and did the packet
//! that came back really come from that peer? Everything is bounds checked and
//! nothing here can panic on a hostile packet.
use std::net::{Ipv4Addr, Ipv6Addr};
/// The addresses of an IP packet, as far as routing cares.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IpHeader {
/// An IPv4 packet.
V4 {
/// Source address.
source: Ipv4Addr,
/// Destination address.
destination: Ipv4Addr,
},
/// An IPv6 packet.
V6 {
/// Source address.
source: Ipv6Addr,
/// Destination address.
destination: Ipv6Addr,
},
}
impl IpHeader {
/// Reads the addresses out of a packet, or `None` if it is not one.
pub fn parse(packet: &[u8]) -> Option<Self> {
let version = packet.first()? >> 4;
match version {
4 => {
let source: [u8; 4] = packet.get(12..16)?.try_into().ok()?;
let destination: [u8; 4] = packet.get(16..20)?.try_into().ok()?;
Some(IpHeader::V4 {
source: Ipv4Addr::from(source),
destination: Ipv4Addr::from(destination),
})
}
6 => {
let source: [u8; 16] = packet.get(8..24)?.try_into().ok()?;
let destination: [u8; 16] = packet.get(24..40)?.try_into().ok()?;
Some(IpHeader::V6 {
source: Ipv6Addr::from(source),
destination: Ipv6Addr::from(destination),
})
}
_ => None,
}
}
/// The destination, when the packet is IPv6.
pub fn v6_destination(&self) -> Option<Ipv6Addr> {
match self {
IpHeader::V6 { destination, .. } => Some(*destination),
IpHeader::V4 { .. } => None,
}
}
/// The source, when the packet is IPv6.
pub fn v6_source(&self) -> Option<Ipv6Addr> {
match self {
IpHeader::V6 { source, .. } => Some(*source),
IpHeader::V4 { .. } => None,
}
}
/// The destination, whichever family it is.
pub fn destination(&self) -> std::net::IpAddr {
match self {
IpHeader::V4 { destination, .. } => std::net::IpAddr::V4(*destination),
IpHeader::V6 { destination, .. } => std::net::IpAddr::V6(*destination),
}
}
/// The source, whichever family it is.
pub fn source(&self) -> std::net::IpAddr {
match self {
IpHeader::V4 { source, .. } => std::net::IpAddr::V4(*source),
IpHeader::V6 { source, .. } => std::net::IpAddr::V6(*source),
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
fn ipv6_packet(source: Ipv6Addr, destination: Ipv6Addr) -> Vec<u8> {
let mut packet = vec![0u8; 48];
packet[0] = 6 << 4;
packet[8..24].copy_from_slice(&source.octets());
packet[24..40].copy_from_slice(&destination.octets());
packet
}
#[test]
fn ipv6_addresses_are_read_correctly() {
let source: Ipv6Addr = "fd00::1".parse().unwrap();
let destination: Ipv6Addr = "fd00::2".parse().unwrap();
let header = IpHeader::parse(&ipv6_packet(source, destination)).unwrap();
assert_eq!(header.v6_source(), Some(source));
assert_eq!(header.v6_destination(), Some(destination));
}
#[test]
fn ipv4_addresses_are_read_correctly() {
let mut packet = vec![0u8; 20];
packet[0] = 4 << 4;
packet[12..16].copy_from_slice(&[10, 0, 0, 1]);
packet[16..20].copy_from_slice(&[10, 0, 0, 2]);
let header = IpHeader::parse(&packet).unwrap();
assert_eq!(
header,
IpHeader::V4 {
source: Ipv4Addr::new(10, 0, 0, 1),
destination: Ipv4Addr::new(10, 0, 0, 2),
}
);
// The overlay is IPv6, so the v6 accessors correctly report nothing.
assert_eq!(header.v6_destination(), None);
}
#[test]
fn truncated_and_nonsense_packets_are_rejected_without_panicking() {
assert!(IpHeader::parse(&[]).is_none());
assert!(IpHeader::parse(&[0x60]).is_none());
assert!(IpHeader::parse(&[0x40; 19]).is_none(), "short IPv4");
assert!(IpHeader::parse(&[0x60; 39]).is_none(), "short IPv6");
assert!(IpHeader::parse(&[0x00; 64]).is_none(), "version 0");
assert!(IpHeader::parse(&[0xf0; 64]).is_none(), "version 15");
// Every possible first byte is safe to feed in.
for byte in 0..=u8::MAX {
let _ = IpHeader::parse(&[byte; 64]);
let _ = IpHeader::parse(&[byte]);
}
}
}