net-mumu 0.2.0-rc.3

Network tools plugin for the Lava language
Documentation
// src/sys_ping.rs
//
// OS ping helpers for the net-mumu plugin.
// - `real_system_ping_loop` spawns the platform `ping` binary and streams lines.
// - `real_raw_icmp_loop` (Unix only) sends ICMP echo requests with a raw socket.
// - `icmp_checksum` implements the standard 16‑bit one's‑complement checksum.
//
// None of these functions are directly referenced by the public bridge right now,
// but they are useful for future optimization (builtin/raw path when running as
// root). We mark them with `#[allow(dead_code)]` to keep local builds warning‑free.

use std::io::{BufRead, BufReader};
use std::net::IpAddr;
use std::process::{Command, Stdio};
use std::sync::mpsc::Sender;
use std::sync::{
    atomic::{AtomicBool, Ordering},
    Arc,
};
use std::time::{Duration, Instant};

use crate::bridge::NetMessage;
use crate::util::make_display_host_str;

#[allow(dead_code)]
pub fn real_system_ping_loop(
    token_id: usize,
    ip: IpAddr,
    maybe_host: Option<String>,
    count: Option<usize>,
    interval_ms: Option<u64>,
    tx: Sender<NetMessage>,
    stop_flag: Arc<AtomicBool>,
    verbose: bool,
) {
    let dest_display = make_display_host_str(&maybe_host, ip);
    let dest_for_cli = maybe_host.clone().unwrap_or_else(|| ip.to_string());

    #[cfg(unix)]
    let args: Vec<String> = {
        let mut v = Vec::new();
        // Prefer numeric output (no reverse DNS)
        v.push("-n".to_string());

        // IPv6 hint if needed (varies by platform; many ignore -4/-6 gracefully)
        if ip.is_ipv6() {
            v.push("-6".to_string());
        } else {
            #[cfg(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd", target_os = "macos"))]
            v.push("-4".to_string());
        }

        if let Some(c) = count {
            v.push("-c".into());
            v.push(c.to_string());
        }
        if let Some(ms) = interval_ms {
            // `ping -i` takes seconds on most Unix; clamp to sane minimums
            let secs = (ms as f64) / 1000.0;
            v.push("-i".into());
            v.push(format!("{:.3}", secs.max(0.01)));
        }
        v.push(dest_for_cli.clone());
        v
    };

    #[cfg(windows)]
    let args: Vec<String> = {
        let mut v = Vec::new();
        if let Some(c) = count {
            v.push("-n".into());
            v.push(c.to_string());
        }
        // Windows uses -w (timeout) in milliseconds per reply; use interval as timeout.
        if let Some(ms) = interval_ms {
            v.push("-w".into());
            v.push(ms.to_string());
        }
        v.push(dest_for_cli.clone());
        v
    };

    if verbose {
        eprintln!("[net:ping] launching system ping: ping {:?}", args);
    }

    let spawn_res = Command::new("ping")
        .args(&args)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn();

    let mut child = match spawn_res {
        Ok(c) => c,
        Err(e) => {
            let _ = tx.send(NetMessage::PingErr(
                token_id,
                format!("Failed to spawn `ping`: {e}"),
            ));
            let _ = tx.send(NetMessage::PingDone(token_id));
            return;
        }
    };

    // Stream stdout lines as NetMessage::PingLine
    if let Some(stdout) = child.stdout.take() {
        let reader = BufReader::new(stdout);
        for line in reader.lines() {
            if stop_flag.load(Ordering::Relaxed) {
                // Try to terminate the child; ignore errors
                let _ = child.kill();
                let _ = tx.send(NetMessage::PingDone(token_id));
                return;
            }

            match line {
                Ok(mut s) => {
                    // Normalize some common outputs to include our "host (ip)" display
                    if s.contains("bytes from") && !dest_display.is_empty() {
                        // Try to re‑stamp destination if needed
                        if !s.contains(&dest_display) && s.contains(&dest_for_cli) {
                            s = s.replace(&dest_for_cli, &dest_display);
                        }
                    }
                    if !s.trim().is_empty() {
                        let _ = tx.send(NetMessage::PingLine(token_id, s));
                    }
                }
                Err(e) => {
                    let _ = tx.send(NetMessage::PingErr(
                        token_id,
                        format!("Error reading `ping` output: {e}"),
                    ));
                    break;
                }
            }
        }
    }

    // Drain stderr (best effort) so the child can exit cleanly
    if let Some(mut stderr) = child.stderr.take() {
        use std::io::Read;
        let mut _buf = String::new();
        let _ = stderr.read_to_string(&mut _buf);
        // We intentionally ignore forwarding stderr lines; system pings can be noisy.
    }

    let _ = child.kill(); // Ignore errors if already exited
    let _ = tx.send(NetMessage::PingDone(token_id));
}

#[cfg(unix)]
#[allow(dead_code)]
pub fn real_raw_icmp_loop(
    token_id: usize,
    ip: IpAddr,
    maybe_host: Option<String>,
    count: Option<usize>,
    interval_ms: Option<u64>,
    tx: Sender<NetMessage>,
    stop_flag: Arc<AtomicBool>,
    verbose: bool,
) {
    use std::mem::size_of_val;

    // Only IPv4 implemented here (ICMPv6 differs). Fall back if IPv6.
    let v4 = match ip {
        IpAddr::V4(v4) => v4,
        IpAddr::V6(_) => {
            let _ = tx.send(NetMessage::PingErr(
                token_id,
                "raw ICMP: IPv6 not implemented in this plugin".to_string(),
            ));
            let _ = tx.send(NetMessage::PingDone(token_id));
            return;
        }
    };

    unsafe {
        // Create raw ICMP socket
        let fd = libc::socket(libc::AF_INET, libc::SOCK_RAW, libc::IPPROTO_ICMP);
        if fd < 0 {
            let _ = tx.send(NetMessage::PingErr(
                token_id,
                format!(
                    "raw ICMP: socket() failed (are you root?) errno={}",
                    *libc::__errno_location()
                ),
            ));
            let _ = tx.send(NetMessage::PingDone(token_id));
            return;
        }

        // Set a receive timeout so we don't block forever
        let tv = libc::timeval {
            tv_sec: 1,
            tv_usec: 0,
        };
        let r = libc::setsockopt(
            fd,
            libc::SOL_SOCKET,
            libc::SO_RCVTIMEO,
            &tv as *const libc::timeval as *const libc::c_void,
            size_of_val(&tv) as libc::socklen_t,
        );
        if r != 0 && verbose {
            eprintln!(
                "[raw-icmp] warning: setsockopt SO_RCVTIMEO failed errno={}",
                *libc::__errno_location()
            );
        }

        // Destination sockaddr_in
        let oct = v4.octets();
        let mut addr: libc::sockaddr_in = std::mem::zeroed();
        addr.sin_family = libc::AF_INET as libc::sa_family_t;
        addr.sin_port = 0;
        addr.sin_addr = libc::in_addr {
            s_addr: u32::from_be_bytes(oct),
        };

        // Build and send a few echo requests
        let ident: u16 = (libc::getpid() as u16) ^ 0xBEEF;
        let max_count = count.unwrap_or(4);
        let sleep_ms = interval_ms.unwrap_or(1000).max(10);
        let dest_display = make_display_host_str(&maybe_host, ip);

        for seq in 1..=max_count {
            if stop_flag.load(Ordering::Relaxed) {
                break;
            }

            // ICMP Echo layout: type(8) code(0) cksum u16 id u16 seq u16 payload...
            let mut packet = Vec::<u8>::new();
            packet.push(8); // Echo request
            packet.push(0); // code
            packet.extend_from_slice(&[0, 0]); // checksum placeholder
            packet.extend_from_slice(&ident.to_be_bytes());
            packet.extend_from_slice(&(seq as u16).to_be_bytes());

            // Simple payload (timestamp in nanos to aid RTT calc if needed)
            let now_ns = Instant::now();
            let stamp = now_ns.elapsed().as_nanos() as u64; // pseudo‑unique per iteration
            packet.extend_from_slice(&stamp.to_be_bytes());
            if packet.len() < 56 {
                // typical Linux ping default payload ~56 bytes (excluding ICMP header)
                packet.resize(56, 0);
            }

            // Compute checksum
            {
                let cksum = icmp_checksum(&packet);
                packet[2] = (cksum >> 8) as u8;
                packet[3] = (cksum & 0xFF) as u8;
            }

            let send_start = Instant::now();

            // Send
            let send_res = libc::sendto(
                fd,
                packet.as_ptr() as *const libc::c_void,
                packet.len(),
                0,
                &addr as *const libc::sockaddr_in as *const libc::sockaddr,
                std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
            );
            if send_res < 0 {
                let _ = tx.send(NetMessage::PingErr(
                    token_id,
                    format!(
                        "raw ICMP: sendto() failed errno={}",
                        *libc::__errno_location()
                    ),
                ));
                break;
            }

            // Receive
            let mut buf = [0u8; 2048];
            let mut got_reply = false;
            loop {
                if stop_flag.load(Ordering::Relaxed) {
                    break;
                }
                let n = libc::recv(
                    fd,
                    buf.as_mut_ptr() as *mut libc::c_void,
                    buf.len(),
                    0,
                );
                if n < 0 {
                    // likely timeout
                    break;
                }
                if (n as usize) < 28 {
                    // too short for IPv4 + ICMP
                    continue;
                }

                // Parse IPv4 header to find ICMP offset
                let ip_header_len = ((buf[0] & 0x0F) * 4) as usize;
                if ip_header_len + 8 > n as usize {
                    continue;
                }
                let icmp = &buf[ip_header_len..];
                let icmp_type = icmp[0];
                let icmp_code = icmp[1];
                if icmp_type != 0 || icmp_code != 0 {
                    continue; // not Echo Reply
                }
                let recv_ident = u16::from_be_bytes([icmp[4], icmp[5]]);
                let recv_seq = u16::from_be_bytes([icmp[6], icmp[7]]);
                if recv_ident != ident || recv_seq != (seq as u16) {
                    continue;
                }

                let rtt_ms = send_start.elapsed().as_secs_f64() * 1000.0;
                let line = format!(
                    "{} bytes from {}: icmp_seq={} ttl=? time={:.2} ms",
                    n,
                    dest_display,
                    seq,
                    rtt_ms
                );
                let _ = tx.send(NetMessage::PingLine(token_id, line));
                got_reply = true;
                break;
            }

            if !got_reply {
                let _ = tx.send(NetMessage::PingLine(
                    token_id,
                    format!("Request timeout for icmp_seq {}", seq),
                ));
            }

            // Sleep between probes (early‑exit if asked to stop)
            let mut slept = 0u64;
            while slept < sleep_ms {
                if stop_flag.load(Ordering::Relaxed) {
                    break;
                }
                let step = (sleep_ms - slept).min(50);
                std::thread::sleep(Duration::from_millis(step));
                slept += step;
            }
        }

        let _ = tx.send(NetMessage::PingDone(token_id));
        let _ = libc::close(fd);
    }
}

#[cfg(not(unix))]
#[allow(dead_code)]
pub fn real_raw_icmp_loop(
    token_id: usize,
    _ip: IpAddr,
    _maybe_host: Option<String>,
    _count: Option<usize>,
    _interval_ms: Option<u64>,
    tx: Sender<NetMessage>,
    _stop_flag: Arc<AtomicBool>,
    _verbose: bool,
) {
    let _ = tx.send(NetMessage::PingErr(
        token_id,
        "raw ICMP: not supported on this platform".to_string(),
    ));
    let _ = tx.send(NetMessage::PingDone(token_id));
}

#[allow(dead_code)]
pub fn icmp_checksum(data: &[u8]) -> u16 {
    // 16‑bit one's‑complement sum. If len is odd, pad with zero.
    let mut sum: u32 = 0;

    let mut chunks = data.chunks_exact(2);
    for chunk in &mut chunks {
        let word = u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
        sum = sum.wrapping_add(word);
    }

    if let [last] = chunks.remainder() {
        let word = u16::from_be_bytes([*last, 0]) as u32;
        sum = sum.wrapping_add(word);
    }

    // Fold carries
    while (sum >> 16) != 0 {
        sum = (sum & 0xFFFF) + (sum >> 16);
    }

    !(sum as u16)
}