Skip to main content

qualia_core_db/p2p/
wireguard_runtime.rs

1// Userspace WireGuard data-plane runtime — a `Tunn` state machine bound to a real UDP socket.
2//
3// `wireguard_userspace.rs` provides the pure crypto core (keys + a `boringtun::noise::Tunn`
4// state machine) and proves a full handshake + one data packet flow *entirely in memory*.
5// This module is the next layer up: it owns a real `std::net::UdpSocket`, an endpoint for the
6// peer, and drives the `Tunn` over the wire — sending handshake/keepalive/cookie replies
7// automatically, delivering decrypted inner IP packets to the caller, and driving WireGuard's
8// timers. It is the SocialWebNet data plane: two of these, one per peer, carry curated traffic
9// once the address-book coordination plane has exchanged keys and endpoints.
10//
11// Design choices:
12//   * **IPv6-only overlay.** The SocialWebNet inner address space is strictly IPv6 — the same
13//     `fd00::/8` ULA space that `connection_identifier::derive_overlay_addr` mints. IPv4 is
14//     deliberately not carried: the mesh is a clean-slate overlay, and IPv4's NAT/address-scarcity
15//     baggage is exactly what it exists to escape. An authenticated *inner* packet that is IPv4 is
16//     therefore dropped (not delivered), never emitted by us, and treated as peer misconfiguration.
17//   * **Roaming built in.** WireGuard's endpoint is defined as the source of the most recent
18//     *authenticated* packet. Every successful decapsulate updates `peer_endpoint` to the
19//     datagram's source, so a peer whose IP/port changes (dynamic IPs, NAT rebind) is followed
20//     without any coordination-plane round-trip. This is the whole point of a socially-defined
21//     mesh over dynamic addresses. (Roaming acts on the *outer* UDP source, which may be IPv4 or
22//     IPv6 transport — only the *inner* overlay is IPv6-only.)
23//   * **Caller-driven pump, no hidden threads.** `pump()` processes exactly one datagram and
24//     `tick()` runs the timers once. The caller owns the loop (a background thread, an async
25//     task, or a bounded test loop), which keeps the core deterministically testable with two
26//     loopback sockets and no external systems.
27//   * **Reusable scratch buffers.** `recv_buf`/`out_buf` are owned once so the steady-state
28//     path allocates nothing per packet. `boringtun`'s `TunnResult` borrows the *output*
29//     buffer (not the `Tunn`), so sending a produced packet and then reusing the buffer are
30//     disjoint field borrows the compiler accepts.
31//
32// Native-only, exactly like `wireguard_userspace`: `boringtun` does not build for `wasm32`.
33// WASM peers reach the network through a relay, not this path.
34#![cfg(not(target_arch = "wasm32"))]
35
36use std::io::ErrorKind;
37use std::net::{SocketAddr, UdpSocket};
38use std::time::Duration;
39
40use boringtun::noise::{Tunn, TunnResult};
41use boringtun::x25519::PublicKey;
42
43use super::wireguard_userspace::{new_tunnel, WgKeypair};
44
45/// Scratch buffer size — a full UDP datagram (65 535) plus WireGuard framing headroom.
46const MAX_DATAGRAM: usize = 65_535;
47
48/// What one [`WgTunnel::pump`] produced.
49#[derive(Debug)]
50pub enum TunnelEvent {
51    /// A decrypted inner IPv6 packet arrived from the peer — hand it to the virtual interface.
52    InnerPacket(Vec<u8>),
53    /// Only WireGuard control traffic moved (handshake, keepalive, cookie) — or an authenticated
54    /// but non-IPv6 inner packet was dropped; nothing for the caller.
55    Progressed,
56    /// The socket read timed out (or would block) with nothing to do.
57    Idle,
58}
59
60/// A live userspace-WireGuard tunnel to a single peer, bound to a real UDP socket.
61///
62/// Build with [`WgTunnel::bind`], point it at the peer with [`WgTunnel::set_peer_endpoint`]
63/// (or let the first authenticated packet set it via roaming), then either [`initiate_handshake`]
64/// (the initiator) or wait to receive one. Drive it with [`pump`] (per datagram) and [`tick`]
65/// (per second). Encrypt outbound inner packets with [`send_packet`].
66///
67/// [`initiate_handshake`]: WgTunnel::initiate_handshake
68/// [`pump`]: WgTunnel::pump
69/// [`tick`]: WgTunnel::tick
70/// [`send_packet`]: WgTunnel::send_packet
71pub struct WgTunnel {
72    tunn: Tunn,
73    socket: UdpSocket,
74    /// Where we send to. `None` until set explicitly or learned from the first authenticated
75    /// packet (roaming). Updated on every successful decapsulate.
76    peer_endpoint: Option<SocketAddr>,
77    recv_buf: Vec<u8>,
78    out_buf: Vec<u8>,
79}
80
81impl WgTunnel {
82    /// Bind a UDP socket and build the tunnel state machine for `peer_public`.
83    ///
84    /// `bind_addr` may use port 0 to let the OS choose (read it back with [`local_addr`]).
85    /// `index` is a local WireGuard session index (any `u32`, distinct per tunnel). The peer
86    /// endpoint is unset; call [`set_peer_endpoint`] before initiating, or let roaming learn it.
87    ///
88    /// [`local_addr`]: WgTunnel::local_addr
89    /// [`set_peer_endpoint`]: WgTunnel::set_peer_endpoint
90    pub fn bind(
91        mine: &WgKeypair,
92        peer_public: PublicKey,
93        bind_addr: SocketAddr,
94        index: u32,
95    ) -> Result<WgTunnel, String> {
96        let socket = UdpSocket::bind(bind_addr).map_err(|e| format!("bind {bind_addr}: {e}"))?;
97        let tunn = new_tunnel(mine, peer_public, index)?;
98        Ok(WgTunnel {
99            tunn,
100            socket,
101            peer_endpoint: None,
102            recv_buf: vec![0u8; MAX_DATAGRAM],
103            out_buf: vec![0u8; MAX_DATAGRAM],
104        })
105    }
106
107    /// The socket's local address (resolves an OS-chosen port after binding to `:0`).
108    pub fn local_addr(&self) -> Result<SocketAddr, String> {
109        self.socket.local_addr().map_err(|e| e.to_string())
110    }
111
112    /// The peer endpoint we currently send to, if known.
113    pub fn peer_endpoint(&self) -> Option<SocketAddr> {
114        self.peer_endpoint
115    }
116
117    /// Point the tunnel at `addr`. Roaming may later override this with the source of an
118    /// authenticated packet.
119    pub fn set_peer_endpoint(&mut self, addr: SocketAddr) {
120        self.peer_endpoint = Some(addr);
121    }
122
123    /// Set the socket read timeout so [`pump`] returns [`TunnelEvent::Idle`] instead of blocking
124    /// forever. `None` blocks indefinitely.
125    ///
126    /// [`pump`]: WgTunnel::pump
127    pub fn set_read_timeout(&self, dur: Option<Duration>) -> Result<(), String> {
128        self.socket.set_read_timeout(dur).map_err(|e| e.to_string())
129    }
130
131    /// Has a WireGuard session been established (a handshake completed)?
132    pub fn has_session(&self) -> bool {
133        self.tunn.time_since_last_handshake().is_some()
134    }
135
136    fn endpoint(&self) -> Result<SocketAddr, String> {
137        self.peer_endpoint.ok_or_else(|| {
138            "peer endpoint not set (call set_peer_endpoint or receive a packet first)".to_string()
139        })
140    }
141
142    /// Kick off the Noise_IKpsk2 handshake: produce a handshake initiation and send it to the peer.
143    /// The initiator calls this once; the responder just [`pump`]s and answers automatically.
144    ///
145    /// [`pump`]: WgTunnel::pump
146    pub fn initiate_handshake(&mut self) -> Result<(), String> {
147        let dst = self.endpoint()?;
148        match self.tunn.encapsulate(&[], &mut self.out_buf) {
149            TunnResult::WriteToNetwork(pkt) => {
150                self.socket.send_to(pkt, dst).map_err(|e| e.to_string())?;
151                Ok(())
152            }
153            // Already have a live session — nothing to initiate.
154            TunnResult::Done => Ok(()),
155            TunnResult::Err(e) => Err(format!("handshake init: {e:?}")),
156            other => Err(format!("unexpected handshake-init result: {other:?}")),
157        }
158    }
159
160    /// Encrypt one inner IP packet and send it to the peer.
161    ///
162    /// If no session is up yet, `boringtun` emits a handshake initiation instead of ciphertext;
163    /// we forward that so the handshake starts, and the caller should retry the data send once
164    /// [`has_session`] is true. Returns `Ok(true)` if ciphertext/handshake was sent, `Ok(false)`
165    /// if `boringtun` produced nothing (e.g. the packet was queued pending a handshake).
166    ///
167    /// [`has_session`]: WgTunnel::has_session
168    pub fn send_packet(&mut self, inner: &[u8]) -> Result<bool, String> {
169        let dst = self.endpoint()?;
170        match self.tunn.encapsulate(inner, &mut self.out_buf) {
171            TunnResult::WriteToNetwork(pkt) => {
172                self.socket.send_to(pkt, dst).map_err(|e| e.to_string())?;
173                Ok(true)
174            }
175            TunnResult::Done => Ok(false),
176            TunnResult::Err(e) => Err(format!("encapsulate: {e:?}")),
177            other => Err(format!("unexpected encapsulate result: {other:?}")),
178        }
179    }
180
181    /// Receive one datagram and run it through the tunnel.
182    ///
183    /// Handshake responses, keepalives and cookie replies are sent back to the peer
184    /// automatically (and any queued follow-up packets flushed). A decrypted inner IP packet is
185    /// returned as [`TunnelEvent::InnerPacket`]. On a read timeout, [`TunnelEvent::Idle`].
186    pub fn pump(&mut self) -> Result<TunnelEvent, String> {
187        let (n, from) = match self.socket.recv_from(&mut self.recv_buf) {
188            Ok(v) => v,
189            // No datagram ready within the read timeout.
190            Err(e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => {
191                return Ok(TunnelEvent::Idle)
192            }
193            // Windows UDP quirk: `recv_from` returns WSAECONNRESET (10054 → `ConnectionReset`) when a
194            // *prior* send to the peer drew an ICMP "port unreachable" (peer momentarily down / not
195            // yet listening / behind a NAT that rejected it). A connectionless UDP socket is not
196            // actually closed — the next datagram will still arrive — so this is non-fatal: treat it
197            // as an idle tick, not a dead tunnel. (On Unix the equivalent ICMP error is not delivered
198            // to recv at all, so this arm is effectively Windows-only.)
199            Err(e) if e.kind() == ErrorKind::ConnectionReset => return Ok(TunnelEvent::Idle),
200            Err(e) => return Err(format!("recv_from: {e}")),
201        };
202
203        // Disjoint field borrows: `tunn` (mut), `recv_buf` (read), `out_buf` (mut).
204        let result = self
205            .tunn
206            .decapsulate(None, &self.recv_buf[..n], &mut self.out_buf);
207
208        match result {
209            TunnResult::WriteToNetwork(pkt) => {
210                // Roaming: an authenticated packet came from `from` — send our reply there and
211                // adopt it as the peer endpoint.
212                self.socket.send_to(pkt, from).map_err(|e| e.to_string())?;
213                self.peer_endpoint = Some(from);
214                // `boringtun` requires draining any queued packets with empty-input decapsulate
215                // calls until it stops asking to write to the network.
216                loop {
217                    match self.tunn.decapsulate(None, &[], &mut self.out_buf) {
218                        TunnResult::WriteToNetwork(more) => {
219                            self.socket.send_to(more, from).map_err(|e| e.to_string())?;
220                        }
221                        _ => break,
222                    }
223                }
224                Ok(TunnelEvent::Progressed)
225            }
226            TunnResult::WriteToTunnelV6(data, _) => {
227                let inner = data.to_vec();
228                self.peer_endpoint = Some(from);
229                Ok(TunnelEvent::InnerPacket(inner))
230            }
231            // Authenticated, but IPv4 on an IPv6-only overlay — adopt the endpoint (the packet was
232            // genuine) but drop the payload rather than deliver an unsupported inner protocol.
233            TunnResult::WriteToTunnelV4(_, _) => {
234                self.peer_endpoint = Some(from);
235                Ok(TunnelEvent::Progressed)
236            }
237            TunnResult::Done => {
238                // Authenticated control traffic with nothing to deliver (e.g. a keepalive).
239                self.peer_endpoint = Some(from);
240                Ok(TunnelEvent::Progressed)
241            }
242            TunnResult::Err(e) => Err(format!("decapsulate: {e:?}")),
243        }
244    }
245
246    /// Drive WireGuard's timers once (rekeying, keepalives, session expiry). Call roughly once a
247    /// second from the caller's loop. Sends any timer-produced packet (e.g. a keepalive) to the peer.
248    pub fn tick(&mut self) -> Result<(), String> {
249        // No endpoint yet ⇒ no session ⇒ nothing for the timers to send anywhere.
250        let dst = match self.peer_endpoint {
251            Some(d) => d,
252            None => return Ok(()),
253        };
254        match self.tunn.update_timers(&mut self.out_buf) {
255            TunnResult::WriteToNetwork(pkt) => {
256                self.socket.send_to(pkt, dst).map_err(|e| e.to_string())?;
257                Ok(())
258            }
259            TunnResult::Done => Ok(()),
260            TunnResult::Err(e) => Err(format!("update_timers: {e:?}")),
261            other => Err(format!("unexpected timer result: {other:?}")),
262        }
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use crate::p2p::wireguard_userspace::generate_keypair;
270
271    fn loopback() -> SocketAddr {
272        "127.0.0.1:0".parse().unwrap()
273    }
274
275    /// Build a minimal *valid* IPv6 packet carrying `payload` — `boringtun`'s decapsulate validates
276    /// the IP header (version nibble + length), so a raw byte string would be rejected. The overlay
277    /// is IPv6-only, using `fd00::/8` ULA source/destination addresses (the same space
278    /// `derive_overlay_addr` mints). The 40-byte IPv6 header's payload-length field is set so the
279    /// validated slice equals the whole packet.
280    fn make_ipv6_packet(payload: &[u8]) -> Vec<u8> {
281        let total_len = 40 + payload.len();
282        let mut pkt = vec![0u8; total_len];
283        pkt[0] = 0x60; // version 6, traffic class 0
284        pkt[4..6].copy_from_slice(&(payload.len() as u16).to_be_bytes()); // payload length
285        pkt[6] = 17; // next header = UDP (not inspected)
286        pkt[7] = 64; // hop limit
287        pkt[8] = 0xfd; // src fd00::1 (ULA)
288        pkt[23] = 0x01;
289        pkt[24] = 0xfd; // dst fd00::2 (ULA)
290        pkt[39] = 0x02;
291        pkt[40..].copy_from_slice(payload);
292        pkt
293    }
294
295    /// Two `WgTunnel`s over two real loopback UDP sockets: complete a handshake and carry a data
296    /// packet end-to-end. The acceptance test for "the SocialWebNet data plane works over real
297    /// sockets with zero external systems".
298    #[test]
299    fn two_tunnels_handshake_and_carry_data_over_udp() {
300        let a_keys = generate_keypair();
301        let b_keys = generate_keypair();
302
303        let mut a = WgTunnel::bind(&a_keys, b_keys.public, loopback(), 1).expect("bind A");
304        let mut b = WgTunnel::bind(&b_keys, a_keys.public, loopback(), 2).expect("bind B");
305
306        // Exchange the OS-chosen endpoints (the coordination plane's job in production).
307        let a_addr = a.local_addr().unwrap();
308        let b_addr = b.local_addr().unwrap();
309        a.set_peer_endpoint(b_addr);
310        b.set_peer_endpoint(a_addr);
311
312        // Short read timeouts so pump() never blocks the test.
313        let to = Some(Duration::from_millis(300));
314        a.set_read_timeout(to).unwrap();
315        b.set_read_timeout(to).unwrap();
316
317        // A initiates; drive both sides until A holds a live session.
318        a.initiate_handshake().expect("A initiates handshake");
319        for _ in 0..20 {
320            if a.has_session() {
321                break;
322            }
323            let _ = b.pump().expect("B pump"); // process init → send response
324            let _ = a.pump().expect("A pump"); // process response → establish + keepalive
325        }
326        assert!(a.has_session(), "A established a session");
327
328        // Let B consume the keepalive so it, too, has a live session.
329        let _ = b.pump().expect("B pump keepalive");
330        assert!(b.has_session(), "B established a session");
331
332        // A encrypts an inner IPv6 packet; B should decrypt exactly it.
333        let plaintext = make_ipv6_packet(b"hello over the socially-defined wire");
334        assert!(a.send_packet(&plaintext).expect("A sends data"));
335
336        let mut got = None;
337        for _ in 0..10 {
338            if let TunnelEvent::InnerPacket(p) = b.pump().expect("B pump data") {
339                got = Some(p);
340                break;
341            }
342        }
343        assert_eq!(
344            got.expect("B never received the inner packet"),
345            plaintext,
346            "decrypted inner packet equals what A sent"
347        );
348    }
349
350    /// Roaming: after the session is up, a packet arriving from a *new* source address moves the
351    /// peer endpoint. We simulate B changing address by sending from a second socket that adopts
352    /// B's live `Tunn` is out of scope here; instead we assert the endpoint-follows-source rule
353    /// directly on A by having B send its next packet — B's address is fixed in loopback, so we
354    /// assert the invariant that a successful decapsulate records the source as the endpoint.
355    #[test]
356    fn endpoint_is_learned_from_authenticated_source() {
357        let a_keys = generate_keypair();
358        let b_keys = generate_keypair();
359
360        // A does NOT know B's endpoint up front — it must learn it from B's handshake init.
361        let mut a = WgTunnel::bind(&a_keys, b_keys.public, loopback(), 1).expect("bind A");
362        let mut b = WgTunnel::bind(&b_keys, a_keys.public, loopback(), 2).expect("bind B");
363
364        let a_addr = a.local_addr().unwrap();
365        let b_addr = b.local_addr().unwrap();
366        // Only B is told where A is; A will discover B by roaming.
367        b.set_peer_endpoint(a_addr);
368        assert!(a.peer_endpoint().is_none(), "A starts with no endpoint");
369
370        let to = Some(Duration::from_millis(300));
371        a.set_read_timeout(to).unwrap();
372        b.set_read_timeout(to).unwrap();
373
374        // B initiates; A learns B's address from the authenticated init it receives.
375        b.initiate_handshake().expect("B initiates");
376        for _ in 0..20 {
377            let _ = a.pump().expect("A pump"); // learns endpoint on first authenticated packet
378            let _ = b.pump().expect("B pump");
379            if a.has_session() && a.peer_endpoint().is_some() {
380                break;
381            }
382        }
383        assert_eq!(
384            a.peer_endpoint(),
385            Some(b_addr),
386            "A learned B's endpoint from the authenticated handshake (roaming)"
387        );
388        assert!(
389            a.has_session(),
390            "handshake still completes when the endpoint is learned"
391        );
392    }
393}