Skip to main content

clawft_kernel/
mesh_quic.rs

1//! QUIC transport backend for mesh networking (WEFT-118 / ADR-026).
2//!
3//! Uses [`quinn`] for the QUIC wire protocol. Application-level encryption
4//! and identity remain Noise (`mesh_noise` / snow) per ADR-024 — quinn's
5//! TLS layer only satisfies the QUIC handshake. We mint an ephemeral
6//! self-signed certificate on listen and skip certificate verification
7//! on connect (the Noise XX/IK session authenticates peers).
8//!
9//! Addresses use the `quic://host:port` scheme (bare `host:port` is also
10//! accepted when this transport is selected via `[kernel.mesh] transport`).
11//!
12//! # Cancel-safety (ADR-010 / WEFT-18)
13//!
14//! [`QuicMeshStream::recv`] keeps partial length/body progress on `self`
15//! so it is safe to race inside `tokio::select!` (mesh peer loop).
16
17use std::net::SocketAddr;
18use std::sync::Arc;
19use std::sync::Once;
20
21use async_trait::async_trait;
22use quinn::crypto::rustls::{QuicClientConfig, QuicServerConfig};
23use quinn::{ClientConfig, Connection, Endpoint, RecvStream, SendStream, ServerConfig};
24use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName, UnixTime};
25
26use crate::mesh::{MAX_MESSAGE_SIZE, MeshError, MeshStream, MeshTransport, TransportListener};
27
28/// ALPN protocol identifier for WeftOS mesh over QUIC.
29const MESH_ALPN: &[u8] = b"weftos-mesh";
30
31/// SNI name used on the client; certs are self-signed for this name.
32const MESH_SNI: &str = "localhost";
33
34static INSTALL_CRYPTO: Once = Once::new();
35
36fn ensure_crypto_provider() {
37    INSTALL_CRYPTO.call_once(|| {
38        // Best-effort: another crate may have installed already.
39        let _ = rustls::crypto::ring::default_provider().install_default();
40    });
41}
42
43// ── Skip-verify TLS (Noise owns peer auth) ─────────────────────────
44
45#[derive(Debug)]
46struct SkipServerVerification(Arc<rustls::crypto::CryptoProvider>);
47
48impl SkipServerVerification {
49    fn new() -> Arc<Self> {
50        Arc::new(Self(Arc::new(rustls::crypto::ring::default_provider())))
51    }
52}
53
54impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
55    fn verify_server_cert(
56        &self,
57        _end_entity: &CertificateDer<'_>,
58        _intermediates: &[CertificateDer<'_>],
59        _server_name: &ServerName<'_>,
60        _ocsp_response: &[u8],
61        _now: UnixTime,
62    ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
63        Ok(rustls::client::danger::ServerCertVerified::assertion())
64    }
65
66    fn verify_tls12_signature(
67        &self,
68        message: &[u8],
69        cert: &CertificateDer<'_>,
70        dss: &rustls::DigitallySignedStruct,
71    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
72        rustls::crypto::verify_tls12_signature(
73            message,
74            cert,
75            dss,
76            &self.0.signature_verification_algorithms,
77        )
78    }
79
80    fn verify_tls13_signature(
81        &self,
82        message: &[u8],
83        cert: &CertificateDer<'_>,
84        dss: &rustls::DigitallySignedStruct,
85    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
86        rustls::crypto::verify_tls13_signature(
87            message,
88            cert,
89            dss,
90            &self.0.signature_verification_algorithms,
91        )
92    }
93
94    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
95        self.0.signature_verification_algorithms.supported_schemes()
96    }
97}
98
99// ── TLS / endpoint helpers ─────────────────────────────────────────
100
101fn make_server_config() -> Result<ServerConfig, MeshError> {
102    ensure_crypto_provider();
103
104    let certified = rcgen::generate_simple_self_signed(vec![MESH_SNI.into()]).map_err(|e| {
105        MeshError::Transport(format!("quic self-signed cert: {e}"))
106    })?;
107    let cert_der = CertificateDer::from(certified.cert);
108    let key_der = PrivatePkcs8KeyDer::from(certified.key_pair.serialize_der());
109
110    let mut tls = rustls::ServerConfig::builder()
111        .with_no_client_auth()
112        .with_single_cert(vec![cert_der], PrivateKeyDer::Pkcs8(key_der))
113        .map_err(|e| MeshError::Transport(format!("quic server tls: {e}")))?;
114    tls.alpn_protocols = vec![MESH_ALPN.to_vec()];
115
116    let quic_tls = QuicServerConfig::try_from(tls)
117        .map_err(|e| MeshError::Transport(format!("quic server crypto: {e}")))?;
118    Ok(ServerConfig::with_crypto(Arc::new(quic_tls)))
119}
120
121fn make_client_config() -> Result<ClientConfig, MeshError> {
122    ensure_crypto_provider();
123
124    let mut tls = rustls::ClientConfig::builder()
125        .dangerous()
126        .with_custom_certificate_verifier(SkipServerVerification::new())
127        .with_no_client_auth();
128    tls.alpn_protocols = vec![MESH_ALPN.to_vec()];
129
130    let quic_tls = QuicClientConfig::try_from(tls)
131        .map_err(|e| MeshError::Transport(format!("quic client crypto: {e}")))?;
132    Ok(ClientConfig::new(Arc::new(quic_tls)))
133}
134
135fn strip_quic_scheme(addr: &str) -> &str {
136    addr.strip_prefix("quic://").unwrap_or(addr)
137}
138
139fn parse_socket_addr(addr: &str) -> Result<SocketAddr, MeshError> {
140    strip_quic_scheme(addr)
141        .parse()
142        .map_err(|e: std::net::AddrParseError| MeshError::Io(format!("bad quic addr '{addr}': {e}")))
143}
144
145// ── QuicMeshStream ─────────────────────────────────────────────────
146
147/// A [`MeshStream`] backed by a single bidirectional QUIC stream.
148///
149/// Messages are length-prefixed on the stream (4-byte big-endian length
150/// followed by that many bytes of payload), matching TCP/WS framing so
151/// higher layers (`NoiseChannel`, mesh peer loop) stay transport-neutral.
152///
153/// `recv` is **cancel-safe** (WEFT-18): partial prefix/body progress is
154/// retained on `self` across cancellations.
155pub struct QuicMeshStream {
156    send: SendStream,
157    recv: RecvStream,
158    remote: SocketAddr,
159    /// Keeps the QUIC connection open for the stream lifetime.
160    _conn: Connection,
161    /// Client-side endpoint must outlive the connection.
162    _endpoint: Option<Endpoint>,
163    /// Staging for the 4-byte length prefix.
164    len_buf: [u8; 4],
165    len_filled: usize,
166    body: Option<Vec<u8>>,
167    body_filled: usize,
168}
169
170impl QuicMeshStream {
171    fn new(
172        send: SendStream,
173        recv: RecvStream,
174        remote: SocketAddr,
175        conn: Connection,
176        endpoint: Option<Endpoint>,
177    ) -> Self {
178        Self {
179            send,
180            recv,
181            remote,
182            _conn: conn,
183            _endpoint: endpoint,
184            len_buf: [0u8; 4],
185            len_filled: 0,
186            body: None,
187            body_filled: 0,
188        }
189    }
190
191    fn reset_frame(&mut self) {
192        self.len_filled = 0;
193        self.body = None;
194        self.body_filled = 0;
195    }
196}
197
198#[async_trait]
199impl MeshStream for QuicMeshStream {
200    async fn send(&mut self, data: &[u8]) -> Result<(), MeshError> {
201        if data.len() > MAX_MESSAGE_SIZE {
202            return Err(MeshError::MessageTooLarge {
203                size: data.len(),
204                max: MAX_MESSAGE_SIZE,
205            });
206        }
207        let len = (data.len() as u32).to_be_bytes();
208        self.send
209            .write_all(&len)
210            .await
211            .map_err(|e| MeshError::Io(e.to_string()))?;
212        self.send
213            .write_all(data)
214            .await
215            .map_err(|e| MeshError::Io(e.to_string()))?;
216        // quinn SendStream has no explicit flush; writes are packetized by the
217        // connection. finish() is only for closing the write half.
218        Ok(())
219    }
220
221    async fn recv(&mut self) -> Result<Vec<u8>, MeshError> {
222        // Cancel-safe framed read (ADR-010 / WEFT-18). Progress lives on
223        // `self` so dropping this future mid-frame does not desync the
224        // length-prefix stream.
225
226        while self.len_filled < 4 {
227            let Self {
228                recv,
229                len_buf,
230                len_filled,
231                ..
232            } = self;
233            let n = recv
234                .read(&mut len_buf[*len_filled..])
235                .await
236                .map_err(|e| MeshError::Io(e.to_string()))?
237                .ok_or(MeshError::ConnectionClosed)?;
238            *len_filled += n;
239        }
240
241        if self.body.is_none() {
242            let len = u32::from_be_bytes(self.len_buf) as usize;
243            if len > MAX_MESSAGE_SIZE {
244                self.reset_frame();
245                return Err(MeshError::MessageTooLarge {
246                    size: len,
247                    max: MAX_MESSAGE_SIZE,
248                });
249            }
250            self.body = Some(vec![0u8; len]);
251            self.body_filled = 0;
252        }
253
254        let expected = self.body.as_ref().map(|b| b.len()).unwrap_or(0);
255        while self.body_filled < expected {
256            let Self {
257                recv,
258                body,
259                body_filled,
260                ..
261            } = self;
262            let buf = body
263                .as_mut()
264                .expect("body allocated before body-fill loop");
265            if expected == 0 {
266                break;
267            }
268            let n = recv
269                .read(&mut buf[*body_filled..])
270                .await
271                .map_err(|e| MeshError::Io(e.to_string()))?
272                .ok_or(MeshError::ConnectionClosed)?;
273            *body_filled += n;
274        }
275
276        let out = self.body.take().expect("body complete");
277        self.reset_frame();
278        Ok(out)
279    }
280
281    async fn close(&mut self) -> Result<(), MeshError> {
282        // Finish the send half; connection drop closes the rest.
283        self.send
284            .finish()
285            .map_err(|e| MeshError::Io(e.to_string()))?;
286        Ok(())
287    }
288
289    fn remote_addr(&self) -> Option<SocketAddr> {
290        Some(self.remote)
291    }
292}
293
294// ── QuicTransportListener ──────────────────────────────────────────
295
296/// A [`TransportListener`] backed by a quinn [`Endpoint`] server.
297pub struct QuicTransportListener {
298    endpoint: Endpoint,
299}
300
301#[async_trait]
302impl TransportListener for QuicTransportListener {
303    async fn accept(&mut self) -> Result<(Box<dyn MeshStream>, SocketAddr), MeshError> {
304        let incoming = self
305            .endpoint
306            .accept()
307            .await
308            .ok_or_else(|| MeshError::Io("quic endpoint closed".into()))?;
309        let conn = incoming
310            .await
311            .map_err(|e| MeshError::Io(format!("quic accept handshake: {e}")))?;
312        let remote = conn.remote_address();
313        let (send, recv) = conn
314            .accept_bi()
315            .await
316            .map_err(|e| MeshError::Io(format!("quic accept_bi: {e}")))?;
317        Ok((
318            Box::new(QuicMeshStream::new(send, recv, remote, conn, None)),
319            remote,
320        ))
321    }
322
323    fn local_addr(&self) -> Result<SocketAddr, MeshError> {
324        self.endpoint
325            .local_addr()
326            .map_err(|e| MeshError::Io(e.to_string()))
327    }
328}
329
330// ── QuicTransport ──────────────────────────────────────────────────
331
332/// QUIC mesh transport (ADR-026 primary for Cloud/Edge).
333///
334/// Implements [`MeshTransport`] with quinn. Wire addresses use
335/// `quic://host:port`. Real peer authentication is Noise on top of the
336/// bidirectional stream (see `mesh_noise` / `[kernel.mesh] noise`).
337pub struct QuicTransport;
338
339#[async_trait]
340impl MeshTransport for QuicTransport {
341    fn name(&self) -> &str {
342        "quic"
343    }
344
345    async fn listen(&self, addr: &str) -> Result<Box<dyn TransportListener>, MeshError> {
346        let bind_addr = parse_socket_addr(addr)?;
347        let server_config = make_server_config()?;
348        let endpoint = Endpoint::server(server_config, bind_addr)
349            .map_err(|e| MeshError::Io(format!("quic bind {bind_addr}: {e}")))?;
350        Ok(Box::new(QuicTransportListener { endpoint }))
351    }
352
353    async fn connect(&self, addr: &str) -> Result<Box<dyn MeshStream>, MeshError> {
354        let remote = parse_socket_addr(addr)?;
355        let client_config = make_client_config()?;
356
357        // Bind ephemeral local UDP port for the client endpoint.
358        let bind: SocketAddr = if remote.is_ipv6() {
359            "[::]:0".parse().unwrap()
360        } else {
361            "0.0.0.0:0".parse().unwrap()
362        };
363        let mut endpoint =
364            Endpoint::client(bind).map_err(|e| MeshError::Io(format!("quic client endpoint: {e}")))?;
365        endpoint.set_default_client_config(client_config);
366
367        let conn = endpoint
368            .connect(remote, MESH_SNI)
369            .map_err(|e| MeshError::Io(format!("quic connect setup: {e}")))?
370            .await
371            .map_err(|e| MeshError::Io(format!("quic connect: {e}")))?;
372
373        let (send, recv) = conn
374            .open_bi()
375            .await
376            .map_err(|e| MeshError::Io(format!("quic open_bi: {e}")))?;
377
378        Ok(Box::new(QuicMeshStream::new(
379            send,
380            recv,
381            remote,
382            conn,
383            Some(endpoint),
384        )))
385    }
386
387    fn supports(&self, addr: &str) -> bool {
388        addr.starts_with("quic://")
389    }
390}
391
392// ── Tests ──────────────────────────────────────────────────────────
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397
398    #[tokio::test]
399    async fn quic_transport_connect_send_recv() {
400        let transport = QuicTransport;
401
402        let mut listener = transport.listen("127.0.0.1:0").await.unwrap();
403        let addr = listener.local_addr().unwrap();
404        let quic_url = format!("quic://{addr}");
405
406        let send_task = tokio::spawn(async move {
407            let mut stream = QuicTransport.connect(&quic_url).await.unwrap();
408            stream.send(b"hello from node A").await.unwrap();
409            let response = stream.recv().await.unwrap();
410            assert_eq!(response, b"hello from node B");
411            stream.close().await.unwrap();
412        });
413
414        let (mut server_stream, _client_addr) = listener.accept().await.unwrap();
415        let received = server_stream.recv().await.unwrap();
416        assert_eq!(received, b"hello from node A");
417        server_stream.send(b"hello from node B").await.unwrap();
418
419        send_task.await.unwrap();
420    }
421
422    /// Two-node integration: WeftHandshake frames over QUIC (AC for WEFT-118).
423    #[tokio::test]
424    async fn two_node_cluster_forms_over_quic() {
425        use crate::mesh::WeftHandshake;
426        use crate::mesh_framing::{FrameType, MeshFrame};
427
428        let transport = QuicTransport;
429        let mut listener = transport.listen("127.0.0.1:0").await.unwrap();
430        let addr = listener.local_addr().unwrap();
431        let quic_url = format!("quic://{addr}");
432
433        let node_a = tokio::spawn(async move {
434            let mut stream = QuicTransport.connect(&quic_url).await.unwrap();
435
436            let handshake = WeftHandshake {
437                node_id: "node-a".into(),
438                governance_genesis_hash: [0u8; 32],
439                governance_version: "1.0".into(),
440                capabilities: 0xFF,
441                kem_supported: false,
442                chain_seq: 0,
443                supported_sync_streams: vec![1, 2, 3],
444            };
445            let payload = serde_json::to_vec(&handshake).unwrap();
446            let frame = MeshFrame {
447                frame_type: FrameType::Handshake,
448                payload,
449            };
450            stream.send(&frame.encode().unwrap()).await.unwrap();
451
452            let data = stream.recv().await.unwrap();
453            let len = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
454            let response = MeshFrame::decode(&data[4..4 + len]).unwrap();
455            assert_eq!(response.frame_type, FrameType::Handshake);
456            let remote: WeftHandshake = serde_json::from_slice(&response.payload).unwrap();
457            assert_eq!(remote.node_id, "node-b");
458
459            stream.close().await.unwrap();
460        });
461
462        let (mut stream, _) = listener.accept().await.unwrap();
463        let data = stream.recv().await.unwrap();
464        let len = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
465        let frame = MeshFrame::decode(&data[4..4 + len]).unwrap();
466        assert_eq!(frame.frame_type, FrameType::Handshake);
467        let remote: WeftHandshake = serde_json::from_slice(&frame.payload).unwrap();
468        assert_eq!(remote.node_id, "node-a");
469
470        let handshake = WeftHandshake {
471            node_id: "node-b".into(),
472            governance_genesis_hash: [0u8; 32],
473            governance_version: "1.0".into(),
474            capabilities: 0xFF,
475            kem_supported: false,
476            chain_seq: 0,
477            supported_sync_streams: vec![1, 2, 3],
478        };
479        let payload = serde_json::to_vec(&handshake).unwrap();
480        let response = MeshFrame {
481            frame_type: FrameType::Handshake,
482            payload,
483        };
484        stream.send(&response.encode().unwrap()).await.unwrap();
485
486        node_a.await.unwrap();
487    }
488
489    /// QUIC + Noise XX: application encryption on top of quinn TLS (AC: quinn + snow).
490    #[tokio::test]
491    async fn two_node_quic_with_noise() {
492        use crate::mesh_noise::{EncryptedChannel, NoiseChannel, NoiseConfig, NoisePattern};
493        use rand::RngCore;
494
495        let transport = QuicTransport;
496        let mut listener = transport.listen("127.0.0.1:0").await.unwrap();
497        let addr = listener.local_addr().unwrap();
498        let quic_url = format!("quic://{addr}");
499
500        let mut key_a = [0u8; 32];
501        let mut key_b = [0u8; 32];
502        rand::thread_rng().fill_bytes(&mut key_a);
503        rand::thread_rng().fill_bytes(&mut key_b);
504
505        let cfg_a = NoiseConfig {
506            pattern: NoisePattern::XX,
507            local_private_key: key_a,
508            remote_static_key: None,
509        };
510        let cfg_b = NoiseConfig {
511            pattern: NoisePattern::XX,
512            local_private_key: key_b,
513            remote_static_key: None,
514        };
515
516        let node_a = tokio::spawn(async move {
517            let stream = QuicTransport.connect(&quic_url).await.unwrap();
518            let mut ch = NoiseChannel::initiate(stream, &cfg_a).await.unwrap();
519            ch.send_encrypted(b"secret-from-a").await.unwrap();
520            let reply = ch.recv_encrypted().await.unwrap();
521            assert_eq!(reply, b"secret-from-b");
522            ch.close().await.unwrap();
523        });
524
525        let (stream, _) = listener.accept().await.unwrap();
526        let mut ch = NoiseChannel::respond(stream, &cfg_b).await.unwrap();
527        let msg = ch.recv_encrypted().await.unwrap();
528        assert_eq!(msg, b"secret-from-a");
529        ch.send_encrypted(b"secret-from-b").await.unwrap();
530
531        node_a.await.unwrap();
532    }
533
534    #[test]
535    fn quic_transport_supports() {
536        let t = QuicTransport;
537        assert!(t.supports("quic://127.0.0.1:9489"));
538        assert!(!t.supports("tcp://127.0.0.1:9489"));
539        assert!(!t.supports("ws://127.0.0.1:9489"));
540        assert!(!t.supports("127.0.0.1:9489"));
541    }
542
543    #[tokio::test]
544    async fn quic_recv_survives_select_cancellation() {
545        use std::time::Duration;
546
547        let transport = QuicTransport;
548        let mut listener = transport.listen("127.0.0.1:0").await.unwrap();
549        let addr = listener.local_addr().unwrap();
550        let quic_url = format!("quic://{addr}");
551
552        let payload_a = vec![0xAAu8; 64];
553        let payload_b = vec![0xBBu8; 128];
554        let payload_a2 = payload_a.clone();
555        let payload_b2 = payload_b.clone();
556
557        let writer = tokio::spawn(async move {
558            let mut stream = QuicTransport.connect(&quic_url).await.unwrap();
559            stream.send(&payload_a2).await.unwrap();
560            tokio::time::sleep(Duration::from_millis(5)).await;
561            stream.send(&payload_b2).await.unwrap();
562            // Keep the QUIC connection alive until the server has drained
563            // both frames. Dropping the client Connection early can abort
564            // in-flight stream data (unlike TCP FIN buffering).
565            tokio::time::sleep(Duration::from_millis(500)).await;
566            let _ = stream.close().await;
567        });
568
569        let (mut server, _) = listener.accept().await.unwrap();
570
571        let mut got_first = None;
572        for _ in 0..200 {
573            tokio::select! {
574                biased;
575                result = server.recv() => {
576                    got_first = Some(result.expect("recv after cancel must succeed"));
577                    break;
578                }
579                _ = tokio::time::sleep(Duration::from_micros(50)) => {}
580            }
581        }
582        let first = got_first.expect("should eventually complete first frame");
583        assert_eq!(first, payload_a);
584
585        let second = server.recv().await.expect("second frame");
586        assert_eq!(second, payload_b);
587
588        writer.await.unwrap();
589    }
590}