Skip to main content

ping/
main.rs

1//! ping – ICMP echo utility for strat9-os
2//!
3//! Sends ICMP echo requests by writing to `/net/ping/<target_ip>` and reads
4//! replies from the same scheme path.  The actual ICMP socket is driven by
5//! the `strate-net` silo via smoltcp; this tool is a thin userspace wrapper.
6//!
7//! Usage:  ping <ipv4-address> [-c count]
8//!
9//! All I/O is done through Plan 9–style schemes – no BSD sockets.
10
11#![no_std]
12#![no_main]
13#![feature(alloc_error_handler)]
14
15extern crate alloc;
16
17use core::{alloc::Layout, fmt::Write, panic::PanicInfo};
18use strat9_syscall::{call, data::TimeSpec, number};
19
20// ---------------------------------------------------------------------------
21// Minimal bump allocator
22// ---------------------------------------------------------------------------
23
24alloc_freelist::define_freelist_allocator!(pub struct BumpAllocator; heap_size = 64 * 1024;);
25
26#[global_allocator]
27static GLOBAL_ALLOCATOR: BumpAllocator = BumpAllocator;
28
29#[alloc_error_handler]
30/// Implements alloc error.
31fn alloc_error(_layout: Layout) -> ! {
32    log("[ping] OOM\n");
33    call::exit(12)
34}
35
36#[panic_handler]
37/// Implements panic.
38fn panic(info: &PanicInfo) -> ! {
39    log("[ping] PANIC: ");
40    let msg = info.message();
41    let mut buf = [0u8; 256];
42    let mut cursor = BufWriter {
43        buf: &mut buf,
44        pos: 0,
45    };
46    let _ = write!(cursor, "{}", msg);
47    let written = cursor.pos;
48    if written > 0 {
49        if let Ok(s) = core::str::from_utf8(&buf[..written]) {
50            log(s);
51        }
52    }
53    log("\n");
54    call::exit(255)
55}
56
57// ---------------------------------------------------------------------------
58// Helpers
59// ---------------------------------------------------------------------------
60
61/// Implements log.
62fn log(msg: &str) {
63    let _ = call::write(1, msg.as_bytes());
64}
65
66/// Implements log u32.
67fn log_u32(val: u32) {
68    let mut buf = [0u8; 12];
69    let s = u32_to_str(val, &mut buf);
70    log(s);
71}
72
73/// Implements u32 to str.
74fn u32_to_str(mut val: u32, buf: &mut [u8; 12]) -> &str {
75    if val == 0 {
76        return "0";
77    }
78    let mut i = buf.len();
79    while val > 0 {
80        i -= 1;
81        buf[i] = b'0' + (val % 10) as u8;
82        val /= 10;
83    }
84    unsafe { core::str::from_utf8_unchecked(&buf[i..]) }
85}
86
87struct BufWriter<'a> {
88    buf: &'a mut [u8],
89    pos: usize,
90}
91impl core::fmt::Write for BufWriter<'_> {
92    /// Writes str.
93    fn write_str(&mut self, s: &str) -> core::fmt::Result {
94        let bytes = s.as_bytes();
95        let avail = self.buf.len().saturating_sub(self.pos);
96        let n = bytes.len().min(avail);
97        self.buf[self.pos..self.pos + n].copy_from_slice(&bytes[..n]);
98        self.pos += n;
99        Ok(())
100    }
101}
102
103/// Implements clock ns.
104fn clock_ns() -> u64 {
105    unsafe { strat9_syscall::syscall0(number::SYS_CLOCK_GETTIME) }
106        .map(|v| v as u64)
107        .unwrap_or(0)
108}
109
110/// Implements sleep ms.
111fn sleep_ms(ms: u64) {
112    let req = TimeSpec {
113        tv_sec: (ms / 1000) as i64,
114        tv_nsec: ((ms % 1000) * 1_000_000) as i64,
115    };
116    let _ = unsafe {
117        strat9_syscall::syscall2(number::SYS_NANOSLEEP, &req as *const TimeSpec as usize, 0)
118    };
119}
120
121/// Read a scheme file, return bytes read.
122fn scheme_read(path: &str, buf: &mut [u8]) -> Result<usize, ()> {
123    let fd = call::openat(0, path, 0x1, 0).map_err(|_| ())?; // O_READ
124    let n = call::read(fd as usize, buf).map_err(|_| {
125        let _ = call::close(fd as usize);
126    })?;
127    let _ = call::close(fd as usize);
128    Ok(n)
129}
130
131/// Write to a scheme file, return bytes written.
132fn scheme_write(path: &str, data: &[u8]) -> Result<usize, ()> {
133    let fd = call::openat(0, path, 0x2, 0).map_err(|_| ())?; // O_WRITE
134    let n = call::write(fd as usize, data).map_err(|_| {
135        let _ = call::close(fd as usize);
136    })?;
137    let _ = call::close(fd as usize);
138    Ok(n)
139}
140
141/// Parses ipv4 literal.
142fn parse_ipv4_literal(s: &str) -> bool {
143    let bytes = s.as_bytes();
144    if bytes.is_empty() {
145        return false;
146    }
147    let mut dots = 0usize;
148    let mut val: u16 = 0;
149    let mut has_digit = false;
150    for &b in bytes {
151        if b == b'.' {
152            if !has_digit || val > 255 || dots >= 3 {
153                return false;
154            }
155            dots += 1;
156            val = 0;
157            has_digit = false;
158            continue;
159        }
160        if !b.is_ascii_digit() {
161            return false;
162        }
163        val = val * 10 + (b - b'0') as u16;
164        has_digit = true;
165    }
166    has_digit && val <= 255 && dots == 3
167}
168
169fn resolve_target<'a>(target: &'a str, resolved_buf: &'a mut [u8; 64]) -> Option<&'a str> {
170    if parse_ipv4_literal(target) {
171        return Some(target);
172    }
173    let mut path_buf = [0u8; 128];
174    let path_len = {
175        let mut pw = BufWriter {
176            buf: &mut path_buf,
177            pos: 0,
178        };
179        let _ = write!(pw, "/net/resolve/{}", target);
180        pw.pos
181    };
182    let path = core::str::from_utf8(&path_buf[..path_len]).ok()?;
183    let n = scheme_read(path, resolved_buf).ok()?;
184    if n == 0 {
185        return None;
186    }
187    let end = resolved_buf[..n]
188        .iter()
189        .position(|&b| b == b'\n')
190        .unwrap_or(n);
191    if end == 0 {
192        return None;
193    }
194    let resolved = core::str::from_utf8(&resolved_buf[..end]).ok()?;
195    if parse_ipv4_literal(resolved) {
196        Some(resolved)
197    } else {
198        None
199    }
200}
201
202// ---------------------------------------------------------------------------
203//  Minimal argument parsing (no clap – we are no_std)
204// ---------------------------------------------------------------------------
205
206struct PingArgs {
207    target: [u8; 64],
208    target_len: usize,
209    count: u32,
210}
211
212/// Parses args.
213fn parse_args() -> PingArgs {
214    // In strat9-os, command-line args are not yet available via /proc or argc/argv.
215    // For now, we hard-code a sensible default.  When the init passes arguments
216    // through a scheme, this can be extended.
217    //
218    // Target: the gateway (read from /net/gateway), or fallback 10.0.2.2 (QEMU default)
219    let mut target = [0u8; 64];
220    let target_len;
221
222    let mut gw_buf = [0u8; 64];
223    if let Ok(n) = scheme_read("/net/gateway", &mut gw_buf) {
224        if n > 0 && !gw_buf.starts_with(b"0.0.0.0") {
225            // Trim trailing newline
226            let end = gw_buf[..n].iter().position(|&b| b == b'\n').unwrap_or(n);
227            target[..end].copy_from_slice(&gw_buf[..end]);
228            target_len = end;
229        } else {
230            let default = b"10.0.2.2";
231            target[..default.len()].copy_from_slice(default);
232            target_len = default.len();
233        }
234    } else {
235        let default = b"10.0.2.2";
236        target[..default.len()].copy_from_slice(default);
237        target_len = default.len();
238    }
239
240    PingArgs {
241        target,
242        target_len,
243        count: 4,
244    }
245}
246
247// ---------------------------------------------------------------------------
248// Ping implementation
249// ---------------------------------------------------------------------------
250
251/// ICMP echo request/reply payload passed through the `/net/ping/<ip>` scheme.
252///
253/// Write: seq(u16 LE) + timestamp_ns(u64 LE) + padding(40 bytes) = 50 bytes
254/// Read:  seq(u16 LE) + rtt_us(u64 LE)                           = 10 bytes
255#[repr(C)]
256struct PingRequest {
257    seq: u16,
258    timestamp_ns: u64,
259    payload: [u8; 40],
260}
261
262#[repr(C)]
263struct PingReply {
264    seq: u16,
265    rtt_us: u64,
266}
267
268#[unsafe(no_mangle)]
269/// Implements start.
270pub extern "C" fn _start() -> ! {
271    let args = parse_args();
272    let raw_target = unsafe { core::str::from_utf8_unchecked(&args.target[..args.target_len]) };
273    let mut resolved_buf = [0u8; 64];
274    let target = match resolve_target(raw_target, &mut resolved_buf) {
275        Some(ip) => ip,
276        None => {
277            log("ping: cannot resolve target ");
278            log(raw_target);
279            log("\n");
280            call::exit(2);
281        }
282    };
283
284    log("PING ");
285    log(target);
286    log(" - ");
287    log_u32(args.count);
288    log(" packets\n");
289
290    // Build the scheme path: /net/ping/<ip>
291    let mut path_buf = [0u8; 128];
292    let mut pw = BufWriter {
293        buf: &mut path_buf,
294        pos: 0,
295    };
296    let _ = write!(pw, "/net/ping/{}", target);
297    let path_len = pw.pos;
298    let path = unsafe { core::str::from_utf8_unchecked(&path_buf[..path_len]) };
299
300    let mut sent: u32 = 0;
301    let mut received: u32 = 0;
302    let mut min_rtt_us: u64 = u64::MAX;
303    let mut max_rtt_us: u64 = 0;
304    let mut total_rtt_us: u64 = 0;
305
306    for seq in 0..args.count {
307        // Build the request payload
308        let ts = clock_ns();
309        let req = PingRequest {
310            seq: seq as u16,
311            timestamp_ns: ts,
312            payload: [0xAA; 40],
313        };
314        let req_bytes = unsafe {
315            core::slice::from_raw_parts(
316                &req as *const PingRequest as *const u8,
317                core::mem::size_of::<PingRequest>(),
318            )
319        };
320
321        // Send
322        if scheme_write(path, req_bytes).is_err() {
323            log("  Request timeout (write failed)\n");
324            sent += 1;
325            sleep_ms(1000);
326            continue;
327        }
328        sent += 1;
329
330        // Wait a bit then read reply
331        sleep_ms(100);
332        let mut reply_buf = [0u8; 64];
333        match scheme_read(path, &mut reply_buf) {
334            Ok(n) if n >= 10 => {
335                let rtt_us = u64::from_le_bytes([
336                    reply_buf[2],
337                    reply_buf[3],
338                    reply_buf[4],
339                    reply_buf[5],
340                    reply_buf[6],
341                    reply_buf[7],
342                    reply_buf[8],
343                    reply_buf[9],
344                ]);
345                let rtt_ms = rtt_us / 1000;
346                let rtt_frac = (rtt_us % 1000) / 100;
347
348                log("  Reply from ");
349                log(target);
350                log(": seq=");
351                log_u32(seq);
352                log(" time=");
353                log_u32(rtt_ms as u32);
354                log(".");
355                log_u32(rtt_frac as u32);
356                log("ms\n");
357
358                received += 1;
359                total_rtt_us += rtt_us;
360                if rtt_us < min_rtt_us {
361                    min_rtt_us = rtt_us;
362                }
363                if rtt_us > max_rtt_us {
364                    max_rtt_us = rtt_us;
365                }
366            }
367            _ => {
368                log("  Request timeout: seq=");
369                log_u32(seq);
370                log("\n");
371            }
372        }
373
374        if seq + 1 < args.count {
375            sleep_ms(900); // ~1s interval
376        }
377    }
378
379    // Statistics
380    log("\n--- ");
381    log(target);
382    log(" ping statistics ---\n");
383    log_u32(sent);
384    log(" packets transmitted, ");
385    log_u32(received);
386    log(" received");
387    if sent > 0 {
388        let loss = ((sent - received) * 100) / sent;
389        log(", ");
390        log_u32(loss);
391        log("% packet loss");
392    }
393    log("\n");
394
395    if received > 0 {
396        let avg = total_rtt_us / received as u64;
397        log("rtt min/avg/max = ");
398        log_u32((min_rtt_us / 1000) as u32);
399        log("/");
400        log_u32((avg / 1000) as u32);
401        log("/");
402        log_u32((max_rtt_us / 1000) as u32);
403        log(" ms\n");
404    }
405
406    call::exit(0)
407}