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}