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}