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 [-6] [-c count] [ip|hostname] [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_abi::ip::{
19    is_ipv4_literal_candidate, is_ipv6_literal_candidate, parse_ipv4_literal, parse_ipv6_literal,
20};
21use strat9_syscall::{call, data::TimeSpec, number};
22
23// ===========================================================================
24// Minimal bump allocator
25// ===========================================================================
26
27alloc_freelist::define_freelist_allocator!(pub struct BumpAllocator; heap_size = 64 * 1024;);
28
29#[global_allocator]
30static GLOBAL_ALLOCATOR: BumpAllocator = BumpAllocator;
31
32#[alloc_error_handler]
33/// Implements alloc error.
34fn alloc_error(_layout: Layout) -> ! {
35    log("[ping] OOM\n");
36    call::exit(12)
37}
38
39#[panic_handler]
40fn panic(info: &PanicInfo) -> ! {
41    call::handle_panic("ping", info)
42}
43
44// ===========================================================================
45// Helpers
46// ===========================================================================
47
48/// Implements log.
49fn log(msg: &str) {
50    let _ = call::write(1, msg.as_bytes());
51}
52
53/// Implements log u32.
54fn log_u32(val: u32) {
55    let mut buf = [0u8; 12];
56    let s = u32_to_str(val, &mut buf);
57    log(s);
58}
59
60/// Implements u32 to str.
61fn u32_to_str(mut val: u32, buf: &mut [u8; 12]) -> &str {
62    if val == 0 {
63        return "0";
64    }
65    let mut i = buf.len();
66    while val > 0 {
67        i -= 1;
68        buf[i] = b'0' + (val % 10) as u8;
69        val /= 10;
70    }
71    unsafe { core::str::from_utf8_unchecked(&buf[i..]) }
72}
73
74struct BufWriter<'a> {
75    buf: &'a mut [u8],
76    pos: usize,
77}
78impl core::fmt::Write for BufWriter<'_> {
79    /// Writes str.
80    fn write_str(&mut self, s: &str) -> core::fmt::Result {
81        let bytes = s.as_bytes();
82        let avail = self.buf.len().saturating_sub(self.pos);
83        let n = bytes.len().min(avail);
84        self.buf[self.pos..self.pos + n].copy_from_slice(&bytes[..n]);
85        self.pos += n;
86        Ok(())
87    }
88}
89
90/// Implements clock ns.
91fn clock_ns() -> u64 {
92    unsafe { strat9_syscall::syscall0(number::SYS_CLOCK_GETTIME) }
93        .map(|v| v as u64)
94        .unwrap_or(0)
95}
96
97/// Implements sleep ms.
98fn sleep_ms(ms: u64) {
99    let req = TimeSpec {
100        tv_sec: (ms / 1000) as i64,
101        tv_nsec: ((ms % 1000) * 1_000_000) as i64,
102    };
103    let _ = unsafe {
104        strat9_syscall::syscall2(number::SYS_NANOSLEEP, &req as *const TimeSpec as usize, 0)
105    };
106}
107
108/// Open a scheme file, return fd.
109fn scheme_open(path: &str) -> Result<usize, ()> {
110    call::openat(0, path, 0x3, 0)
111        .map(|fd| fd)
112        .map_err(|_| ())
113}
114
115/// Write to an open scheme fd, return bytes written.
116fn scheme_write_fd(fd: usize, data: &[u8]) -> Result<usize, ()> {
117    call::write(fd, data).map_err(|_| ())
118}
119
120/// Read from an open scheme fd, return bytes read.
121fn scheme_read_fd(fd: usize, buf: &mut [u8]) -> Result<usize, ()> {
122    call::read(fd, buf).map_err(|_| ())
123}
124
125/// Close a scheme fd.
126fn scheme_close(fd: usize) {
127    let _ = call::close(fd);
128}
129
130#[derive(Clone, Copy, PartialEq, Eq)]
131enum AddressFamily {
132    Ipv4,
133    Ipv6,
134}
135
136struct ResolvedTarget<'a> {
137    addr: &'a str,
138    family: AddressFamily,
139}
140
141enum ResolveError {
142    InvalidLiteral(AddressFamily),
143    ResolveFailed,
144    FamilyMismatch {
145        requested: AddressFamily,
146        actual: AddressFamily,
147    },
148}
149
150fn family_flag(family: AddressFamily) -> &'static str {
151    match family {
152        AddressFamily::Ipv4 => "-4",
153        AddressFamily::Ipv6 => "-6",
154    }
155}
156
157fn family_name(family: AddressFamily) -> &'static str {
158    match family {
159        AddressFamily::Ipv4 => "IPv4",
160        AddressFamily::Ipv6 => "IPv6",
161    }
162}
163
164fn apply_family_constraint(
165    requested: Option<AddressFamily>,
166    actual: AddressFamily,
167) -> Result<(), ResolveError> {
168    match requested {
169        Some(expected) if expected != actual => Err(ResolveError::FamilyMismatch {
170            requested: expected,
171            actual,
172        }),
173        _ => Ok(()),
174    }
175}
176
177fn resolve_target<'a>(
178    target: &'a str,
179    requested_family: Option<AddressFamily>,
180    resolved_buf: &'a mut [u8; 64],
181) -> Result<ResolvedTarget<'a>, ResolveError> {
182    if parse_ipv4_literal(target).is_some() {
183        apply_family_constraint(requested_family, AddressFamily::Ipv4)?;
184        return Ok(ResolvedTarget {
185            addr: target,
186            family: AddressFamily::Ipv4,
187        });
188    }
189    if parse_ipv6_literal(target).is_some() {
190        apply_family_constraint(requested_family, AddressFamily::Ipv6)?;
191        return Ok(ResolvedTarget {
192            addr: target,
193            family: AddressFamily::Ipv6,
194        });
195    }
196    if is_ipv4_literal_candidate(target) {
197        return Err(ResolveError::InvalidLiteral(AddressFamily::Ipv4));
198    }
199    if is_ipv6_literal_candidate(target) {
200        return Err(ResolveError::InvalidLiteral(AddressFamily::Ipv6));
201    }
202
203    let mut path_buf = [0u8; 128];
204    let path_len = {
205        let mut pw = BufWriter {
206            buf: &mut path_buf,
207            pos: 0,
208        };
209        let _ = write!(pw, "/net/resolve/{}", target);
210        pw.pos
211    };
212    let path =
213        core::str::from_utf8(&path_buf[..path_len]).map_err(|_| ResolveError::ResolveFailed)?;
214    let n = scheme_open(path)
215        .and_then(|fd| {
216            let r = scheme_read_fd(fd, resolved_buf);
217            scheme_close(fd);
218            r
219        })
220        .map_err(|_| ResolveError::ResolveFailed)?;
221    if n == 0 {
222        return Err(ResolveError::ResolveFailed);
223    }
224    let end = resolved_buf[..n]
225        .iter()
226        .position(|&b| b == b'\n')
227        .unwrap_or(n);
228    if end == 0 {
229        return Err(ResolveError::ResolveFailed);
230    }
231    let resolved =
232        core::str::from_utf8(&resolved_buf[..end]).map_err(|_| ResolveError::ResolveFailed)?;
233    if parse_ipv4_literal(resolved).is_some() {
234        apply_family_constraint(requested_family, AddressFamily::Ipv4)?;
235        Ok(ResolvedTarget {
236            addr: resolved,
237            family: AddressFamily::Ipv4,
238        })
239    } else if parse_ipv6_literal(resolved).is_some() {
240        apply_family_constraint(requested_family, AddressFamily::Ipv6)?;
241        Ok(ResolvedTarget {
242            addr: resolved,
243            family: AddressFamily::Ipv6,
244        })
245    } else {
246        Err(ResolveError::ResolveFailed)
247    }
248}
249
250fn print_usage() {
251    log("Usage: ping [-6] [-c count] [target] [count]\n");
252    log("       ping -h | --help\n");
253    log("Options:\n");
254    log("  -6           Require IPv6\n");
255    log("  -c <count>   Send count requests (default: 4)\n");
256    log("  -h, --help   Show this help\n");
257}
258
259fn copy_target(dst: &mut [u8; 64], value: &str) -> usize {
260    let len = value.len().min(dst.len() - 1);
261    dst[..len].copy_from_slice(&value.as_bytes()[..len]);
262    len
263}
264
265fn read_default_target(family: Option<AddressFamily>, target: &mut [u8; 64]) -> Option<usize> {
266    let mut gateway_buf = [0u8; 64];
267    let path = if family == Some(AddressFamily::Ipv6) {
268        "/net/ip6/gateway"
269    } else {
270        "/net/gateway"
271    };
272    let n = scheme_open(path)
273        .and_then(|fd| {
274            let r = scheme_read_fd(fd, &mut gateway_buf);
275            scheme_close(fd);
276            r
277        })
278        .ok()?;
279    if n == 0 {
280        return None;
281    }
282
283    let end = gateway_buf[..n]
284        .iter()
285        .position(|&b| b == b'\n')
286        .unwrap_or(n);
287    if end == 0 {
288        return None;
289    }
290
291    let gateway = &gateway_buf[..end];
292    if gateway == b"0.0.0.0" || gateway == b"::" {
293        return None;
294    }
295
296    let gateway_str = core::str::from_utf8(gateway).ok()?;
297    Some(copy_target(target, gateway_str))
298}
299
300// ===========================================================================
301//  Minimal argument parsing
302// ===========================================================================
303
304struct PingArgs {
305    target: [u8; 64],
306    target_len: usize,
307    count: u32,
308    family: Option<AddressFamily>,
309}
310
311/// Convert a null-terminated C string pointer to a `&'static str`.
312///
313/// # Safety
314/// `ptr` must point to a valid null-terminated byte sequence that remains
315/// valid for the lifetime of the process (true for argv pointers on the
316/// initial user stack).
317unsafe fn cstr_to_str(ptr: *const u8) -> &'static str {
318    let mut len = 0usize;
319    while *ptr.add(len) != 0 {
320        len += 1;
321    }
322    core::str::from_utf8_unchecked(core::slice::from_raw_parts(ptr, len))
323}
324
325/// Parse an ASCII decimal string into a `u32`.
326fn str_to_u32(s: &str) -> Option<u32> {
327    if s.is_empty() {
328        return None;
329    }
330    let mut val: u32 = 0;
331    for &b in s.as_bytes() {
332        if !b.is_ascii_digit() {
333            return None;
334        }
335        val = val.checked_mul(10)?.checked_add((b - b'0') as u32)?;
336    }
337    Some(val)
338}
339
340/// Build `PingArgs` from the SysV initial user stack.
341///
342/// Supported forms:
343/// - `ping <target> [count]`
344/// - `ping -c <count> [target]`
345/// - `ping -6 ...`
346/// - `ping -h|--help`
347fn parse_args_from_stack(argc: usize, argv: *const *const u8) -> PingArgs {
348    let mut target = [0u8; 64];
349    let mut target_len = 0usize;
350    let mut count = 4u32;
351    let mut family: Option<AddressFamily> = None;
352    let mut positional_count_set = false;
353
354    let mut i = 1usize;
355    while i < argc {
356        let arg = unsafe { cstr_to_str(*argv.add(i)) };
357        match arg {
358            "-h" | "--help" => {
359                print_usage();
360                call::exit(0);
361            }
362            "-6" => {
363                family = Some(AddressFamily::Ipv6);
364            }
365            "-c" => {
366                i += 1;
367                if i >= argc {
368                    log("ping: missing value for -c\n");
369                    print_usage();
370                    call::exit(1);
371                }
372                let count_arg = unsafe { cstr_to_str(*argv.add(i)) };
373                let Some(parsed) = str_to_u32(count_arg) else {
374                    log("ping: invalid count: ");
375                    log(count_arg);
376                    log("\n");
377                    call::exit(1);
378                };
379                count = parsed;
380            }
381            _ if arg.starts_with('-') => {
382                log("ping: unknown option: ");
383                log(arg);
384                log("\n");
385                print_usage();
386                call::exit(1);
387            }
388            _ if target_len == 0 => {
389                target_len = copy_target(&mut target, arg);
390            }
391            _ if !positional_count_set => {
392                let Some(parsed) = str_to_u32(arg) else {
393                    log("ping: unexpected argument: ");
394                    log(arg);
395                    log("\n");
396                    print_usage();
397                    call::exit(1);
398                };
399                count = parsed;
400                positional_count_set = true;
401            }
402            _ => {
403                log("ping: unexpected argument: ");
404                log(arg);
405                log("\n");
406                print_usage();
407                call::exit(1);
408            }
409        }
410
411        i += 1;
412    }
413
414    if target_len == 0 {
415        target_len = match read_default_target(family, &mut target) {
416            Some(len) => len,
417            None => {
418                log("ping: no target specified and no default gateway configured\n");
419                print_usage();
420                call::exit(1);
421            }
422        };
423    }
424
425    PingArgs {
426        target,
427        target_len,
428        count,
429        family,
430    }
431}
432
433// ===========================================================================
434// Ping implementation
435// ===========================================================================
436
437/// ICMP echo request/reply payload passed through the `/net/ping/<ip>` scheme.
438///
439/// Write: seq(u16 LE) + timestamp_ns(u64 LE) + padding(40 bytes) = 50 bytes
440/// Read:  seq(u16 LE) + rtt_us(u64 LE)                           = 10 bytes
441#[repr(C)]
442struct PingRequest {
443    seq: u16,
444    timestamp_ns: u64,
445    payload: [u8; 40],
446}
447
448/// Entry point: captures the initial RSP (= SysV stack pointer) and calls
449/// `start_impl`. Using a naked function guarantees that no prologue has
450/// modified RSP before we pass it as `rdi`.
451#[unsafe(naked)]
452#[unsafe(no_mangle)]
453pub unsafe extern "C" fn _start() -> ! {
454    // At entry via iretq: RSP = boot_sp from the ELF loader.
455    // SysV layout: [RSP] = argc, [RSP+8] = argv[0] ptr, ...
456    core::arch::naked_asm!(
457        "mov rdi, rsp",   // pass initial stack pointer as first arg
458        "call {f}",
459        "ud2",
460        f = sym start_impl,
461    );
462}
463
464/// Main program logic; called from the naked `_start` trampoline.
465///
466/// `initial_sp` is the SysV stack pointer captured before any prologue code
467/// has run, so `*initial_sp` == argc and `*(initial_sp + 1)` == argv[0].
468extern "C" fn start_impl(initial_sp: *const u64) -> ! {
469    let argc = unsafe { *initial_sp } as usize;
470    let argv = unsafe { initial_sp.add(1) as *const *const u8 };
471    let args = parse_args_from_stack(argc, argv);
472    let raw_target = unsafe { core::str::from_utf8_unchecked(&args.target[..args.target_len]) };
473    let mut resolved_buf = [0u8; 64];
474    let target = match resolve_target(raw_target, args.family, &mut resolved_buf) {
475        Ok(target) => target,
476        Err(ResolveError::InvalidLiteral(family)) => {
477            log("ping: invalid ");
478            log(family_name(family));
479            log(" address: ");
480            log(raw_target);
481            log("\n");
482            call::exit(1);
483        }
484        Err(ResolveError::ResolveFailed) => {
485            log("ping: cannot resolve host: ");
486            log(raw_target);
487            log("\n");
488            call::exit(1);
489        }
490        Err(ResolveError::FamilyMismatch { requested, actual }) => {
491            log("ping: ");
492            log(family_flag(requested));
493            log(" requires an ");
494            log(family_name(requested));
495            log(" target, but got ");
496            log(family_name(actual));
497            log("\n");
498            call::exit(1);
499        }
500    };
501
502    log("PING ");
503    log(target.addr);
504    log(", sending ");
505    log_u32(args.count);
506    log(" ICMP now\n");
507
508    // Build the scheme path: /net/ping/<ip> or /net/ping6/<ip>
509    let mut path_buf = [0u8; 128];
510    let mut pw = BufWriter {
511        buf: &mut path_buf,
512        pos: 0,
513    };
514    let scheme_name = match target.family {
515        AddressFamily::Ipv4 => "ping",
516        AddressFamily::Ipv6 => "ping6",
517    };
518    let _ = write!(pw, "/net/{}/{}", scheme_name, target.addr);
519    let path_len = pw.pos;
520    let path = unsafe { core::str::from_utf8_unchecked(&path_buf[..path_len]) };
521
522    // Open the scheme fd once and reuse for all pings (avoids open/close per packet).
523    let fd = match scheme_open(path) {
524        Ok(fd) => fd,
525        Err(()) => {
526            log("ping: cannot open ");
527            log(path);
528            log("\n");
529            call::exit(1);
530        }
531    };
532
533    const PING_TIMEOUT_MS: u64 = 5_000;
534    const POLL_INTERVAL_MS: u64 = 100;
535
536    let mut sent: u32 = 0;
537    let mut received: u32 = 0;
538    let mut min_rtt_us: u64 = u64::MAX;
539    let mut max_rtt_us: u64 = 0;
540    let mut total_rtt_us: u64 = 0;
541
542    for seq in 0..args.count {
543        // Build the request payload
544        let ts = clock_ns();
545        let req = PingRequest {
546            seq: seq as u16,
547            timestamp_ns: ts,
548            payload: [0xAA; 40],
549        };
550        let req_bytes = unsafe {
551            core::slice::from_raw_parts(
552                &req as *const PingRequest as *const u8,
553                core::mem::size_of::<PingRequest>(),
554            )
555        };
556
557        let mut wrote = false;
558        let write_deadline_ns = clock_ns().saturating_add(PING_TIMEOUT_MS * 1_000_000);
559        while clock_ns() < write_deadline_ns {
560            log("[pt] wr ");
561            if scheme_write_fd(fd, req_bytes).is_ok() {
562                wrote = true;
563                log("ok\n");
564                break;
565            }
566            log(".");
567            // NOTE: do NOT read-drain here : with a persistent FD, a drain
568            // would consume replies from previously sent pings, losing them.
569            sleep_ms(POLL_INTERVAL_MS);
570        }
571
572        if !wrote {
573            log("  Request timeout (write failed): seq=");
574            log_u32(seq);
575            log("\n");
576            sent += 1;
577            continue;
578        }
579        sent += 1;
580
581        log("[pt] rd ");
582        let mut got_reply = false;
583        let read_deadline_ns = clock_ns().saturating_add(PING_TIMEOUT_MS * 1_000_000);
584        while clock_ns() < read_deadline_ns {
585            let mut reply_buf = [0u8; 64];
586            match scheme_read_fd(fd, &mut reply_buf) {
587                Ok(n) if n >= 10 => {
588                    log("rep\n");
589                    // Use the RTT computed by strate-net (accurate ICMP-level timing),
590                    // not a local timestamp which would include IPC overhead.
591                    let reply_seq = u16::from_le_bytes([reply_buf[0], reply_buf[1]]);
592                    let rtt_us =
593                        u64::from_le_bytes(reply_buf[2..10].try_into().unwrap_or([0u8; 8]));
594                    let rtt_ms = rtt_us / 1000;
595                    let rtt_frac = (rtt_us % 1000) / 100;
596
597                    log("  Reply from ");
598                    log(target.addr);
599                    log(": seq=");
600                    log_u32(reply_seq as u32);
601                    log(" time=");
602                    log_u32(rtt_ms as u32);
603                    log(".");
604                    log_u32(rtt_frac as u32);
605                    log("ms\n");
606
607                    received += 1;
608                    total_rtt_us += rtt_us;
609                    if rtt_us < min_rtt_us {
610                        min_rtt_us = rtt_us;
611                    }
612                    if rtt_us > max_rtt_us {
613                        max_rtt_us = rtt_us;
614                    }
615                    got_reply = true;
616                    break;
617                }
618                Ok(0) => {
619                    log(".");
620                    sleep_ms(POLL_INTERVAL_MS);
621                }
622                _ => {
623                    log("x");
624                    sleep_ms(POLL_INTERVAL_MS);
625                }
626            }
627        }
628
629        if !got_reply {
630            log("[pt] rto\n");
631            log("  Request timeout: seq=");
632            log_u32(seq);
633            log("\n");
634        }
635
636        if seq + 1 < args.count {
637            sleep_ms(900); // ~1s interval
638        }
639    }
640
641    // Statistics
642    log("\n--- ");
643    log(target.addr);
644    log(" ping statistics ---\n");
645    log_u32(sent);
646    log(" packets transmitted, ");
647    log_u32(received);
648    log(" received");
649    if sent > 0 {
650        let loss = ((sent - received) * 100) / sent;
651        log(", ");
652        log_u32(loss);
653        log("% packet loss");
654    }
655    log("\n");
656
657    if received > 0 {
658        let avg = total_rtt_us / received as u64;
659        log("rtt min/avg/max = ");
660        log_u32((min_rtt_us / 1000) as u32);
661        log("/");
662        log_u32((avg / 1000) as u32);
663        log("/");
664        log_u32((max_rtt_us / 1000) as u32);
665        log(" ms\n");
666    }
667
668    scheme_close(fd);
669    call::exit(0)
670}