feat: M3 seed — the lumen/1 native protocol: QUIC control plane + reference client (Phase 5)

The first end-to-end run of lumen's own protocol, past the GameStream compatibility layer.

- lumen-core/src/quic.rs (behind the `quic` feature): the lumen/1 handshake — Hello/Welcome/
  Start as length-prefixed LE binary on one QUIC bi-stream. Welcome carries the COMPLETE
  data-plane Config: mode, FEC scheme incl. GF(2^16) Leopard (inexpressible in GameStream),
  shard sizing, AES-GCM key + per-direction salt, data UDP port. Plus quinn endpoint helpers
  (self-signed server; accepts-any client — pinning lands with the trust model) and framed
  async IO. Round-trip unit-tested.
- lumen-host m3-host: serves one lumen/1 session — QUIC handshake, then a NATIVE thread
  (no async on the frame path — design invariant) streams deterministic 64KB test frames
  through the hardened M1 Session over UdpTransport.
- lumen-client-rs: from scaffold to working reference client — connects, negotiates, brings
  up the client Session over UDP, reassembles + FEC-recovers + byte-verifies every frame.

VALIDATED END-TO-END on localhost: 300/300 frames verified, 0 mismatches, through
QUIC-negotiated GF(2^16) FEC + AES-GCM over real UDP sockets. M4 (decode+present) builds on
this exact client skeleton.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 23:33:40 +00:00
parent 1eeb35a723
commit de3123038f
10 changed files with 621 additions and 14 deletions
+3 -1
View File
@@ -9,7 +9,9 @@ authors.workspace = true
repository.workspace = true
[dependencies]
lumen-core = { path = "../lumen-core" }
lumen-core = { path = "../lumen-core", features = ["quic"] }
# M3 native control plane (the `lumen/1` QUIC handshake; data plane stays native-thread UDP).
quinn = "0.11"
anyhow = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+122
View File
@@ -0,0 +1,122 @@
//! M3 seed — the `lumen/1` native host: QUIC control plane (lumen-core `quic`) + the hardened
//! M1 data plane over real UDP. This is the first end-to-end run of lumen's own protocol,
//! past the GameStream compatibility layer: the Welcome negotiates **GF(2¹⁶) Leopard FEC**
//! (positively not expressible in GameStream) and AES-GCM with per-direction salts.
//!
//! `lumen-host m3-host [--port 9777] [--frames 300]` serves one session: handshake on QUIC,
//! then a native thread streams deterministic, verifiable test frames through
//! `lumen_core::Session` → `UdpTransport`. `lumen-client-rs --connect host:9777` is the
//! counterpart (reassembles, FEC-recovers, verifies content).
use anyhow::{anyhow, Context, Result};
use lumen_core::config::{FecConfig, FecScheme, Mode, Role};
use lumen_core::packet::{FLAG_PIC, FLAG_SOF};
use lumen_core::quic::{endpoint, io, Hello, Start, Welcome};
use lumen_core::transport::UdpTransport;
use lumen_core::Session;
use rand::RngCore;
/// Deterministic test frame: `u32 LE index` then `data[i] = idx + i` (wrapping).
pub fn test_frame(idx: u32, len: usize) -> Vec<u8> {
let mut d = vec![0u8; len];
d[0..4].copy_from_slice(&idx.to_le_bytes());
for (i, b) in d.iter_mut().enumerate().skip(4) {
*b = (idx as u8).wrapping_add(i as u8);
}
d
}
pub fn run(port: u16, frames: u32) -> Result<()> {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.context("tokio runtime")?;
rt.block_on(serve_one(port, frames))
}
async fn serve_one(port: u16, frames: u32) -> Result<()> {
let ep = endpoint::server(([0, 0, 0, 0], port).into())
.map_err(|e| anyhow!("QUIC server endpoint: {e}"))?;
tracing::info!(port, "lumen/1 host listening (QUIC)");
let incoming = ep
.accept()
.await
.ok_or_else(|| anyhow!("endpoint closed"))?;
let conn = incoming.await.context("QUIC accept")?;
let peer = conn.remote_address();
tracing::info!(%peer, "lumen/1 client connected");
let (mut send, mut recv) = conn.accept_bi().await.context("accept control stream")?;
let hello = Hello::decode(&io::read_msg(&mut recv).await?)
.map_err(|e| anyhow!("Hello decode: {e:?}"))?;
anyhow::ensure!(
hello.abi_version == lumen_core::ABI_VERSION,
"ABI mismatch: client {} host {}",
hello.abi_version,
lumen_core::ABI_VERSION
);
// Reserve a UDP port for the data plane (bind, read it back, rebind in UdpTransport).
let probe = std::net::UdpSocket::bind("0.0.0.0:0")?;
let udp_port = probe.local_addr()?.port();
drop(probe);
let mut key = [0u8; 16];
rand::thread_rng().fill_bytes(&mut key);
let welcome = Welcome {
abi_version: lumen_core::ABI_VERSION,
udp_port,
mode: Mode {
width: 1920,
height: 1080,
refresh_hz: 60,
},
// The post-GameStream point of lumen/1: Leopard GF(2¹⁶) FEC + real encryption.
fec: FecConfig {
scheme: FecScheme::Gf16,
fec_percent: 20,
max_data_per_block: 4096,
},
shard_payload: 1200,
encrypt: true,
key,
salt: *b"lmn1",
frames,
};
io::write_msg(&mut send, &welcome.encode()).await?;
let start = Start::decode(&io::read_msg(&mut recv).await?)
.map_err(|e| anyhow!("Start decode: {e:?}"))?;
let client_udp = std::net::SocketAddr::new(peer.ip(), start.client_udp_port);
tracing::info!(%client_udp, udp_port, "handshake complete — streaming");
// Data plane on a native thread (no async on the hot path — design invariant).
let cfg = welcome.session_config(Role::Host);
tokio::task::spawn_blocking(move || -> Result<()> {
let transport =
UdpTransport::connect(&format!("0.0.0.0:{udp_port}"), &client_udp.to_string())
.context("bind data plane")?;
let mut session =
Session::new(cfg, Box::new(transport)).map_err(|e| anyhow!("host session: {e:?}"))?;
let interval = std::time::Duration::from_millis(1000 / 60);
for idx in 0..frames {
let data = test_frame(idx, 64 * 1024);
session
.submit_frame(&data, idx as u64 * 16_666_667, (FLAG_PIC | FLAG_SOF) as u32)
.map_err(|e| anyhow!("submit_frame: {e:?}"))?;
std::thread::sleep(interval);
}
tracing::info!(frames, "all frames sent");
Ok(())
})
.await
.context("stream thread")??;
// Give the client a moment to drain, then close cleanly.
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
conn.close(0u32.into(), b"done");
ep.wait_idle().await;
Ok(())
}
+17
View File
@@ -19,6 +19,7 @@ mod encode;
mod gamestream;
mod inject;
mod m0;
mod m3;
mod pipeline;
mod pwinit;
mod vdisplay;
@@ -59,6 +60,22 @@ fn real_main() -> Result<()> {
Some("zerocopy-probe") => zerocopy::probe(),
// M0 pipeline spike.
Some("m0") => m0::run(parse_m0(&args[1..])?),
// M3 seed: native lumen/1 host (QUIC control plane + UDP data plane).
Some("m3-host") => {
let port = args
.iter()
.skip_while(|a| *a != "--port")
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(9777);
let frames = args
.iter()
.skip_while(|a| *a != "--frames")
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(300);
m3::run(port, frames)
}
Some("-h") | Some("--help") | Some("help") | None => {
print_usage();
Ok(())