1#![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_abi::ip::parse_ipv4_literal;
17use strat9_syscall::{call, data::TimeSpec, number};
18
19alloc_freelist::define_freelist_allocator!(pub struct BumpAllocator; heap_size = 96 * 1024;);
20
21#[global_allocator]
22static GLOBAL_ALLOCATOR: BumpAllocator = BumpAllocator;
23
24#[alloc_error_handler]
25fn alloc_error(_layout: Layout) -> ! {
26 log("[udp-tool] OOM\n");
27 call::exit(12)
28}
29
30#[panic_handler]
31fn panic(info: &PanicInfo) -> ! {
32 call::handle_panic("udp-tool", info)
33}
34
35struct BufWriter<'a> {
36 buf: &'a mut [u8],
37 pos: usize,
38}
39
40impl core::fmt::Write for BufWriter<'_> {
41 fn write_str(&mut self, s: &str) -> core::fmt::Result {
42 let bytes = s.as_bytes();
43 let avail = self.buf.len().saturating_sub(self.pos);
44 let n = bytes.len().min(avail);
45 self.buf[self.pos..self.pos + n].copy_from_slice(&bytes[..n]);
46 self.pos += n;
47 Ok(())
48 }
49}
50
51fn log(msg: &str) {
52 let _ = call::write(1, msg.as_bytes());
53}
54
55fn debug(msg: &str) {
56 let _ = call::debug_log(msg.as_bytes());
57}
58
59fn sleep_ms(ms: u64) {
60 let req = TimeSpec {
61 tv_sec: (ms / 1000) as i64,
62 tv_nsec: ((ms % 1000) * 1_000_000) as i64,
63 };
64 let _ = unsafe {
65 strat9_syscall::syscall2(number::SYS_NANOSLEEP, &req as *const TimeSpec as usize, 0)
66 };
67}
68
69fn clock_ns() -> u64 {
70 unsafe { strat9_syscall::syscall0(number::SYS_CLOCK_GETTIME) }
71 .map(|v| v as u64)
72 .unwrap_or(0)
73}
74
75fn open_rw(path: &str) -> Result<usize, i32> {
76 call::openat(0, path, 0x2, 0)
77 .map(|fd| fd)
78 .map_err(|e| e.to_errno() as i32)
79}
80
81fn read_text(path: &str, out: &mut [u8]) -> usize {
82 let Ok(fd) = call::openat(0, path, 0x0, 0) else {
83 return 0;
84 };
85 let n = call::read(fd, out).unwrap_or(0);
86 let _ = call::close(fd);
87 n
88}
89
90fn parse_first_ipv4_line(path: &str, buf: &mut [u8; 128]) -> Option<[u8; 4]> {
91 let n = read_text(path, buf);
92 if n == 0 {
93 return None;
94 }
95 let line_end = buf[..n].iter().position(|&b| b == b'\n').unwrap_or(n);
96 let mut s = core::str::from_utf8(&buf[..line_end]).ok()?.trim();
97 if let Some((head, _tail)) = s.split_once('/') {
98 s = head;
99 }
100 parse_ipv4_literal(s)
101}
102
103fn ip_to_path<'a>(dst: &[u8; 4], port: u16, out: &'a mut [u8; 96]) -> &'a str {
104 let n = {
105 let mut w = BufWriter { buf: out, pos: 0 };
106 let _ = write!(
107 w,
108 "/net/udp/send/{}.{}.{}.{}/{}",
109 dst[0], dst[1], dst[2], dst[3], port
110 );
111 w.pos
112 };
113 core::str::from_utf8(&out[..n]).unwrap_or("/net/udp/send/0.0.0.0/0")
114}
115
116fn format_src<'a>(src: &[u8; 4], port: u16, out: &'a mut [u8; 64]) -> &'a str {
117 let n = {
118 let mut w = BufWriter { buf: out, pos: 0 };
119 let _ = write!(w, "{}.{}.{}.{}:{}", src[0], src[1], src[2], src[3], port);
120 w.pos
121 };
122 core::str::from_utf8(&out[..n]).unwrap_or("0.0.0.0:0")
123}
124
125fn dump_payload_ascii<'a>(data: &[u8], out: &'a mut [u8; 96]) -> &'a str {
126 let n = data.len().min(out.len());
127 for (i, &b) in data.iter().take(n).enumerate() {
128 out[i] = if (0x20..=0x7e).contains(&b) { b } else { b'.' };
129 }
130 core::str::from_utf8(&out[..n]).unwrap_or("")
131}
132
133#[unsafe(no_mangle)]
134pub extern "C" fn _start() -> ! {
135 const PORT: u16 = 9999;
136 const HEARTBEAT_MS: u64 = 2000;
137
138 log("[udp-tool] starting\n");
139
140 let mut path_buf = [0u8; 96];
141 let bind_path = {
142 let n = {
143 let mut w = BufWriter {
144 buf: &mut path_buf,
145 pos: 0,
146 };
147 let _ = write!(w, "/net/udp/bind/{}", PORT);
148 w.pos
149 };
150 core::str::from_utf8(&path_buf[..n]).unwrap_or("/net/udp/bind/9999")
151 };
152
153 let bind_fd = loop {
154 match open_rw(bind_path) {
155 Ok(fd) => break fd,
156 Err(_) => {
157 debug("[udp-tool] waiting for /net/udp bind\n");
158 sleep_ms(200);
159 }
160 }
161 };
162
163 let mut ip_buf = [0u8; 128];
164 let local_ip = parse_first_ipv4_line("/net/address", &mut ip_buf);
165 let gateway_ip = parse_first_ipv4_line("/net/gateway", &mut ip_buf);
166 let default_target = local_ip.or(gateway_ip).unwrap_or([127, 0, 0, 1]);
167
168 let send_path = ip_to_path(&default_target, PORT, &mut path_buf);
169 let send_fd = open_rw(send_path).ok();
170
171 log("[udp-tool] bound on /net/udp/bind/9999\n");
172 log("[udp-tool] target path: ");
173 log(send_path);
174 log("\n");
175
176 if let Some(fd) = send_fd {
177 let _ = call::write(fd, b"udp-tool: hello\n");
178 }
179
180 let mut last_heartbeat = clock_ns();
181 let mut rx_buf = [0u8; 512];
182 let mut src_txt = [0u8; 64];
183 let mut ascii = [0u8; 96];
184
185 loop {
186 match call::read(bind_fd, &mut rx_buf) {
187 Ok(n) if n >= 6 => {
188 let src = [rx_buf[0], rx_buf[1], rx_buf[2], rx_buf[3]];
189 let src_port = u16::from_be_bytes([rx_buf[4], rx_buf[5]]);
190 let payload = &rx_buf[6..n];
191
192 log("[udp-tool] rx from ");
193 log(format_src(&src, src_port, &mut src_txt));
194 log(" | ");
195 log(dump_payload_ascii(payload, &mut ascii));
196 log("\n");
197
198 let reply_path = ip_to_path(&src, src_port, &mut path_buf);
200 if let Ok(fd) = open_rw(reply_path) {
201 let _ = call::write(fd, payload);
202 let _ = call::close(fd);
203 }
204 }
205 Ok(_) => {
206 }
208 Err(e) => {
209 if e.to_errno() != 11 {
210 log("[udp-tool] read error\n");
211 }
212 }
213 }
214
215 let now = clock_ns();
216 if now.saturating_sub(last_heartbeat) >= HEARTBEAT_MS * 1_000_000 {
217 if let Some(fd) = send_fd {
218 let _ = call::write(fd, b"udp-tool: heartbeat\n");
219 }
220 last_heartbeat = now;
221 }
222
223 sleep_ms(50);
224 }
225}