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