Skip to main content

ice_candidate/
main.rs

1#![no_std]
2#![no_main]
3#![feature(alloc_error_handler)]
4
5extern crate alloc;
6
7use alloc::format;
8use core::{alloc::Layout, fmt::Write, panic::PanicInfo};
9use strat9_syscall::{call, data::TimeSpec, number};
10
11/// Default STUN host used when /net/stun-config is absent or unreadable.
12const DEFAULT_STUN_HOST: &str = "stun.l.google.com";
13/// Default STUN port used when /net/stun-config does not specify one.
14const DEFAULT_STUN_PORT: u16 = 19302;
15
16// RFC 8445 §5.1.2 priority formula: (type_preference << 24) | (local_preference << 8) | (256 - component_id)
17// host type_pref=126, srflx type_pref=100; local_pref=65535 (single interface); comp_id=1.
18const ICE_HOST_PRIORITY: u32 = (126u32 << 24) | (65535u32 << 8) | (256 - 1);
19const ICE_SRFLX_PRIORITY: u32 = (100u32 << 24) | (65535u32 << 8) | (256 - 1);
20
21/// File open flags used with openat.
22const O_RDONLY: usize = 0x1;
23const O_RDWR: usize = 0x3;
24
25alloc_freelist::define_freelist_allocator!(pub struct BumpAllocator; heap_size = 64 * 1024;);
26
27#[global_allocator]
28static GLOBAL_ALLOCATOR: BumpAllocator = BumpAllocator;
29
30#[alloc_error_handler]
31fn alloc_error(_layout: Layout) -> ! {
32    let _ = call::write(1, b"[ice-candidate] OOM\n");
33    call::exit(12)
34}
35
36#[panic_handler]
37fn panic(info: &PanicInfo) -> ! {
38    let _ = call::write(1, b"[ice-candidate] PANIC: ");
39    let mut buf = [0u8; 192];
40    let mut w = BufWriter {
41        buf: &mut buf,
42        pos: 0,
43    };
44    let _ = write!(w, "{}", info.message());
45    let len = w.pos;
46    drop(w); // release mutable borrow before reusing `buf`
47    if len > 0 {
48        let _ = call::write(1, &buf[..len]);
49    }
50    let _ = call::write(1, b"\n");
51    call::exit(255)
52}
53
54struct BufWriter<'a> {
55    buf: &'a mut [u8],
56    pos: usize,
57}
58
59impl core::fmt::Write for BufWriter<'_> {
60    fn write_str(&mut self, s: &str) -> core::fmt::Result {
61        let bytes = s.as_bytes();
62        let avail = self.buf.len().saturating_sub(self.pos);
63        let n = bytes.len().min(avail);
64        self.buf[self.pos..self.pos + n].copy_from_slice(&bytes[..n]);
65        self.pos += n;
66        Ok(())
67    }
68}
69
70fn log(msg: &str) {
71    let _ = call::write(1, msg.as_bytes());
72}
73
74fn sleep_ms(ms: u64) {
75    let req = TimeSpec {
76        tv_sec: (ms / 1000) as i64,
77        tv_nsec: ((ms % 1000) * 1_000_000) as i64,
78    };
79    let _ = unsafe {
80        strat9_syscall::syscall2(number::SYS_NANOSLEEP, &req as *const TimeSpec as usize, 0)
81    };
82}
83
84fn scheme_read(path: &str, buf: &mut [u8]) -> Result<usize, ()> {
85    let fd = call::openat(0, path, O_RDONLY, 0).map_err(|_| ())?;
86    let n = call::read(fd as usize, buf).map_err(|_| {
87        let _ = call::close(fd as usize);
88    })?;
89    let _ = call::close(fd as usize);
90    Ok(n)
91}
92
93fn scheme_open(path: &str, flags: usize) -> Result<usize, ()> {
94    call::openat(0, path, flags, 0).map_err(|_| ())
95}
96
97fn parse_ipv4_literal(s: &str) -> bool {
98    let bytes = s.as_bytes();
99    if bytes.is_empty() {
100        return false;
101    }
102    let mut dots = 0usize;
103    let mut val: u16 = 0;
104    let mut has_digit = false;
105    for &b in bytes {
106        if b == b'.' {
107            if !has_digit || val > 255 || dots >= 3 {
108                return false;
109            }
110            dots += 1;
111            val = 0;
112            has_digit = false;
113            continue;
114        }
115        if !b.is_ascii_digit() {
116            return false;
117        }
118        val = val * 10 + (b - b'0') as u16;
119        has_digit = true;
120    }
121    has_digit && val <= 255 && dots == 3
122}
123
124fn resolve_target<'a>(target: &'a str, resolved_buf: &'a mut [u8; 64]) -> Option<&'a str> {
125    if parse_ipv4_literal(target) {
126        return Some(target);
127    }
128    let path = format!("/net/resolve/{}", target);
129    let n = scheme_read(&path, resolved_buf).ok()?;
130    if n == 0 {
131        return None;
132    }
133    let end = resolved_buf[..n]
134        .iter()
135        .position(|&b| b == b'\n')
136        .unwrap_or(n);
137    if end == 0 {
138        return None;
139    }
140    let resolved = core::str::from_utf8(&resolved_buf[..end]).ok()?;
141    if parse_ipv4_literal(resolved) {
142        Some(resolved)
143    } else {
144        None
145    }
146}
147
148fn parse_u16_decimal(s: &[u8]) -> Option<u16> {
149    if s.is_empty() {
150        return None;
151    }
152    let mut v: u32 = 0;
153    for &b in s {
154        if !b.is_ascii_digit() {
155            return None;
156        }
157        v = v * 10 + (b - b'0') as u32;
158        if v > 65535 {
159            return None;
160        }
161    }
162    Some(v as u16)
163}
164
165/// Read STUN configuration from /net/stun-config.
166///
167/// Accepted formats (newline-terminated):
168///   host          — uses DEFAULT_STUN_PORT
169///   host:port     — overrides both host and port
170///
171/// On any parse or I/O error the defaults are returned unchanged.
172fn read_stun_config<'a>(host_buf: &'a mut [u8; 253], port_out: &mut u16) -> &'a str {
173    let mut raw = [0u8; 260];
174    let n = match scheme_read("/net/stun-config", &mut raw) {
175        Ok(n) if n > 0 => n,
176        _ => return DEFAULT_STUN_HOST,
177    };
178    // Trim trailing whitespace / newlines.
179    let mut end = n;
180    while end > 0 && (raw[end - 1] == b'\n' || raw[end - 1] == b'\r' || raw[end - 1] == b' ') {
181        end -= 1;
182    }
183    let line = &raw[..end];
184    // Find the last ':' to split host from optional port.
185    let colon = line.iter().rposition(|&b| b == b':');
186    let (host_bytes, port_bytes) = if let Some(pos) = colon {
187        (&line[..pos], Some(&line[pos + 1..]))
188    } else {
189        (line, None)
190    };
191    if host_bytes.is_empty() || host_bytes.len() > 253 {
192        return DEFAULT_STUN_HOST;
193    }
194    if let Some(pb) = port_bytes {
195        if let Some(p) = parse_u16_decimal(pb) {
196            *port_out = p;
197        } else {
198            return DEFAULT_STUN_HOST;
199        }
200    }
201    host_buf[..host_bytes.len()].copy_from_slice(host_bytes);
202    match core::str::from_utf8(&host_buf[..host_bytes.len()]) {
203        Ok(s) => s,
204        Err(_) => DEFAULT_STUN_HOST,
205    }
206}
207
208fn read_local_ip<'a>(out: &'a mut [u8; 64]) -> Option<&'a str> {
209    let n = scheme_read("/net/address", out).ok()?;
210    if n == 0 {
211        return None;
212    }
213    let mut end = out[..n].iter().position(|&b| b == b'\n').unwrap_or(n);
214    if let Some(slash) = out[..end].iter().position(|&b| b == b'/') {
215        end = slash;
216    }
217    if end == 0 {
218        return None;
219    }
220    let ip = core::str::from_utf8(&out[..end]).ok()?;
221    if parse_ipv4_literal(ip) {
222        Some(ip)
223    } else {
224        None
225    }
226}
227
228fn parse_stun_binding(resp: &[u8], txid: &[u8; 12]) -> Option<([u8; 4], u16)> {
229    if resp.len() < 20 {
230        return None;
231    }
232    let msg_type = u16::from_be_bytes([resp[0], resp[1]]);
233    if msg_type != 0x0101 {
234        return None;
235    }
236    let msg_len = u16::from_be_bytes([resp[2], resp[3]]) as usize;
237    if msg_len + 20 > resp.len() {
238        return None;
239    }
240    if resp[4..8] != [0x21, 0x12, 0xA4, 0x42] {
241        return None;
242    }
243    if resp[8..20] != txid[..] {
244        return None;
245    }
246    let mut off = 20usize;
247    let end = 20 + msg_len;
248    while off + 4 <= end && off + 4 <= resp.len() {
249        let attr_ty = u16::from_be_bytes([resp[off], resp[off + 1]]);
250        let attr_len = u16::from_be_bytes([resp[off + 2], resp[off + 3]]) as usize;
251        let val_off = off + 4;
252        let val_end = val_off + attr_len;
253        if val_end > end || val_end > resp.len() {
254            return None;
255        }
256        if attr_ty == 0x0020 && attr_len >= 8 {
257            if resp[val_off + 1] != 0x01 {
258                return None;
259            }
260            let xport = u16::from_be_bytes([resp[val_off + 2], resp[val_off + 3]]);
261            let port = xport ^ 0x2112;
262            let ip = [
263                resp[val_off + 4] ^ 0x21,
264                resp[val_off + 5] ^ 0x12,
265                resp[val_off + 6] ^ 0xA4,
266                resp[val_off + 7] ^ 0x42,
267            ];
268            return Some((ip, port));
269        }
270        if attr_ty == 0x0001 && attr_len >= 8 {
271            if resp[val_off + 1] != 0x01 {
272                return None;
273            }
274            let port = u16::from_be_bytes([resp[val_off + 2], resp[val_off + 3]]);
275            let ip = [
276                resp[val_off + 4],
277                resp[val_off + 5],
278                resp[val_off + 6],
279                resp[val_off + 7],
280            ];
281            return Some((ip, port));
282        }
283        let pad = (4 - (attr_len % 4)) % 4;
284        off = val_end + pad;
285    }
286    None
287}
288
289#[unsafe(no_mangle)]
290pub extern "C" fn _start() -> ! {
291    let mut stun_port: u16 = DEFAULT_STUN_PORT;
292    let mut stun_host_buf = [0u8; 253];
293    let stun_host = read_stun_config(&mut stun_host_buf, &mut stun_port);
294    let mut resolved = [0u8; 64];
295    let Some(stun_ip) = resolve_target(stun_host, &mut resolved) else {
296        log("[ice-candidate] resolve failed\n");
297        call::exit(1);
298    };
299
300    let path = format!("/net/udp/connect/{}/{}", stun_ip, stun_port);
301    let mut req = [0u8; 20];
302    req[0..2].copy_from_slice(&0x0001u16.to_be_bytes());
303    req[2..4].copy_from_slice(&0u16.to_be_bytes());
304    req[4..8].copy_from_slice(&0x2112A442u32.to_be_bytes());
305    let now = unsafe { strat9_syscall::syscall0(number::SYS_CLOCK_GETTIME) }.unwrap_or(0) as u64;
306    let tid = now.to_be_bytes();
307    req[8..16].copy_from_slice(&tid);
308    req[16..20].copy_from_slice(&[0x53, 0x49, 0x4C, 0x4F]);
309    let mut txid = [0u8; 12];
310    txid.copy_from_slice(&req[8..20]);
311
312    let fd = match scheme_open(&path, O_RDWR) {
313        Ok(fd) => fd,
314        Err(_) => {
315            log("[ice-candidate] stun open failed\n");
316            call::exit(2);
317        }
318    };
319
320    if call::write(fd as usize, &req).is_err() {
321        log("[ice-candidate] stun send failed\n");
322        let _ = call::close(fd as usize);
323        call::exit(2);
324    }
325
326    let mut resp = [0u8; 128];
327    let mut mapped: Option<([u8; 4], u16)> = None;
328    let mut tries = 0usize;
329    while tries < 50 {
330        tries += 1;
331        if let Ok(n) = call::read(fd as usize, &mut resp) {
332            if n > 0 {
333                mapped = parse_stun_binding(&resp[..n], &txid);
334                if mapped.is_some() {
335                    break;
336                }
337            }
338        }
339        sleep_ms(20);
340    }
341
342    let mut local_ip_buf = [0u8; 64];
343    if let Some(local_ip) = read_local_ip(&mut local_ip_buf) {
344        // RFC 8445 §5.1.1: use port 0 when the actual bound port is not known.
345        let host = format!(
346            "candidate:1 1 UDP {} {} 0 typ host\r\n",
347            ICE_HOST_PRIORITY, local_ip
348        );
349        log(&host);
350    }
351
352    if let Some((ip, port)) = mapped {
353        let srflx = format!(
354            // RFC 8445 §5.1.1: rport 0 when the reflexive base port is not tracked.
355            "candidate:2 1 UDP {} {}.{}.{}.{} {} typ srflx raddr 0.0.0.0 rport 0\r\n",
356            ICE_SRFLX_PRIORITY, ip[0], ip[1], ip[2], ip[3], port
357        );
358        log(&srflx);
359        let _ = call::close(fd as usize);
360        call::exit(0);
361    }
362
363    log("[ice-candidate] no srflx candidate\n");
364    let _ = call::close(fd as usize);
365    call::exit(3)
366}