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