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}