Skip to main content

qualia_core_db/p2p/
wireguard_userspace.rs

1// Userspace WireGuard core — turnkey, no-admin, no `wg` CLI.
2//
3// This module replaces shelling out to the `wg`/`wg-quick` command-line tools with a
4// fully in-process WireGuard implementation built on `boringtun` 0.7.1 (Cloudflare's
5// portable, pure-Rust WireGuard). Nothing here spawns a subprocess, opens a kernel TUN
6// device, or requires elevated privileges: a `boringtun::noise::Tunn` is a pure state
7// machine that turns plaintext IP packets into encrypted WireGuard datagrams and back.
8// The caller owns the UDP socket and the virtual interface; this core owns the crypto
9// and the Noise_IKpsk2 handshake.
10//
11// The whole file is native-only: `boringtun` pulls in `ring` and OS entropy and does not
12// build for `wasm32`. WASM peers reach the network through a relay, not this path.
13#![cfg(not(target_arch = "wasm32"))]
14
15use boringtun::noise::Tunn;
16use boringtun::x25519::{PublicKey, StaticSecret};
17
18/// A WireGuard static keypair (Curve25519).
19///
20/// `private` is the peer's long-term secret; `public` is what you publish so other peers
21/// can address you. Both are the `x25519_dalek` types that `boringtun` itself consumes, so
22/// they hand straight to [`new_tunnel`]/[`Tunn::new`] with no conversion.
23pub struct WgKeypair {
24    pub private: StaticSecret,
25    pub public: PublicKey,
26}
27
28impl WgKeypair {
29    /// Reconstruct a keypair from a 32-byte Curve25519 secret (e.g. loaded from the vault).
30    /// The public key is derived deterministically from the secret.
31    pub fn from_secret_bytes(secret: [u8; 32]) -> WgKeypair {
32        let private = StaticSecret::from(secret);
33        let public = PublicKey::from(&private);
34        WgKeypair { private, public }
35    }
36
37    /// The 32 raw bytes of the secret key (for serialization into the key vault).
38    pub fn private_bytes(&self) -> [u8; 32] {
39        self.private.to_bytes()
40    }
41
42    /// The 32 raw bytes of the public key.
43    pub fn public_bytes(&self) -> [u8; 32] {
44        self.public.to_bytes()
45    }
46
47    /// This keypair's public key rendered as lowercase hex (see [`public_key_hex`]).
48    pub fn public_hex(&self) -> String {
49        public_key_hex(&self.public)
50    }
51}
52
53/// Generate a fresh WireGuard keypair using the OS CSPRNG.
54///
55/// We fill 32 bytes via `rand` (the workspace's `rand = 0.10`, backed by the OS entropy
56/// source) and build the `StaticSecret` from them, then derive the public key. This is
57/// equivalent to `StaticSecret::random_from_rng(OsRng)` but avoids coupling to a specific
58/// `rand_core` trait version — `x25519-dalek` 2 speaks `rand_core` 0.6 while the crate's
59/// `rand` is 0.9-era, and the two RNG traits do not unify. Clamping is applied by
60/// `x25519-dalek` at key-agreement time, exactly as in `random_from_rng`.
61pub fn generate_keypair() -> WgKeypair {
62    let mut secret = [0u8; 32];
63    rand::fill(&mut secret[..]);
64    WgKeypair::from_secret_bytes(secret)
65}
66
67/// Render a WireGuard public key as lowercase hex.
68///
69/// WireGuard's own config files use base64, but hex is what the rest of this codebase
70/// uses for key identifiers, so we keep it consistent here. The 32-byte key becomes a
71/// 64-character string.
72pub fn public_key_hex(pk: &PublicKey) -> String {
73    hex::encode(pk.as_bytes())
74}
75
76/// Parse a 32-byte public key from a 64-character hex string.
77pub fn public_key_from_hex(s: &str) -> Result<PublicKey, String> {
78    let bytes = hex::decode(s.trim()).map_err(|e| format!("invalid public-key hex: {e}"))?;
79    let arr: [u8; 32] = bytes
80        .as_slice()
81        .try_into()
82        .map_err(|_| format!("public key must be 32 bytes, got {}", bytes.len()))?;
83    Ok(PublicKey::from(arr))
84}
85
86/// Build a `boringtun` tunnel state machine for one peer.
87///
88/// A `Tunn` is a point-to-point WireGuard connection: `mine` is our static keypair,
89/// `peer_public` is the remote peer's static public key, and `index` is a local session
90/// index (any `u32`; `boringtun` shifts it into the WireGuard sender-index space). This
91/// core deliberately keeps the tunnel plain — no preshared key, no persistent keepalive,
92/// and no shared rate limiter — so callers get a minimal, predictable state machine:
93///
94/// * `preshared_key = None`  — optional psk2 layer left off; add later if a peer requires it.
95/// * `persistent_keepalive = None` — the caller drives keepalives/timers explicitly.
96/// * `rate_limiter = None` — `Tunn` builds its own default under-load limiter.
97///
98/// `Tunn::new` in 0.7.1 is infallible (returns `Self`), but we keep a `Result` signature so
99/// the public surface stays stable if a future boringtun revision makes construction fallible
100/// or we add validation (e.g. rejecting an all-zero peer key) here.
101pub fn new_tunnel(mine: &WgKeypair, peer_public: PublicKey, index: u32) -> Result<Tunn, String> {
102    // `StaticSecret` is not `Copy` and `Tunn::new` takes it by value, so hand it a clone
103    // built from our stored secret bytes; `mine` keeps ownership of its own key.
104    let static_private = StaticSecret::from(mine.private.to_bytes());
105    let tunn = Tunn::new(
106        static_private,
107        peer_public,
108        None, // preshared_key
109        None, // persistent_keepalive (seconds)
110        index,
111        None, // rate_limiter — Tunn constructs a default
112    );
113    Ok(tunn)
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use boringtun::noise::TunnResult;
120
121    /// Scratch buffer large enough for any WireGuard control/data frame.
122    const BUF: usize = 2048;
123
124    /// Build a minimal *valid* IPv4 packet carrying `payload`.
125    ///
126    /// This matters: `Tunn::decapsulate` runs `validate_decapsulated_packet`, which checks
127    /// the IP version nibble and that the header's total-length field does not exceed the
128    /// buffer. A raw `b"hello"` is **not** a valid IP packet and would decapsulate to
129    /// `TunnResult::Err(InvalidPacket)`, so the end-to-end data assertion must use a real
130    /// IPv4 frame. We hand-roll a 20-byte header (version 4, IHL 5) + payload and set the
131    /// total-length field so validation passes and the returned slice equals the input.
132    fn make_ipv4_packet(payload: &[u8]) -> Vec<u8> {
133        let total_len = 20 + payload.len();
134        let mut pkt = vec![0u8; total_len];
135        pkt[0] = 0x45; // IPv4, IHL = 5 (20-byte header)
136        pkt[2..4].copy_from_slice(&(total_len as u16).to_be_bytes()); // total length (big-endian)
137        pkt[8] = 64; // TTL
138        pkt[9] = 17; // protocol = UDP (arbitrary; not inspected)
139        pkt[12..16].copy_from_slice(&[192, 168, 0, 1]); // src IP
140        pkt[16..20].copy_from_slice(&[192, 168, 0, 2]); // dst IP
141        pkt[20..].copy_from_slice(payload);
142        pkt
143    }
144
145    /// Two peers, entirely in memory, no sockets: prove a full WireGuard handshake and one
146    /// data packet complete. This is the acceptance test for "userspace WireGuard works with
147    /// zero external systems".
148    #[test]
149    fn two_peer_handshake_and_data() {
150        // --- key setup -------------------------------------------------------------------
151        let a_keys = generate_keypair();
152        let b_keys = generate_keypair();
153
154        // Hex round-trips and public keys are distinct.
155        let a_pub_hex = public_key_hex(&a_keys.public);
156        assert_eq!(a_pub_hex.len(), 64, "public key hex must be 64 chars");
157        let a_pub_back = public_key_from_hex(&a_pub_hex).expect("hex round-trip");
158        assert_eq!(a_pub_back.as_bytes(), a_keys.public.as_bytes());
159        assert_ne!(
160            a_keys.public.as_bytes(),
161            b_keys.public.as_bytes(),
162            "two fresh keypairs must differ"
163        );
164
165        // A knows B's public key; B knows A's. Session indices are arbitrary but distinct.
166        let mut a = new_tunnel(&a_keys, b_keys.public, 1).expect("build A tunnel");
167        let mut b = new_tunnel(&b_keys, a_keys.public, 2).expect("build B tunnel");
168
169        // --- drive the handshake by hand -------------------------------------------------
170        // A initiates. Then we shuttle each side's WriteToNetwork output to the other side's
171        // decapsulate, bounded to a handful of iterations. In practice WireGuard needs:
172        //   A --init-->  B
173        //   A <--resp--  B
174        //   A --keepalive(data)--> B
175        // i.e. two shuttled packets after the init. The loop is defensive: it stops as soon
176        // as neither side has more handshake traffic to send.
177        let mut a_buf = [0u8; BUF];
178        let mut in_flight: Vec<u8> = match a.encapsulate(&[], &mut a_buf) {
179            TunnResult::WriteToNetwork(pkt) => pkt.to_vec(),
180            other => panic!("A did not produce a handshake init: {other:?}"),
181        };
182
183        // `send_to_a` flips which tunnel receives the next in-flight packet each round.
184        let mut send_to_a = false; // next packet goes to B first
185        let mut handshake_done = false;
186
187        for _round in 0..10 {
188            let mut out = [0u8; BUF];
189            let (recv, _peer_label) = if send_to_a {
190                (&mut a, "A")
191            } else {
192                (&mut b, "B")
193            };
194
195            let result = recv.decapsulate(None, &in_flight, &mut out);
196            match result {
197                TunnResult::WriteToNetwork(pkt) => {
198                    // The receiver produced a reply (handshake response or keepalive) that
199                    // must be delivered to the other side on the next round.
200                    in_flight = pkt.to_vec();
201                    send_to_a = !send_to_a;
202                }
203                TunnResult::Done => {
204                    // No more handshake traffic to shuttle. If both sides now hold a live
205                    // session, the handshake has completed.
206                    handshake_done = true;
207                    break;
208                }
209                TunnResult::WriteToTunnelV4(_, _) | TunnResult::WriteToTunnelV6(_, _) => {
210                    // Unexpected during the handshake phase (no data sent yet), but harmless.
211                    handshake_done = true;
212                    break;
213                }
214                TunnResult::Err(e) => panic!("handshake decapsulate error: {e:?}"),
215            }
216        }
217
218        assert!(
219            handshake_done,
220            "handshake did not converge within the bounded loop"
221        );
222
223        // --- prove a data packet flows A -> B --------------------------------------------
224        // After the handshake, A should have an established session and be able to send real
225        // data. Give the encapsulate a couple of tries in case a queued keepalive comes out
226        // first (WireGuard may emit control traffic before the first data frame).
227        let plaintext = make_ipv4_packet(b"hello");
228        let mut sent: Option<Vec<u8>> = None;
229        for _ in 0..4 {
230            let mut enc = [0u8; BUF];
231            match a.encapsulate(&plaintext, &mut enc) {
232                TunnResult::WriteToNetwork(pkt) => {
233                    // Could be the data packet, or a fresh handshake init if no session yet.
234                    // Try to decapsulate on B; if it yields our plaintext we're done.
235                    let candidate = pkt.to_vec();
236                    let mut dec = [0u8; BUF];
237                    match b.decapsulate(None, &candidate, &mut dec) {
238                        TunnResult::WriteToTunnelV4(data, _addr) => {
239                            sent = Some(data.to_vec());
240                            break;
241                        }
242                        TunnResult::WriteToNetwork(reply) => {
243                            // B answered with a handshake response/keepalive; feed it back to
244                            // A so the session establishes, then retry the data send.
245                            let reply = reply.to_vec();
246                            let mut back = [0u8; BUF];
247                            let _ = a.decapsulate(None, &reply, &mut back);
248                        }
249                        TunnResult::Done => {}
250                        other => panic!("unexpected B decapsulate during data phase: {other:?}"),
251                    }
252                }
253                TunnResult::Done => {}
254                other => panic!("unexpected A encapsulate during data phase: {other:?}"),
255            }
256        }
257
258        let received = sent.expect("B never received the data packet from A");
259        assert_eq!(
260            received, plaintext,
261            "decapsulated packet must equal the plaintext A sent"
262        );
263    }
264
265    #[test]
266    fn keypair_serialization_round_trip() {
267        let kp = generate_keypair();
268        let sk = kp.private_bytes();
269        let rebuilt = WgKeypair::from_secret_bytes(sk);
270        assert_eq!(rebuilt.public_bytes(), kp.public_bytes());
271        assert_eq!(rebuilt.public_hex(), kp.public_hex());
272    }
273}