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_abi::ip::parse_ipv4_literal;
19use strat9_syscall::{call, data::IpcMessage, CLOCK_MONOTONIC};
20
21alloc_freelist::define_freelist_brk_allocator!(
22    pub struct BumpAllocator;
23    brk = strat9_syscall::call::brk;
24    heap_max = 16 * 1024 * 1024;
25);
26
27#[global_allocator]
28static ALLOCATOR: BumpAllocator = BumpAllocator;
29
30#[alloc_error_handler]
31fn alloc_error(_layout: Layout) -> ! {
32    let _ = call::debug_log(b"[strate-webrtc] OOM\n");
33    call::exit(12);
34}
35
36#[panic_handler]
37fn panic(info: &PanicInfo) -> ! {
38    call::handle_panic("strate-webrtc", info);
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 read_local_ipv4() -> Option<[u8; 4]> {
297    let text = read_text_file("/net/address")?;
298    let first = text.lines().next()?.trim();
299    let ip_only = first.split('/').next().unwrap_or(first).trim();
300    parse_ipv4_literal(ip_only)
301}
302
303fn parse_endpoint_candidate(raw: &[u8]) -> Option<([u8; 4], u16)> {
304    let s = core::str::from_utf8(raw).ok()?.trim();
305    let s = if let Some(rest) = s.strip_prefix("udp4:") {
306        rest
307    } else {
308        s
309    };
310    let (ip_s, port_s) = s.rsplit_once(':')?;
311    let ip = parse_ipv4_literal(ip_s.trim())?;
312    let port = port_s.trim().parse::<u16>().ok()?;
313    if port == 0 {
314        return None;
315    }
316    Some((ip, port))
317}
318
319fn encode_endpoint_candidate(ip: [u8; 4], port: u16, out: &mut [u8; 38]) -> usize {
320    let s = format!("udp4:{}.{}.{}.{}:{}", ip[0], ip[1], ip[2], ip[3], port);
321    let b = s.as_bytes();
322    let n = core::cmp::min(b.len(), out.len());
323    out[..n].copy_from_slice(&b[..n]);
324    n
325}
326
327fn open_udp_bind(local_port: u16) -> Option<usize> {
328    let path = format!("/net/udp/bind/{}", local_port);
329    call::openat(0, &path, 0x3, 0).ok().map(|fd| fd)
330}
331
332fn open_udp_connect(ip: [u8; 4], port: u16) -> Option<usize> {
333    let path = format!(
334        "/net/udp/connect/{}.{}.{}.{}/{}",
335        ip[0], ip[1], ip[2], ip[3], port
336    );
337    call::openat(0, &path, 0x3, 0).ok().map(|fd| fd)
338}
339
340fn send_response(sender: u64, status: u32, fill: impl FnOnce(&mut IpcMessage)) {
341    let mut msg = IpcMessage::new(REPLY_MSG_TYPE);
342    msg.sender = sender;
343    msg.payload[0..4].copy_from_slice(&status.to_le_bytes());
344    fill(&mut msg);
345    let _ = call::ipc_reply(&msg);
346}
347
348fn bind_alias(port_h: usize, label: &str) {
349    let p = format!("/srv/strate-webrtc/{}", label);
350    let _ = call::ipc_bind_port(port_h, p.as_bytes());
351}
352
353fn extract_label(payload: &[u8]) -> String {
354    let len = payload[0] as usize;
355    if len == 0 {
356        return String::from("default");
357    }
358    let end = core::cmp::min(1 + len, payload.len());
359    let raw = &payload[1..end];
360    let mut out = String::new();
361    for &b in raw {
362        let c = b as char;
363        if c.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b'.' {
364            out.push(c);
365        }
366    }
367    if out.is_empty() {
368        String::from("default")
369    } else {
370        out
371    }
372}
373
374#[unsafe(no_mangle)]
375pub unsafe extern "C" fn _start() -> ! {
376    let mut rt = Runtime::new();
377    let port_h = call::ipc_create_port(0).unwrap_or_else(|_| call::exit(2));
378
379    let _ = call::ipc_bind_port(port_h, b"/srv/strate-webrtc/bootstrap");
380    bind_alias(port_h, "default");
381    log("[strate-webrtc] online\n");
382
383    loop {
384        cleanup_expired(&mut rt, now_ns());
385        for s in rt.sessions.values_mut() {
386            let Some(fd) = s.udp_rx_fd else {
387                continue;
388            };
389            let mut buf = [0u8; 64];
390            if let Ok(n) = call::read(fd, &mut buf) {
391                if n >= 6 {
392                    if s.udp_tx_fd.is_none() {
393                        let src_ip = [buf[0], buf[1], buf[2], buf[3]];
394                        let src_port = u16::from_be_bytes([buf[4], buf[5]]);
395                        s.udp_tx_fd = open_udp_connect(src_ip, src_port);
396                        let mut cand = Candidate {
397                            direction: 2,
398                            data: [0u8; 38],
399                            len: 0,
400                        };
401                        let clen = encode_endpoint_candidate(src_ip, src_port, &mut cand.data);
402                        cand.len = clen as u8;
403                        if s.candidates.len() >= MAX_CANDIDATES {
404                            let _ = s.candidates.pop_front();
405                        }
406                        s.candidates.push_back(cand);
407                    }
408                    let m = core::cmp::min(n.saturating_sub(6), 24);
409                    s.last_input[..m].copy_from_slice(&buf[6..6 + m]);
410                    s.last_input_len = m as u8;
411                    s.last_frame_seq = s.last_frame_seq.wrapping_add(1);
412                }
413            }
414        }
415
416        let mut msg = IpcMessage::new(0);
417        if call::ipc_try_recv(port_h, &mut msg).is_err() {
418            let _ = call::sched_yield();
419            continue;
420        }
421
422        match msg.msg_type {
423            OP_BOOTSTRAP => {
424                let label = extract_label(&msg.payload);
425                bind_alias(port_h, &label);
426                send_response(msg.sender, RESP_OK, |_| {});
427            }
428            OP_POLICY_REFRESH => {
429                refresh_policies(&mut rt);
430                send_response(msg.sender, RESP_OK, |_| {});
431            }
432            OP_SESSION_OPEN => {
433                refresh_policies(&mut rt);
434                let sid = parse_u32(&msg.payload, 0).unwrap_or(0);
435                let force_ro = msg.payload[4] != 0;
436                let Some(policy) = rt.policies.get(&sid).copied() else {
437                    send_response(msg.sender, RESP_NOT_FOUND, |_| {});
438                    continue;
439                };
440                if policy.flags & SILO_FLAG_GRAPHICS == 0 {
441                    send_response(msg.sender, RESP_DISABLED, |_| {});
442                    continue;
443                }
444                if policy.flags & SILO_FLAG_WEBRTC_NATIVE == 0 {
445                    send_response(msg.sender, RESP_DENIED, |_| {});
446                    continue;
447                }
448                let active_for_sid = rt.sessions.values().filter(|s| s.silo_id == sid).count();
449                if rt.sessions.len() >= MAX_SESSIONS
450                    || active_for_sid >= policy.max_sessions as usize
451                {
452                    send_response(msg.sender, RESP_FULL, |_| {});
453                    continue;
454                }
455
456                let id = NEXT_SESSION_ID.fetch_add(1, Ordering::Relaxed);
457                let t = now_ns();
458                let mut flags = policy.flags;
459                if force_ro {
460                    flags |= SILO_FLAG_GRAPHICS_READ_ONLY;
461                }
462                let token = derive_token(id, sid, t);
463                let local_port = 40_000u16.saturating_add((id as u16) & 0x1FFF);
464                let udp_rx_fd = open_udp_bind(local_port);
465                let mut sess = Session::new(
466                    id,
467                    sid,
468                    token,
469                    flags,
470                    policy.ttl_sec,
471                    t,
472                    local_port,
473                    udp_rx_fd,
474                );
475                if let Some(ip) = read_local_ipv4() {
476                    let mut cand = Candidate {
477                        direction: 0,
478                        data: [0u8; 38],
479                        len: 0,
480                    };
481                    let clen = encode_endpoint_candidate(ip, local_port, &mut cand.data);
482                    cand.len = clen as u8;
483                    sess.candidates.push_back(cand);
484                }
485                rt.sessions.insert(id, sess);
486
487                send_response(msg.sender, RESP_OK, |out| {
488                    out.payload[4..12].copy_from_slice(&id.to_le_bytes());
489                    out.payload[12..20].copy_from_slice(&token.to_le_bytes());
490                    out.payload[20..24].copy_from_slice(&policy.ttl_sec.to_le_bytes());
491                });
492            }
493            OP_SESSION_CLOSE => {
494                let Some(id) = parse_u64(&msg.payload, 0) else {
495                    send_response(msg.sender, RESP_BAD_REQ, |_| {});
496                    continue;
497                };
498                if let Some(mut s) = rt.sessions.remove(&id) {
499                    if let Some(fd) = s.udp_rx_fd.take() {
500                        let _ = call::close(fd);
501                    }
502                    if let Some(fd) = s.udp_tx_fd.take() {
503                        let _ = call::close(fd);
504                    }
505                    send_response(msg.sender, RESP_OK, |_| {});
506                } else {
507                    send_response(msg.sender, RESP_NOT_FOUND, |_| {});
508                }
509            }
510            OP_SESSION_PUT_OFFER | OP_SESSION_PUT_ANSWER => {
511                let Some(id) = parse_u64(&msg.payload, 0) else {
512                    send_response(msg.sender, RESP_BAD_REQ, |_| {});
513                    continue;
514                };
515                let Some(len) = parse_u16(&msg.payload, 8) else {
516                    send_response(msg.sender, RESP_BAD_REQ, |_| {});
517                    continue;
518                };
519                let len = core::cmp::min(len as usize, 38);
520                let Some(s) = rt.sessions.get_mut(&id) else {
521                    send_response(msg.sender, RESP_NOT_FOUND, |_| {});
522                    continue;
523                };
524                if s.is_expired(now_ns()) {
525                    let _ = rt.sessions.remove(&id);
526                    send_response(msg.sender, RESP_EXPIRED, |_| {});
527                    continue;
528                }
529                if msg.msg_type == OP_SESSION_PUT_OFFER {
530                    let dst_off = s.offer_len as usize;
531                    let n = core::cmp::min(len, MAX_SDP_BYTES.saturating_sub(dst_off));
532                    s.offer[dst_off..dst_off + n].copy_from_slice(&msg.payload[10..10 + n]);
533                    s.offer_len = (dst_off + n) as u16;
534                } else {
535                    let dst_off = s.answer_len as usize;
536                    let n = core::cmp::min(len, MAX_SDP_BYTES.saturating_sub(dst_off));
537                    s.answer[dst_off..dst_off + n].copy_from_slice(&msg.payload[10..10 + n]);
538                    s.answer_len = (dst_off + n) as u16;
539                }
540                send_response(msg.sender, RESP_OK, |_| {});
541            }
542            OP_SESSION_GET_OFFER | OP_SESSION_GET_ANSWER => {
543                let Some(id) = parse_u64(&msg.payload, 0) else {
544                    send_response(msg.sender, RESP_BAD_REQ, |_| {});
545                    continue;
546                };
547                let Some(offset) = parse_u16(&msg.payload, 8) else {
548                    send_response(msg.sender, RESP_BAD_REQ, |_| {});
549                    continue;
550                };
551                let Some(s) = rt.sessions.get(&id) else {
552                    send_response(msg.sender, RESP_NOT_FOUND, |_| {});
553                    continue;
554                };
555                if s.is_expired(now_ns()) {
556                    send_response(msg.sender, RESP_EXPIRED, |_| {});
557                    continue;
558                }
559                let (buf, total_len) = if msg.msg_type == OP_SESSION_GET_OFFER {
560                    (&s.offer[..], s.offer_len as usize)
561                } else {
562                    (&s.answer[..], s.answer_len as usize)
563                };
564                let off = core::cmp::min(offset as usize, total_len);
565                let n = core::cmp::min(38, total_len.saturating_sub(off));
566                send_response(msg.sender, RESP_OK, |out| {
567                    out.payload[4..6].copy_from_slice(&(total_len as u16).to_le_bytes());
568                    out.payload[6..8].copy_from_slice(&(off as u16).to_le_bytes());
569                    out.payload[8..10].copy_from_slice(&(n as u16).to_le_bytes());
570                    out.payload[10..10 + n].copy_from_slice(&buf[off..off + n]);
571                });
572            }
573            OP_SESSION_ADD_CANDIDATE => {
574                let Some(id) = parse_u64(&msg.payload, 0) else {
575                    send_response(msg.sender, RESP_BAD_REQ, |_| {});
576                    continue;
577                };
578                let dir = msg.payload[8];
579                let len = core::cmp::min(msg.payload[9] as usize, 38);
580                let Some(s) = rt.sessions.get_mut(&id) else {
581                    send_response(msg.sender, RESP_NOT_FOUND, |_| {});
582                    continue;
583                };
584                if s.candidates.len() >= MAX_CANDIDATES {
585                    let _ = s.candidates.pop_front();
586                }
587                let mut c = Candidate {
588                    direction: dir,
589                    data: [0u8; 38],
590                    len: len as u8,
591                };
592                c.data[..len].copy_from_slice(&msg.payload[10..10 + len]);
593                s.candidates.push_back(c);
594                if let Some((ip, port)) = parse_endpoint_candidate(&msg.payload[10..10 + len]) {
595                    if let Some(old) = s.udp_tx_fd.take() {
596                        let _ = call::close(old);
597                    }
598                    s.udp_tx_fd = open_udp_connect(ip, port);
599                }
600                send_response(msg.sender, RESP_OK, |_| {});
601            }
602            OP_SESSION_POP_CANDIDATE => {
603                let Some(id) = parse_u64(&msg.payload, 0) else {
604                    send_response(msg.sender, RESP_BAD_REQ, |_| {});
605                    continue;
606                };
607                let Some(s) = rt.sessions.get_mut(&id) else {
608                    send_response(msg.sender, RESP_NOT_FOUND, |_| {});
609                    continue;
610                };
611                if let Some(c) = s.candidates.pop_front() {
612                    send_response(msg.sender, RESP_OK, |out| {
613                        out.payload[4] = c.direction;
614                        out.payload[5] = c.len;
615                        let n = c.len as usize;
616                        out.payload[6..6 + n].copy_from_slice(&c.data[..n]);
617                    });
618                } else {
619                    send_response(msg.sender, RESP_NOT_FOUND, |_| {});
620                }
621            }
622            OP_INPUT_EVENT => {
623                let Some(id) = parse_u64(&msg.payload, 0) else {
624                    send_response(msg.sender, RESP_BAD_REQ, |_| {});
625                    continue;
626                };
627                let Some(s) = rt.sessions.get_mut(&id) else {
628                    send_response(msg.sender, RESP_NOT_FOUND, |_| {});
629                    continue;
630                };
631                let n = core::cmp::min(msg.payload[8] as usize, 24);
632                s.last_input_len = n as u8;
633                s.last_input[..n].copy_from_slice(&msg.payload[9..9 + n]);
634                send_response(msg.sender, RESP_OK, |_| {});
635            }
636            OP_FRAME_PUSH => {
637                let Some(id) = parse_u64(&msg.payload, 0) else {
638                    send_response(msg.sender, RESP_BAD_REQ, |_| {});
639                    continue;
640                };
641                let Some(s) = rt.sessions.get_mut(&id) else {
642                    send_response(msg.sender, RESP_NOT_FOUND, |_| {});
643                    continue;
644                };
645                s.last_frame_seq = s.last_frame_seq.wrapping_add(1);
646                let n = core::cmp::min(msg.payload[8] as usize, 38);
647                if let Some(fd) = s.udp_tx_fd {
648                    let _ = call::write(fd, &msg.payload[9..9 + n]);
649                }
650                send_response(msg.sender, RESP_OK, |out| {
651                    out.payload[4..12].copy_from_slice(&s.last_frame_seq.to_le_bytes());
652                });
653            }
654            OP_SESSION_INFO => {
655                let Some(id) = parse_u64(&msg.payload, 0) else {
656                    send_response(msg.sender, RESP_BAD_REQ, |_| {});
657                    continue;
658                };
659                let Some(s) = rt.sessions.get(&id) else {
660                    send_response(msg.sender, RESP_NOT_FOUND, |_| {});
661                    continue;
662                };
663                send_response(msg.sender, RESP_OK, |out| {
664                    out.payload[4..12].copy_from_slice(&s.id.to_le_bytes());
665                    out.payload[12..16].copy_from_slice(&s.silo_id.to_le_bytes());
666                    out.payload[16..24].copy_from_slice(&s.token.to_le_bytes());
667                    out.payload[24..32].copy_from_slice(&s.flags.to_le_bytes());
668                    out.payload[32..40].copy_from_slice(&s.expires_at_ns.to_le_bytes());
669                    out.payload[40..42].copy_from_slice(&s.udp_local_port.to_le_bytes());
670                    out.payload[42] = if s.udp_rx_fd.is_some() { 1 } else { 0 };
671                    out.payload[43] = if s.udp_tx_fd.is_some() { 1 } else { 0 };
672                });
673            }
674            _ => send_response(msg.sender, RESP_BAD_REQ, |_| {}),
675        }
676    }
677}