Skip to main content

strate_webrtc/
main.rs

1#![no_std]
2#![no_main]
3#![feature(alloc_error_handler)]
4
5extern crate alloc;
6
7use alloc::{
8    collections::{BTreeMap, VecDeque},
9    format,
10    string::String,
11    vec::Vec,
12};
13use core::{
14    alloc::Layout,
15    panic::PanicInfo,
16    sync::atomic::{AtomicU64, Ordering},
17};
18use strat9_syscall::{call, data::IpcMessage, CLOCK_MONOTONIC};
19
20alloc_freelist::define_freelist_brk_allocator!(
21    pub struct BumpAllocator;
22    brk = strat9_syscall::call::brk;
23    heap_max = 16 * 1024 * 1024;
24);
25
26#[global_allocator]
27static ALLOCATOR: BumpAllocator = BumpAllocator;
28
29#[alloc_error_handler]
30fn alloc_error(_layout: Layout) -> ! {
31    let _ = call::debug_log(b"[strate-webrtc] OOM\n");
32    call::exit(12);
33}
34
35#[panic_handler]
36fn panic(_info: &PanicInfo) -> ! {
37    let _ = call::debug_log(b"[strate-webrtc] PANIC\n");
38    call::exit(1);
39}
40
41const OP_BOOTSTRAP: u32 = 0x10;
42const OP_SESSION_OPEN: u32 = 0x300;
43const OP_SESSION_CLOSE: u32 = 0x301;
44const OP_SESSION_PUT_OFFER: u32 = 0x302;
45const OP_SESSION_GET_OFFER: u32 = 0x303;
46const OP_SESSION_PUT_ANSWER: u32 = 0x304;
47const OP_SESSION_GET_ANSWER: u32 = 0x305;
48const OP_SESSION_ADD_CANDIDATE: u32 = 0x306;
49const OP_SESSION_POP_CANDIDATE: u32 = 0x307;
50const OP_INPUT_EVENT: u32 = 0x308;
51const OP_FRAME_PUSH: u32 = 0x309;
52const OP_SESSION_INFO: u32 = 0x30A;
53const OP_POLICY_REFRESH: u32 = 0x30B;
54
55const REPLY_MSG_TYPE: u32 = 0x80;
56const RESP_OK: u32 = 0;
57const RESP_BAD_REQ: u32 = 1;
58const RESP_NOT_FOUND: u32 = 2;
59const RESP_DENIED: u32 = 3;
60const RESP_DISABLED: u32 = 4;
61const RESP_FULL: u32 = 5;
62const RESP_EXPIRED: u32 = 6;
63
64const SILO_FLAG_GRAPHICS: u64 = 1 << 1;
65const SILO_FLAG_WEBRTC_NATIVE: u64 = 1 << 2;
66const SILO_FLAG_GRAPHICS_READ_ONLY: u64 = 1 << 3;
67
68const MAX_SESSIONS: usize = 32;
69const MAX_SDP_BYTES: usize = 256;
70const MAX_CANDIDATES: usize = 32;
71
72#[derive(Clone, Copy)]
73struct SiloGraphicsPolicy {
74    flags: u64,
75    max_sessions: u16,
76    ttl_sec: u32,
77}
78
79#[derive(Clone)]
80struct Candidate {
81    direction: u8,
82    data: [u8; 38],
83    len: u8,
84}
85
86struct Session {
87    id: u64,
88    silo_id: u32,
89    token: u64,
90    flags: u64,
91    expires_at_ns: u64,
92    offer_len: u16,
93    answer_len: u16,
94    offer: [u8; MAX_SDP_BYTES],
95    answer: [u8; MAX_SDP_BYTES],
96    candidates: VecDeque<Candidate>,
97    last_input: [u8; 24],
98    last_input_len: u8,
99    last_frame_seq: u64,
100    udp_rx_fd: Option<usize>,
101    udp_tx_fd: Option<usize>,
102    udp_local_port: u16,
103}
104
105impl Session {
106    fn new(
107        id: u64,
108        silo_id: u32,
109        token: u64,
110        flags: u64,
111        ttl_sec: u32,
112        now_ns: u64,
113        udp_local_port: u16,
114        udp_rx_fd: Option<usize>,
115    ) -> Self {
116        Self {
117            id,
118            silo_id,
119            token,
120            flags,
121            expires_at_ns: now_ns.saturating_add((ttl_sec as u64).saturating_mul(1_000_000_000)),
122            offer_len: 0,
123            answer_len: 0,
124            offer: [0u8; MAX_SDP_BYTES],
125            answer: [0u8; MAX_SDP_BYTES],
126            candidates: VecDeque::new(),
127            last_input: [0u8; 24],
128            last_input_len: 0,
129            last_frame_seq: 0,
130            udp_rx_fd,
131            udp_tx_fd: None,
132            udp_local_port,
133        }
134    }
135
136    fn is_expired(&self, now_ns: u64) -> bool {
137        now_ns >= self.expires_at_ns
138    }
139}
140
141struct Runtime {
142    sessions: BTreeMap<u64, Session>,
143    policies: BTreeMap<u32, SiloGraphicsPolicy>,
144}
145
146impl Runtime {
147    fn new() -> Self {
148        Self {
149            sessions: BTreeMap::new(),
150            policies: BTreeMap::new(),
151        }
152    }
153}
154
155static NEXT_SESSION_ID: AtomicU64 = AtomicU64::new(1);
156
157fn log(msg: &str) {
158    let _ = call::debug_log(msg.as_bytes());
159}
160
161fn now_ns() -> u64 {
162    let mut ts = strat9_syscall::data::TimeSpec::zero();
163    if call::clock_gettime(CLOCK_MONOTONIC, &mut ts).is_ok() {
164        ts.to_nanos()
165    } else {
166        NEXT_SESSION_ID.load(Ordering::Relaxed)
167    }
168}
169
170fn derive_token(session_id: u64, sid: u32, t: u64) -> u64 {
171    let mut x = session_id ^ ((sid as u64) << 32) ^ t.rotate_left(17);
172    x ^= x >> 33;
173    x = x.wrapping_mul(0xff51afd7ed558ccd);
174    x ^= x >> 33;
175    x = x.wrapping_mul(0xc4ceb9fe1a85ec53);
176    x ^ (x >> 33)
177}
178
179fn parse_u16(payload: &[u8], off: usize) -> Option<u16> {
180    let end = off.checked_add(2)?;
181    if end > payload.len() {
182        return None;
183    }
184    Some(u16::from_le_bytes([payload[off], payload[off + 1]]))
185}
186
187fn parse_u32(payload: &[u8], off: usize) -> Option<u32> {
188    let end = off.checked_add(4)?;
189    if end > payload.len() {
190        return None;
191    }
192    Some(u32::from_le_bytes([
193        payload[off],
194        payload[off + 1],
195        payload[off + 2],
196        payload[off + 3],
197    ]))
198}
199
200fn parse_u64(payload: &[u8], off: usize) -> Option<u64> {
201    let end = off.checked_add(8)?;
202    if end > payload.len() {
203        return None;
204    }
205    Some(u64::from_le_bytes([
206        payload[off],
207        payload[off + 1],
208        payload[off + 2],
209        payload[off + 3],
210        payload[off + 4],
211        payload[off + 5],
212        payload[off + 6],
213        payload[off + 7],
214    ]))
215}
216
217fn read_text_file(path: &str) -> Option<String> {
218    let fd = call::openat(0, path, 0x1, 0).ok()?;
219    let mut out = Vec::new();
220    let mut chunk = [0u8; 512];
221    loop {
222        match call::read(fd as usize, &mut chunk) {
223            Ok(0) => break,
224            Ok(n) => out.extend_from_slice(&chunk[..n]),
225            Err(_) => {
226                let _ = call::close(fd as usize);
227                return None;
228            }
229        }
230        if out.len() > 256 * 1024 {
231            break;
232        }
233    }
234    let _ = call::close(fd as usize);
235    core::str::from_utf8(&out).ok().map(String::from)
236}
237
238fn refresh_policies(rt: &mut Runtime) {
239    let mut next = BTreeMap::new();
240    let Some(text) = read_text_file("/proc/silos") else {
241        rt.policies = next;
242        return;
243    };
244
245    for (idx, line) in text.lines().enumerate() {
246        if idx == 0 || line.is_empty() {
247            continue;
248        }
249        let mut cols = line.split('\t');
250        let sid = cols.next().and_then(|v| v.parse::<u32>().ok());
251        let _state = cols.next();
252        let _tasks = cols.next();
253        let _mem_used = cols.next();
254        let _mem_min = cols.next();
255        let _mem_max = cols.next();
256        let flags = cols.next().and_then(|v| v.parse::<u64>().ok());
257        let max_sessions = cols.next().and_then(|v| v.parse::<u16>().ok());
258        let ttl = cols.next().and_then(|v| v.parse::<u32>().ok());
259
260        let (Some(sid), Some(flags), Some(max_sessions), Some(ttl_sec)) =
261            (sid, flags, max_sessions, ttl)
262        else {
263            continue;
264        };
265        next.insert(
266            sid,
267            SiloGraphicsPolicy {
268                flags,
269                max_sessions,
270                ttl_sec,
271            },
272        );
273    }
274    rt.policies = next;
275}
276
277fn cleanup_expired(rt: &mut Runtime, now: u64) {
278    let mut expired = Vec::new();
279    for (id, s) in rt.sessions.iter() {
280        if s.is_expired(now) {
281            expired.push(*id);
282        }
283    }
284    for id in expired {
285        if let Some(mut s) = rt.sessions.remove(&id) {
286            if let Some(fd) = s.udp_rx_fd.take() {
287                let _ = call::close(fd);
288            }
289            if let Some(fd) = s.udp_tx_fd.take() {
290                let _ = call::close(fd);
291            }
292        }
293    }
294}
295
296fn parse_ipv4_addr(s: &str) -> Option<[u8; 4]> {
297    let mut out = [0u8; 4];
298    let mut idx = 0usize;
299    let mut val: u16 = 0;
300    let mut has = false;
301    for &b in s.as_bytes() {
302        if b == b'.' {
303            if !has || idx >= 3 || val > 255 {
304                return None;
305            }
306            out[idx] = val as u8;
307            idx += 1;
308            val = 0;
309            has = false;
310            continue;
311        }
312        if !b.is_ascii_digit() {
313            return None;
314        }
315        val = val * 10 + (b - b'0') as u16;
316        has = true;
317    }
318    if !has || idx != 3 || val > 255 {
319        return None;
320    }
321    out[3] = val as u8;
322    Some(out)
323}
324
325fn read_local_ipv4() -> Option<[u8; 4]> {
326    let text = read_text_file("/net/address")?;
327    let first = text.lines().next()?.trim();
328    let ip_only = first.split('/').next().unwrap_or(first).trim();
329    parse_ipv4_addr(ip_only)
330}
331
332fn parse_endpoint_candidate(raw: &[u8]) -> Option<([u8; 4], u16)> {
333    let s = core::str::from_utf8(raw).ok()?.trim();
334    let s = if let Some(rest) = s.strip_prefix("udp4:") {
335        rest
336    } else {
337        s
338    };
339    let (ip_s, port_s) = s.rsplit_once(':')?;
340    let ip = parse_ipv4_addr(ip_s.trim())?;
341    let port = port_s.trim().parse::<u16>().ok()?;
342    if port == 0 {
343        return None;
344    }
345    Some((ip, port))
346}
347
348fn encode_endpoint_candidate(ip: [u8; 4], port: u16, out: &mut [u8; 38]) -> usize {
349    let s = format!("udp4:{}.{}.{}.{}:{}", ip[0], ip[1], ip[2], ip[3], port);
350    let b = s.as_bytes();
351    let n = core::cmp::min(b.len(), out.len());
352    out[..n].copy_from_slice(&b[..n]);
353    n
354}
355
356fn open_udp_bind(local_port: u16) -> Option<usize> {
357    let path = format!("/net/udp/bind/{}", local_port);
358    call::openat(0, &path, 0x3, 0).ok().map(|fd| fd as usize)
359}
360
361fn open_udp_connect(ip: [u8; 4], port: u16) -> Option<usize> {
362    let path = format!(
363        "/net/udp/connect/{}.{}.{}.{}/{}",
364        ip[0], ip[1], ip[2], ip[3], port
365    );
366    call::openat(0, &path, 0x3, 0).ok().map(|fd| fd as usize)
367}
368
369fn send_response(sender: u64, status: u32, fill: impl FnOnce(&mut IpcMessage)) {
370    let mut msg = IpcMessage::new(REPLY_MSG_TYPE);
371    msg.sender = sender;
372    msg.payload[0..4].copy_from_slice(&status.to_le_bytes());
373    fill(&mut msg);
374    let _ = call::ipc_reply(&msg);
375}
376
377fn bind_alias(port_h: usize, label: &str) {
378    let p = format!("/srv/strate-webrtc/{}", label);
379    let _ = call::ipc_bind_port(port_h, p.as_bytes());
380}
381
382fn extract_label(payload: &[u8]) -> String {
383    let len = payload[0] as usize;
384    if len == 0 {
385        return String::from("default");
386    }
387    let end = core::cmp::min(1 + len, payload.len());
388    let raw = &payload[1..end];
389    let mut out = String::new();
390    for &b in raw {
391        let c = b as char;
392        if c.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b'.' {
393            out.push(c);
394        }
395    }
396    if out.is_empty() {
397        String::from("default")
398    } else {
399        out
400    }
401}
402
403#[unsafe(no_mangle)]
404pub unsafe extern "C" fn _start() -> ! {
405    let mut rt = Runtime::new();
406    let port_h = call::ipc_create_port(0).unwrap_or_else(|_| call::exit(2));
407
408    let _ = call::ipc_bind_port(port_h, b"/srv/strate-webrtc/bootstrap");
409    bind_alias(port_h, "default");
410    log("[strate-webrtc] online\n");
411
412    loop {
413        cleanup_expired(&mut rt, now_ns());
414        for s in rt.sessions.values_mut() {
415            let Some(fd) = s.udp_rx_fd else {
416                continue;
417            };
418            let mut buf = [0u8; 64];
419            if let Ok(n) = call::read(fd, &mut buf) {
420                if n >= 6 {
421                    if s.udp_tx_fd.is_none() {
422                        let src_ip = [buf[0], buf[1], buf[2], buf[3]];
423                        let src_port = u16::from_be_bytes([buf[4], buf[5]]);
424                        s.udp_tx_fd = open_udp_connect(src_ip, src_port);
425                        let mut cand = Candidate {
426                            direction: 2,
427                            data: [0u8; 38],
428                            len: 0,
429                        };
430                        let clen = encode_endpoint_candidate(src_ip, src_port, &mut cand.data);
431                        cand.len = clen as u8;
432                        if s.candidates.len() >= MAX_CANDIDATES {
433                            let _ = s.candidates.pop_front();
434                        }
435                        s.candidates.push_back(cand);
436                    }
437                    let m = core::cmp::min(n.saturating_sub(6), 24);
438                    s.last_input[..m].copy_from_slice(&buf[6..6 + m]);
439                    s.last_input_len = m as u8;
440                    s.last_frame_seq = s.last_frame_seq.wrapping_add(1);
441                }
442            }
443        }
444
445        let mut msg = IpcMessage::new(0);
446        if call::ipc_try_recv(port_h, &mut msg).is_err() {
447            let _ = call::sched_yield();
448            continue;
449        }
450
451        match msg.msg_type {
452            OP_BOOTSTRAP => {
453                let label = extract_label(&msg.payload);
454                bind_alias(port_h, &label);
455                send_response(msg.sender, RESP_OK, |_| {});
456            }
457            OP_POLICY_REFRESH => {
458                refresh_policies(&mut rt);
459                send_response(msg.sender, RESP_OK, |_| {});
460            }
461            OP_SESSION_OPEN => {
462                refresh_policies(&mut rt);
463                let sid = parse_u32(&msg.payload, 0).unwrap_or(0);
464                let force_ro = msg.payload[4] != 0;
465                let Some(policy) = rt.policies.get(&sid).copied() else {
466                    send_response(msg.sender, RESP_NOT_FOUND, |_| {});
467                    continue;
468                };
469                if policy.flags & SILO_FLAG_GRAPHICS == 0 {
470                    send_response(msg.sender, RESP_DISABLED, |_| {});
471                    continue;
472                }
473                if policy.flags & SILO_FLAG_WEBRTC_NATIVE == 0 {
474                    send_response(msg.sender, RESP_DENIED, |_| {});
475                    continue;
476                }
477                let active_for_sid = rt.sessions.values().filter(|s| s.silo_id == sid).count();
478                if rt.sessions.len() >= MAX_SESSIONS
479                    || active_for_sid >= policy.max_sessions as usize
480                {
481                    send_response(msg.sender, RESP_FULL, |_| {});
482                    continue;
483                }
484
485                let id = NEXT_SESSION_ID.fetch_add(1, Ordering::Relaxed);
486                let t = now_ns();
487                let mut flags = policy.flags;
488                if force_ro {
489                    flags |= SILO_FLAG_GRAPHICS_READ_ONLY;
490                }
491                let token = derive_token(id, sid, t);
492                let local_port = 40_000u16.saturating_add((id as u16) & 0x1FFF);
493                let udp_rx_fd = open_udp_bind(local_port);
494                let mut sess = Session::new(
495                    id,
496                    sid,
497                    token,
498                    flags,
499                    policy.ttl_sec,
500                    t,
501                    local_port,
502                    udp_rx_fd,
503                );
504                if let Some(ip) = read_local_ipv4() {
505                    let mut cand = Candidate {
506                        direction: 0,
507                        data: [0u8; 38],
508                        len: 0,
509                    };
510                    let clen = encode_endpoint_candidate(ip, local_port, &mut cand.data);
511                    cand.len = clen as u8;
512                    sess.candidates.push_back(cand);
513                }
514                rt.sessions.insert(id, sess);
515
516                send_response(msg.sender, RESP_OK, |out| {
517                    out.payload[4..12].copy_from_slice(&id.to_le_bytes());
518                    out.payload[12..20].copy_from_slice(&token.to_le_bytes());
519                    out.payload[20..24].copy_from_slice(&policy.ttl_sec.to_le_bytes());
520                });
521            }
522            OP_SESSION_CLOSE => {
523                let Some(id) = parse_u64(&msg.payload, 0) else {
524                    send_response(msg.sender, RESP_BAD_REQ, |_| {});
525                    continue;
526                };
527                if let Some(mut s) = rt.sessions.remove(&id) {
528                    if let Some(fd) = s.udp_rx_fd.take() {
529                        let _ = call::close(fd);
530                    }
531                    if let Some(fd) = s.udp_tx_fd.take() {
532                        let _ = call::close(fd);
533                    }
534                    send_response(msg.sender, RESP_OK, |_| {});
535                } else {
536                    send_response(msg.sender, RESP_NOT_FOUND, |_| {});
537                }
538            }
539            OP_SESSION_PUT_OFFER | OP_SESSION_PUT_ANSWER => {
540                let Some(id) = parse_u64(&msg.payload, 0) else {
541                    send_response(msg.sender, RESP_BAD_REQ, |_| {});
542                    continue;
543                };
544                let Some(len) = parse_u16(&msg.payload, 8) else {
545                    send_response(msg.sender, RESP_BAD_REQ, |_| {});
546                    continue;
547                };
548                let len = core::cmp::min(len as usize, 38);
549                let Some(s) = rt.sessions.get_mut(&id) else {
550                    send_response(msg.sender, RESP_NOT_FOUND, |_| {});
551                    continue;
552                };
553                if s.is_expired(now_ns()) {
554                    let _ = rt.sessions.remove(&id);
555                    send_response(msg.sender, RESP_EXPIRED, |_| {});
556                    continue;
557                }
558                if msg.msg_type == OP_SESSION_PUT_OFFER {
559                    let dst_off = s.offer_len as usize;
560                    let n = core::cmp::min(len, MAX_SDP_BYTES.saturating_sub(dst_off));
561                    s.offer[dst_off..dst_off + n].copy_from_slice(&msg.payload[10..10 + n]);
562                    s.offer_len = (dst_off + n) as u16;
563                } else {
564                    let dst_off = s.answer_len as usize;
565                    let n = core::cmp::min(len, MAX_SDP_BYTES.saturating_sub(dst_off));
566                    s.answer[dst_off..dst_off + n].copy_from_slice(&msg.payload[10..10 + n]);
567                    s.answer_len = (dst_off + n) as u16;
568                }
569                send_response(msg.sender, RESP_OK, |_| {});
570            }
571            OP_SESSION_GET_OFFER | OP_SESSION_GET_ANSWER => {
572                let Some(id) = parse_u64(&msg.payload, 0) else {
573                    send_response(msg.sender, RESP_BAD_REQ, |_| {});
574                    continue;
575                };
576                let Some(offset) = parse_u16(&msg.payload, 8) else {
577                    send_response(msg.sender, RESP_BAD_REQ, |_| {});
578                    continue;
579                };
580                let Some(s) = rt.sessions.get(&id) else {
581                    send_response(msg.sender, RESP_NOT_FOUND, |_| {});
582                    continue;
583                };
584                if s.is_expired(now_ns()) {
585                    send_response(msg.sender, RESP_EXPIRED, |_| {});
586                    continue;
587                }
588                let (buf, total_len) = if msg.msg_type == OP_SESSION_GET_OFFER {
589                    (&s.offer[..], s.offer_len as usize)
590                } else {
591                    (&s.answer[..], s.answer_len as usize)
592                };
593                let off = core::cmp::min(offset as usize, total_len);
594                let n = core::cmp::min(38, total_len.saturating_sub(off));
595                send_response(msg.sender, RESP_OK, |out| {
596                    out.payload[4..6].copy_from_slice(&(total_len as u16).to_le_bytes());
597                    out.payload[6..8].copy_from_slice(&(off as u16).to_le_bytes());
598                    out.payload[8..10].copy_from_slice(&(n as u16).to_le_bytes());
599                    out.payload[10..10 + n].copy_from_slice(&buf[off..off + n]);
600                });
601            }
602            OP_SESSION_ADD_CANDIDATE => {
603                let Some(id) = parse_u64(&msg.payload, 0) else {
604                    send_response(msg.sender, RESP_BAD_REQ, |_| {});
605                    continue;
606                };
607                let dir = msg.payload[8];
608                let len = core::cmp::min(msg.payload[9] as usize, 38);
609                let Some(s) = rt.sessions.get_mut(&id) else {
610                    send_response(msg.sender, RESP_NOT_FOUND, |_| {});
611                    continue;
612                };
613                if s.candidates.len() >= MAX_CANDIDATES {
614                    let _ = s.candidates.pop_front();
615                }
616                let mut c = Candidate {
617                    direction: dir,
618                    data: [0u8; 38],
619                    len: len as u8,
620                };
621                c.data[..len].copy_from_slice(&msg.payload[10..10 + len]);
622                s.candidates.push_back(c);
623                if let Some((ip, port)) = parse_endpoint_candidate(&msg.payload[10..10 + len]) {
624                    if let Some(old) = s.udp_tx_fd.take() {
625                        let _ = call::close(old);
626                    }
627                    s.udp_tx_fd = open_udp_connect(ip, port);
628                }
629                send_response(msg.sender, RESP_OK, |_| {});
630            }
631            OP_SESSION_POP_CANDIDATE => {
632                let Some(id) = parse_u64(&msg.payload, 0) else {
633                    send_response(msg.sender, RESP_BAD_REQ, |_| {});
634                    continue;
635                };
636                let Some(s) = rt.sessions.get_mut(&id) else {
637                    send_response(msg.sender, RESP_NOT_FOUND, |_| {});
638                    continue;
639                };
640                if let Some(c) = s.candidates.pop_front() {
641                    send_response(msg.sender, RESP_OK, |out| {
642                        out.payload[4] = c.direction;
643                        out.payload[5] = c.len;
644                        let n = c.len as usize;
645                        out.payload[6..6 + n].copy_from_slice(&c.data[..n]);
646                    });
647                } else {
648                    send_response(msg.sender, RESP_NOT_FOUND, |_| {});
649                }
650            }
651            OP_INPUT_EVENT => {
652                let Some(id) = parse_u64(&msg.payload, 0) else {
653                    send_response(msg.sender, RESP_BAD_REQ, |_| {});
654                    continue;
655                };
656                let Some(s) = rt.sessions.get_mut(&id) else {
657                    send_response(msg.sender, RESP_NOT_FOUND, |_| {});
658                    continue;
659                };
660                let n = core::cmp::min(msg.payload[8] as usize, 24);
661                s.last_input_len = n as u8;
662                s.last_input[..n].copy_from_slice(&msg.payload[9..9 + n]);
663                send_response(msg.sender, RESP_OK, |_| {});
664            }
665            OP_FRAME_PUSH => {
666                let Some(id) = parse_u64(&msg.payload, 0) else {
667                    send_response(msg.sender, RESP_BAD_REQ, |_| {});
668                    continue;
669                };
670                let Some(s) = rt.sessions.get_mut(&id) else {
671                    send_response(msg.sender, RESP_NOT_FOUND, |_| {});
672                    continue;
673                };
674                s.last_frame_seq = s.last_frame_seq.wrapping_add(1);
675                let n = core::cmp::min(msg.payload[8] as usize, 38);
676                if let Some(fd) = s.udp_tx_fd {
677                    let _ = call::write(fd, &msg.payload[9..9 + n]);
678                }
679                send_response(msg.sender, RESP_OK, |out| {
680                    out.payload[4..12].copy_from_slice(&s.last_frame_seq.to_le_bytes());
681                });
682            }
683            OP_SESSION_INFO => {
684                let Some(id) = parse_u64(&msg.payload, 0) else {
685                    send_response(msg.sender, RESP_BAD_REQ, |_| {});
686                    continue;
687                };
688                let Some(s) = rt.sessions.get(&id) else {
689                    send_response(msg.sender, RESP_NOT_FOUND, |_| {});
690                    continue;
691                };
692                send_response(msg.sender, RESP_OK, |out| {
693                    out.payload[4..12].copy_from_slice(&s.id.to_le_bytes());
694                    out.payload[12..16].copy_from_slice(&s.silo_id.to_le_bytes());
695                    out.payload[16..24].copy_from_slice(&s.token.to_le_bytes());
696                    out.payload[24..32].copy_from_slice(&s.flags.to_le_bytes());
697                    out.payload[32..40].copy_from_slice(&s.expires_at_ns.to_le_bytes());
698                    out.payload[40..42].copy_from_slice(&s.udp_local_port.to_le_bytes());
699                    out.payload[42] = if s.udp_rx_fd.is_some() { 1 } else { 0 };
700                    out.payload[43] = if s.udp_tx_fd.is_some() { 1 } else { 0 };
701                });
702            }
703            _ => send_response(msg.sender, RESP_BAD_REQ, |_| {}),
704        }
705    }
706}