Separate control and data logically, move WireGuard into userspace, add a CLI
Corrects the architecture on two points raised in review, while the project is still small enough to change cheaply. 1. Control and data are separated *logically*, not physically. The old reading — "nothing but control may ride on iroh" — threw away iroh's whole value and would have forced the data plane to reimplement STUN, ICE and a relay. Now both planes ride on iroh with different ALPNs and different connections, so the data plane inherits hole punching and relay fallback, while proto/ still knows nothing about packets and dataplane/ knows nothing about the control protocol. New boundary: PacketTransport / PacketLink, an authenticated unreliable datagram channel per (network, peer, protocol). tsunagi/data/1 runs the same membership handshake, then DataOpen/DataOpenAck, then QUIC datagrams. Only the smaller endpoint id dials, so exactly one link exists per pair. A plugin is handed links and never learns reachability, so the WireGuard announcement shrank to a public key: there is no address left to lie about. 2. WireGuard now runs in userspace, on boringtun's protocol state machine. No kernel module, no wg tool, no ip shell-out, no loopback proxy: the wgtool, backend and bridge modules are gone. Only creating a TUN device needs privileges, and that sits behind TunFactory, so the entire data plane — handshake, encryption, routing, address ownership — is tested with none. Address ownership is enforced rather than believed: outbound packets go to the owner of the destination address, inbound packets are dropped unless their source is the address derived for the peer that sent them. 3. A `tsunagi` binary: secret, doctor, id, up. It owns the runtime, the logging subscriber and Ctrl-C, which the library still refuses to. Also fixes a reference cycle where IrohTransport held Arc<Inner>, which kept the databases open and the directory lock held after shutdown; two storage tests caught it once the cycle existed. 81 tests pass offline with no privileges, including real IPv6 packets crossing a real WireGuard tunnel over real iroh connections. Verified by hand: two CLI processes forming a mesh both on loopback and via n0 discovery using only an endpoint id. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
//! 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user