feat: M1 lumen-core (FEC/crypto/packet/session + C ABI) and workspace scaffold
Ground-up low-latency streaming stack per docs/implementation-plan.md. M1 is
complete and tested; Linux host backends are cfg-gated stubs to be filled in on
real hardware (M0/M2).
lumen-core (built + tested on macOS/aarch64 — 21 tests):
- fec: ErasureCoder over GF(2^8) (reed-solomon-erasure, Moonlight-compatible)
and GF(2^16) Leopard-RS (reed-solomon-simd, the >1 Gbps wall-breaker); proptested
- packet: zero-copy #[repr(C)] framing, multi-block, FEC-aware reassembly
- crypto: AES-128-GCM with per-direction nonce salts + sequence-as-AAD
- session: host submit / client poll hot paths + input; loopback & UDP transports
- abi: opaque handles, versioned LumenConfig, panic guards; cbindgen-generated header
- acceptance: Rust loopback+proptest and a C harness that links the staticlib
Scaffold (compiles green on all platforms): lumen-host (vdisplay/capture/encode/
inject/web/pipeline seams under cfg(linux)), lumen-client-rs, tools/{loss-harness,
latency-probe}, Apple/Android client stubs, Gitea CI, docs.
Hardened against a multi-agent adversarial review (13 verified findings fixed,
regression-tested): reassembler memory-DoS bounds + block-consistency validation,
GCM nonce-reuse direction separation, ABI struct_size guard + range checks, FEC
shard-length guards, shard_payload datagram bound, key zeroization + Debug redaction.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
//! In-process transport for unit tests and the C ABI harness. Two cross-wired
|
||||
//! [`LoopbackTransport`]s form a host↔client link, with optional deterministic loss so
|
||||
//! tests can exercise FEC recovery without a real network.
|
||||
|
||||
use super::Transport;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// One direction of the link.
|
||||
struct Channel {
|
||||
queue: Mutex<VecDeque<Vec<u8>>>,
|
||||
/// Drop one of every `drop_period` packets (0 = lossless).
|
||||
drop_period: u32,
|
||||
sent: AtomicU64,
|
||||
dropped: AtomicU64,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
fn new(drop_period: u32) -> Arc<Channel> {
|
||||
Arc::new(Channel {
|
||||
queue: Mutex::new(VecDeque::new()),
|
||||
drop_period,
|
||||
sent: AtomicU64::new(0),
|
||||
dropped: AtomicU64::new(0),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends on `tx`, receives on `rx`. Created in cross-wired pairs by [`loopback_pair`].
|
||||
pub struct LoopbackTransport {
|
||||
tx: Arc<Channel>,
|
||||
rx: Arc<Channel>,
|
||||
}
|
||||
|
||||
impl LoopbackTransport {
|
||||
/// Number of packets this transport's send side has deliberately dropped.
|
||||
pub fn dropped(&self) -> u64 {
|
||||
self.tx.dropped.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a connected `(host, client)` pair. `host_drop_period` injects loss on the
|
||||
/// host→client (video) path; `client_drop_period` on the reverse (input) path.
|
||||
pub fn loopback_pair(
|
||||
host_drop_period: u32,
|
||||
client_drop_period: u32,
|
||||
) -> (LoopbackTransport, LoopbackTransport) {
|
||||
let h2c = Channel::new(host_drop_period);
|
||||
let c2h = Channel::new(client_drop_period);
|
||||
let host = LoopbackTransport {
|
||||
tx: h2c.clone(),
|
||||
rx: c2h.clone(),
|
||||
};
|
||||
let client = LoopbackTransport { tx: c2h, rx: h2c };
|
||||
(host, client)
|
||||
}
|
||||
|
||||
impl Transport for LoopbackTransport {
|
||||
fn send(&self, packet: &[u8]) -> std::io::Result<()> {
|
||||
let n = self.tx.sent.fetch_add(1, Ordering::Relaxed);
|
||||
if self.tx.drop_period != 0 && (n % self.tx.drop_period as u64) == 0 {
|
||||
// Deterministically drop in flight (the 1st of each `drop_period` group).
|
||||
self.tx.dropped.fetch_add(1, Ordering::Relaxed);
|
||||
return Ok(());
|
||||
}
|
||||
self.tx.queue.lock().unwrap().push_back(packet.to_vec());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn recv(&self) -> std::io::Result<Option<Vec<u8>>> {
|
||||
Ok(self.rx.queue.lock().unwrap().pop_front())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//! Pluggable packet I/O. The hot path calls [`Transport::send`] / [`Transport::recv`]
|
||||
//! directly — no async runtime is involved.
|
||||
|
||||
mod loopback;
|
||||
mod udp;
|
||||
|
||||
pub use loopback::{loopback_pair, LoopbackTransport};
|
||||
pub use udp::UdpTransport;
|
||||
|
||||
/// A datagram transport. `recv` is non-blocking: it returns `Ok(None)` when no packet
|
||||
/// is currently available, so the caller (decode/present thread) never blocks here.
|
||||
pub trait Transport: Send + Sync {
|
||||
fn send(&self, packet: &[u8]) -> std::io::Result<()>;
|
||||
fn recv(&self) -> std::io::Result<Option<Vec<u8>>>;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
//! Real UDP datagram transport — native sockets, no async runtime.
|
||||
//!
|
||||
//! M1 uses one `recv` syscall per packet; the latency budget (§7) calls for
|
||||
//! `sendmmsg`/UDP-GSO batching to cut syscalls, which is a P2 optimization layered on
|
||||
//! this same [`Transport`] seam.
|
||||
|
||||
use super::Transport;
|
||||
use crate::packet::MAX_DATAGRAM_BYTES;
|
||||
use std::net::UdpSocket;
|
||||
|
||||
/// Receive buffer size. `Config::validate` bounds `shard_payload` so a well-formed
|
||||
/// datagram (header + shard + crypto overhead) always fits in [`MAX_DATAGRAM_BYTES`];
|
||||
/// the `+ 1` byte lets us detect an oversized datagram (a full read) instead of
|
||||
/// silently truncating it.
|
||||
const RECV_BUF: usize = MAX_DATAGRAM_BYTES + 1;
|
||||
|
||||
pub struct UdpTransport {
|
||||
socket: UdpSocket,
|
||||
}
|
||||
|
||||
impl UdpTransport {
|
||||
/// Bind `local` and `connect` to `peer`, so `send`/`recv` need no address and the
|
||||
/// kernel filters to this peer. Non-blocking, matching the [`Transport`] contract.
|
||||
pub fn connect(local: &str, peer: &str) -> std::io::Result<Self> {
|
||||
let socket = UdpSocket::bind(local)?;
|
||||
socket.connect(peer)?;
|
||||
socket.set_nonblocking(true)?;
|
||||
Ok(UdpTransport { socket })
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for UdpTransport {
|
||||
fn send(&self, packet: &[u8]) -> std::io::Result<()> {
|
||||
self.socket.send(packet)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn recv(&self) -> std::io::Result<Option<Vec<u8>>> {
|
||||
let mut buf = vec![0u8; RECV_BUF];
|
||||
match self.socket.recv(&mut buf) {
|
||||
// A read that fills the whole buffer means the datagram was larger than any
|
||||
// valid packet — drop it rather than hand a truncated, corrupt packet up.
|
||||
Ok(n) if n >= RECV_BUF => Ok(None),
|
||||
Ok(n) => {
|
||||
buf.truncate(n);
|
||||
Ok(Some(buf))
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user