Skip to main content

udp_tool/
main.rs

1//! udp-tool – UDP scheme probe utility for strat9-os
2//!
3//! This binary exercises the `/net/udp/*` scheme API exposed by `strate-net`.
4//! It binds a local UDP endpoint, sends periodic probes, prints incoming
5//! datagrams, and echoes received payloads back to their source.
6//!
7//! Usage: currently no argv wiring; defaults are used.
8
9#![no_std]
10#![no_main]
11#![feature(alloc_error_handler)]
12
13extern crate alloc;
14
15use core::{alloc::Layout, fmt::Write, panic::PanicInfo};
16use strat9_syscall::{call, data::TimeSpec, number};
17
18alloc_freelist::define_freelist_allocator!(pub struct BumpAllocator; heap_size = 96 * 1024;);
19
20#[global_allocator]
21static GLOBAL_ALLOCATOR: BumpAllocator = BumpAllocator;
22
23#[alloc_error_handler]
24fn alloc_error(_layout: Layout) -> ! {
25    log("[udp-tool] OOM\n");
26    call::exit(12)
27}
28
29#[panic_handler]
30fn panic(info: &PanicInfo) -> ! {
31    log("[udp-tool] PANIC: ");
32    let mut buf = [0u8; 256];
33    let n = {
34        let mut w = BufWriter {
35            buf: &mut buf,
36            pos: 0,
37        };
38        let _ = write!(w, "{}", info.message());
39        w.pos
40    };
41    if let Ok(s) = core::str::from_utf8(&buf[..n]) {
42        log(s);
43    }
44    log("\n");
45    call::exit(255)
46}
47
48struct BufWriter<'a> {
49    buf: &'a mut [u8],
50    pos: usize,
51}
52
53impl core::fmt::Write for BufWriter<'_> {
54    fn write_str(&mut self, s: &str) -> core::fmt::Result {
55        let bytes = s.as_bytes();
56        let avail = self.buf.len().saturating_sub(self.pos);
57        let n = bytes.len().min(avail);
58        self.buf[self.pos..self.pos + n].copy_from_slice(&bytes[..n]);
59        self.pos += n;
60        Ok(())
61    }
62}
63
64fn log(msg: &str) {
65    let _ = call::write(1, msg.as_bytes());
66}
67
68fn debug(msg: &str) {
69    let _ = call::debug_log(msg.as_bytes());
70}
71
72fn sleep_ms(ms: u64) {
73    let req = TimeSpec {
74        tv_sec: (ms / 1000) as i64,
75        tv_nsec: ((ms % 1000) * 1_000_000) as i64,
76    };
77    let _ = unsafe {
78        strat9_syscall::syscall2(number::SYS_NANOSLEEP, &req as *const TimeSpec as usize, 0)
79    };
80}
81
82fn clock_ns() -> u64 {
83    unsafe { strat9_syscall::syscall0(number::SYS_CLOCK_GETTIME) }
84        .map(|v| v as u64)
85        .unwrap_or(0)
86}
87
88fn open_rw(path: &str) -> Result<usize, i32> {
89    call::openat(0, path, 0x2, 0)
90        .map(|fd| fd as usize)
91        .map_err(|e| e.to_errno() as i32)
92}
93
94fn read_text(path: &str, out: &mut [u8]) -> usize {
95    let Ok(fd) = call::openat(0, path, 0x0, 0) else {
96        return 0;
97    };
98    let n = call::read(fd as usize, out).unwrap_or(0);
99    let _ = call::close(fd as usize);
100    n
101}
102
103fn parse_ipv4_literal(s: &str) -> Option<[u8; 4]> {
104    let mut octets = [0u8; 4];
105    let mut idx = 0usize;
106    let mut val: u16 = 0;
107    let mut has_digit = false;
108
109    for &b in s.as_bytes() {
110        if b == b'.' {
111            if !has_digit || idx >= 3 || val > 255 {
112                return None;
113            }
114            octets[idx] = val as u8;
115            idx += 1;
116            val = 0;
117            has_digit = false;
118            continue;
119        }
120        if !b.is_ascii_digit() {
121            return None;
122        }
123        val = val * 10 + (b - b'0') as u16;
124        has_digit = true;
125    }
126
127    if !has_digit || idx != 3 || val > 255 {
128        return None;
129    }
130
131    octets[3] = val as u8;
132    Some(octets)
133}
134
135fn parse_first_ipv4_line(path: &str, buf: &mut [u8; 128]) -> Option<[u8; 4]> {
136    let n = read_text(path, buf);
137    if n == 0 {
138        return None;
139    }
140    let line_end = buf[..n].iter().position(|&b| b == b'\n').unwrap_or(n);
141    let mut s = core::str::from_utf8(&buf[..line_end]).ok()?.trim();
142    if let Some((head, _tail)) = s.split_once('/') {
143        s = head;
144    }
145    parse_ipv4_literal(s)
146}
147
148fn ip_to_path<'a>(dst: &[u8; 4], port: u16, out: &'a mut [u8; 96]) -> &'a str {
149    let n = {
150        let mut w = BufWriter { buf: out, pos: 0 };
151        let _ = write!(
152            w,
153            "/net/udp/send/{}.{}.{}.{}/{}",
154            dst[0], dst[1], dst[2], dst[3], port
155        );
156        w.pos
157    };
158    core::str::from_utf8(&out[..n]).unwrap_or("/net/udp/send/0.0.0.0/0")
159}
160
161fn format_src<'a>(src: &[u8; 4], port: u16, out: &'a mut [u8; 64]) -> &'a str {
162    let n = {
163        let mut w = BufWriter { buf: out, pos: 0 };
164        let _ = write!(w, "{}.{}.{}.{}:{}", src[0], src[1], src[2], src[3], port);
165        w.pos
166    };
167    core::str::from_utf8(&out[..n]).unwrap_or("0.0.0.0:0")
168}
169
170fn dump_payload_ascii<'a>(data: &[u8], out: &'a mut [u8; 96]) -> &'a str {
171    let n = data.len().min(out.len());
172    for (i, &b) in data.iter().take(n).enumerate() {
173        out[i] = if (0x20..=0x7e).contains(&b) { b } else { b'.' };
174    }
175    core::str::from_utf8(&out[..n]).unwrap_or("")
176}
177
178#[unsafe(no_mangle)]
179pub extern "C" fn _start() -> ! {
180    const PORT: u16 = 9999;
181    const HEARTBEAT_MS: u64 = 2000;
182
183    log("[udp-tool] starting\n");
184
185    let mut path_buf = [0u8; 96];
186    let bind_path = {
187        let n = {
188            let mut w = BufWriter {
189                buf: &mut path_buf,
190                pos: 0,
191            };
192            let _ = write!(w, "/net/udp/bind/{}", PORT);
193            w.pos
194        };
195        core::str::from_utf8(&path_buf[..n]).unwrap_or("/net/udp/bind/9999")
196    };
197
198    let bind_fd = loop {
199        match open_rw(bind_path) {
200            Ok(fd) => break fd,
201            Err(_) => {
202                debug("[udp-tool] waiting for /net/udp bind\n");
203                sleep_ms(200);
204            }
205        }
206    };
207
208    let mut ip_buf = [0u8; 128];
209    let local_ip = parse_first_ipv4_line("/net/address", &mut ip_buf);
210    let gateway_ip = parse_first_ipv4_line("/net/gateway", &mut ip_buf);
211    let default_target = local_ip.or(gateway_ip).unwrap_or([127, 0, 0, 1]);
212
213    let send_path = ip_to_path(&default_target, PORT, &mut path_buf);
214    let send_fd = open_rw(send_path).ok();
215
216    log("[udp-tool] bound on /net/udp/bind/9999\n");
217    log("[udp-tool] target path: ");
218    log(send_path);
219    log("\n");
220
221    if let Some(fd) = send_fd {
222        let _ = call::write(fd, b"udp-tool: hello\n");
223    }
224
225    let mut last_heartbeat = clock_ns();
226    let mut rx_buf = [0u8; 512];
227    let mut src_txt = [0u8; 64];
228    let mut ascii = [0u8; 96];
229
230    loop {
231        match call::read(bind_fd, &mut rx_buf) {
232            Ok(n) if n >= 6 => {
233                let src = [rx_buf[0], rx_buf[1], rx_buf[2], rx_buf[3]];
234                let src_port = u16::from_be_bytes([rx_buf[4], rx_buf[5]]);
235                let payload = &rx_buf[6..n];
236
237                log("[udp-tool] rx from ");
238                log(format_src(&src, src_port, &mut src_txt));
239                log(" | ");
240                log(dump_payload_ascii(payload, &mut ascii));
241                log("\n");
242
243                // Echo payload to sender via scheme path.
244                let reply_path = ip_to_path(&src, src_port, &mut path_buf);
245                if let Ok(fd) = open_rw(reply_path) {
246                    let _ = call::write(fd, payload);
247                    let _ = call::close(fd);
248                }
249            }
250            Ok(_) => {
251                // Ignore short frame.
252            }
253            Err(e) => {
254                if e.to_errno() != 11 {
255                    log("[udp-tool] read error\n");
256                }
257            }
258        }
259
260        let now = clock_ns();
261        if now.saturating_sub(last_heartbeat) >= HEARTBEAT_MS * 1_000_000 {
262            if let Some(fd) = send_fd {
263                let _ = call::write(fd, b"udp-tool: heartbeat\n");
264            }
265            last_heartbeat = now;
266        }
267
268        sleep_ms(50);
269    }
270}