1#![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
23alloc_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]
33fn 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
44fn log(msg: &str) {
50 let _ = call::write(1, msg.as_bytes());
51}
52
53fn log_u32(val: u32) {
55 let mut buf = [0u8; 12];
56 let s = u32_to_str(val, &mut buf);
57 log(s);
58}
59
60fn 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 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
90fn clock_ns() -> u64 {
92 unsafe { strat9_syscall::syscall0(number::SYS_CLOCK_GETTIME) }
93 .map(|v| v as u64)
94 .unwrap_or(0)
95}
96
97fn 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
108fn scheme_open(path: &str) -> Result<usize, ()> {
110 call::openat(0, path, 0x3, 0).map(|fd| fd).map_err(|_| ())
111}
112
113fn scheme_write_fd(fd: usize, data: &[u8]) -> Result<usize, ()> {
115 call::write(fd, data).map_err(|_| ())
116}
117
118fn scheme_read_fd(fd: usize, buf: &mut [u8]) -> Result<usize, ()> {
120 call::read(fd, buf).map_err(|_| ())
121}
122
123fn 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
298struct PingArgs {
303 target: [u8; 64],
304 target_len: usize,
305 count: u32,
306 family: Option<AddressFamily>,
307}
308
309unsafe 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
323fn 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
338fn 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#[repr(C)]
440struct PingRequest {
441 seq: u16,
442 timestamp_ns: u64,
443 payload: [u8; 40],
444}
445
446#[unsafe(naked)]
450#[unsafe(no_mangle)]
451pub unsafe extern "C" fn _start() -> ! {
452 core::arch::naked_asm!(
455 "mov rdi, rsp", "call {f}",
457 "ud2",
458 f = sym start_impl,
459 );
460}
461
462extern "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 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 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 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 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 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); }
637 }
638
639 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}