[
  {
    "path": ".gitignore",
    "content": "/target\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"pong\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nanyhow = \"1.0.52\"\nlibc = \"0.2.112\"\nnix = \"0.23.1\"\nsignal-hook = \"0.3.13\"\n"
  },
  {
    "path": "README.md",
    "content": "# Pong\n\nA Linux program that replies to `ping` but modifies the payload of the ICMP\npacket to get lower ping times in some `ping` implementations.\n\nSee\n- https://twitter.com/m_ou_se/status/1480184730058375176\n- https://twitter.com/m_ou_se/status/1480184732562374656\n- https://twitter.com/m_ou_se/status/1480188334605578242\n\nInstall it with `cargo install --git https://github.com/m-ou-se/pong`.\n\nYou either need to run it as root, or you need to disable your kernel's ping\nreply with `sysctl net.ipv4.icmp_echo_ignore_all=1 net.ipv6.icmp.echo_ignore_all=1`\nand give this program `cap_net_raw` capabilities with `setcap cap_net_raw=ep ~/.cargo/bin/pong`.\n"
  },
  {
    "path": "src/disable_system_pong.rs",
    "content": "use anyhow::{bail, Context, Result};\nuse std::fs::{read, write};\n\nconst PATH_IPV4: &str = \"/proc/sys/net/ipv4/icmp_echo_ignore_all\";\nconst PATH_IPV6: &str = \"/proc/sys/net/ipv6/icmp/echo_ignore_all\";\n\n#[must_use]\npub struct DisableSystemPong {\n    reenable_ipv4_on_drop: bool,\n    reenable_ipv6_on_drop: bool,\n}\n\nimpl DisableSystemPong {\n    pub fn activate() -> Result<Self> {\n        Ok(DisableSystemPong {\n            reenable_ipv4_on_drop: if read(PATH_IPV4)\n                .with_context(|| format!(\"unable to read {}\", PATH_IPV4))?\n                == b\"1\\n\"\n            {\n                // Already disabled.\n                false\n            } else {\n                if write(PATH_IPV4, \"1\\n\").is_err() {\n                    bail!(\n                        \"unable to disable the system's IPv4 ICMP echo reply\\n\\n\\\n                        Disable it manually (using `sysctl net.ipv4.icmp_echo_ignore_all=1`), \\\n                        or re-run this program as root.\"\n                    );\n                }\n                eprintln!(\"disabled the system's IPv4 ICMP echo reply\");\n                true\n            },\n            reenable_ipv6_on_drop: if read(PATH_IPV6)\n                .with_context(|| format!(\"unable to read {}\", PATH_IPV6))?\n                == b\"1\\n\"\n            {\n                // Already disabled.\n                false\n            } else {\n                if write(PATH_IPV6, \"1\\n\").is_err() {\n                    bail!(\n                        \"unable to disable the system's IPv6 ICMP echo reply\\n\\n\\\n                        Disable it manually (using `sysctl net.ipv6.icmp.echo_ignore_all=1`), \\\n                        or re-run this program as root.\"\n                    );\n                }\n                eprintln!(\"disabled the system's IPv6 ICMP echo reply\");\n                true\n            },\n        })\n    }\n\n    pub fn deactivate(&mut self) -> Result<()> {\n        if self.reenable_ipv4_on_drop {\n            self.reenable_ipv4_on_drop = false;\n            write(PATH_IPV4, \"0\\n\")\n                .context(\"unable to re-enable the system's IPv4 ICMP echo reply\")?;\n            eprintln!(\"re-enabled the system's IPv4 ICMP echo reply\");\n        }\n        if self.reenable_ipv6_on_drop {\n            self.reenable_ipv6_on_drop = false;\n            write(PATH_IPV6, \"0\\n\")\n                .context(\"unable to re-enable the system's IPv6 ICMP echo reply\")?;\n            eprintln!(\"re-enabled the system's IPv6 ICMP echo reply\");\n        }\n        Ok(())\n    }\n}\n\nimpl Drop for DisableSystemPong {\n    fn drop(&mut self) {\n        let _ = self.deactivate();\n    }\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "#[cfg(not(target_os = \"linux\"))]\ncompile_error!(\"this program only works on Linux\");\n\nmod disable_system_pong;\n\nuse anyhow::{bail, Context, Result};\nuse disable_system_pong::DisableSystemPong;\nuse libc::{socket, AF_INET, AF_INET6, IPPROTO_ICMP, IPPROTO_ICMPV6, SOCK_RAW};\nuse nix::errno::Errno;\nuse nix::poll::{poll, PollFd, PollFlags};\nuse std::io::ErrorKind;\nuse std::net::UdpSocket;\nuse std::os::unix::io::{AsRawFd, FromRawFd};\nuse std::os::unix::net::UnixStream;\nuse std::time::{Duration, SystemTime};\n\n// It's not really a UdpSocket, but there's no IcmpSocket in std and the interface is close enough. :)\ntype IcmpSocket = UdpSocket;\n\nfn open_icmp_socket(ipv6: bool) -> Result<IcmpSocket> {\n    let sock = unsafe {\n        if ipv6 {\n            socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6)\n        } else {\n            socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)\n        }\n    };\n\n    if sock == -1 {\n        let err = std::io::Error::last_os_error();\n        if err.kind() == ErrorKind::PermissionDenied {\n            bail!(\n                \"unable to create a raw {} ICMP socket\\n\\n\\\n                Re-run this program as root or with cap_net_raw capabilities \\\n                (using `setcap cap_net_raw=ep {:?}`).\",\n                if ipv6 { \"IPv6\" } else { \"IPv4\" },\n                std::env::args_os().next().unwrap()\n            );\n        } else {\n            return Err(err).context(\"creating raw ICMP socket failed\");\n        }\n    }\n\n    Ok(unsafe { IcmpSocket::from_raw_fd(sock) })\n}\n\nfn main() -> Result<()> {\n    let (stop_read, stop_write) = UnixStream::pair()?;\n    for &signal in signal_hook::consts::TERM_SIGNALS {\n        signal_hook::low_level::pipe::register(signal, stop_write.try_clone()?)?;\n    }\n\n    let mut disable_pong = DisableSystemPong::activate()?;\n\n    let sock4 = open_icmp_socket(false)?;\n    let sock6 = open_icmp_socket(true)?;\n\n    let mut buf = [0; 1024];\n\n    loop {\n        let mut fds = [\n            PollFd::new(sock4.as_raw_fd(), PollFlags::POLLIN),\n            PollFd::new(sock6.as_raw_fd(), PollFlags::POLLIN),\n            PollFd::new(stop_read.as_raw_fd(), PollFlags::POLLIN),\n        ];\n\n        match poll(&mut fds, -1) {\n            Ok(_) => {}\n            Err(e) if e == Errno::EINTR => {}\n            Err(e) => return Err(e).context(\"polling the sockets failed\"),\n        }\n\n        if fds[2].revents().unwrap().contains(PollFlags::POLLIN) {\n            // Got signal. Exiting.\n            break;\n        }\n\n        let now = SystemTime::now()\n            .duration_since(SystemTime::UNIX_EPOCH)\n            .unwrap();\n\n        for (ipv6, fd, sock) in [(false, fds[0], &sock4), (true, fds[1], &sock6)] {\n            if fd.revents().unwrap().contains(PollFlags::POLLIN) {\n                let ip_header_len = if ipv6 { 0 } else { 20 };\n\n                let (len, addr) = sock.recv_from(&mut buf).context(\"recv_from failed\")?;\n\n                if len < ip_header_len + 8 {\n                    continue;\n                }\n\n                let icmp = &mut buf[ip_header_len..len];\n\n                let (req_type, reply_type) = if ipv6 { (128, 129) } else { (8, 0) };\n\n                if icmp[0..2] != [req_type, 0] {\n                    // Not a ping packet.\n                    continue;\n                }\n\n                let id = u16::from_be_bytes(icmp[4..6].try_into().unwrap());\n                let seq = u16::from_be_bytes(icmp[6..8].try_into().unwrap());\n\n                let parsed = parse_payload(now, &icmp[8..]);\n\n                // Transform ping packet into pong packet\n                icmp[0] = reply_type;\n                icmp[1] = 0;\n\n                // Change the payload to get better ping times.\n                if let Some((encoding, timestamp)) = parsed {\n                    let new_timestamp =\n                        if timestamp < now && (now - timestamp) < Duration::from_millis(500) {\n                            // Looks like the sender is ntp-synchronized.\n                            // Calculate the arrival time, minus five milliseconds to make it somewhat realistic.\n                            now + (now - timestamp) - Duration::from_millis(5)\n                        } else {\n                            // The sender's clock isn't the same as ours.\n                            // Just decrease the ping time by 50ms.\n                            timestamp + Duration::from_millis(50)\n                        };\n                    write_timestamp_into_payload(&mut icmp[8..], encoding, new_timestamp);\n                } else {\n                    print!(\"unknown encoding for ping payload: \");\n                    for &b in &icmp[8..] {\n                        print!(\"{:02x} \", b);\n                    }\n                    println!();\n                }\n\n                // Update the checksum\n                let checksum = checksum(&icmp[4..]);\n                icmp[2..4].copy_from_slice(&checksum.to_be_bytes());\n\n                // Pong!\n                sock.send_to(icmp, addr).context(\"send_to failed\")?;\n\n                println!(\n                    \"{}: {} bytes, id={}, seq={}, encoding={}\",\n                    addr.ip(),\n                    icmp.len(),\n                    id,\n                    seq,\n                    match parsed {\n                        Some((PayloadEncoding::Le64, _)) => \"le64\",\n                        Some((PayloadEncoding::Be64, _)) => \"be64\",\n                        Some((PayloadEncoding::Le32, _)) => \"le32\",\n                        Some((PayloadEncoding::Be32, _)) => \"be32\",\n                        None => \"unknown\",\n                    }\n                );\n            }\n        }\n    }\n\n    disable_pong.deactivate()?;\n\n    Ok(())\n}\n\n#[derive(Copy, Clone, PartialEq, Eq, Debug)]\nenum PayloadEncoding {\n    Le64,\n    Be64,\n    Le32,\n    Be32,\n}\n\nfn parse_payload(now: Duration, payload: &[u8]) -> Option<(PayloadEncoding, Duration)> {\n    if payload.len() >= 16 {\n        let sec = u64::from_le_bytes(payload[0..8].try_into().unwrap());\n        if sec.abs_diff(now.as_secs()) < 1000 && payload[12..16] == [0; 4] {\n            let usec = u32::from_le_bytes(payload[8..12].try_into().unwrap());\n            let nsec = usec.checked_mul(1000)?;\n            return Some((PayloadEncoding::Le64, Duration::new(sec, nsec)));\n        }\n        let sec = u64::from_be_bytes(payload[0..8].try_into().unwrap());\n        if sec.abs_diff(now.as_secs()) < 1000 && payload[8..12] == [0; 4] {\n            let usec = u32::from_be_bytes(payload[12..16].try_into().unwrap());\n            let nsec = usec.checked_mul(1000)?;\n            return Some((PayloadEncoding::Be64, Duration::new(sec, nsec)));\n        }\n    }\n    if payload.len() >= 8 {\n        let sec = u64::from(u32::from_le_bytes(payload[0..4].try_into().unwrap()));\n        if sec.abs_diff(now.as_secs()) < 1000 {\n            let usec = u32::from_le_bytes(payload[4..8].try_into().unwrap());\n            let nsec = usec.checked_mul(1000)?;\n            return Some((PayloadEncoding::Le32, Duration::new(sec, nsec)));\n        }\n        let sec = u64::from(u32::from_be_bytes(payload[0..4].try_into().unwrap()));\n        if sec.abs_diff(now.as_secs()) < 1000 {\n            let usec = u32::from_be_bytes(payload[4..8].try_into().unwrap());\n            let nsec = usec.checked_mul(1000)?;\n            return Some((PayloadEncoding::Be32, Duration::new(sec, nsec)));\n        }\n    }\n    None\n}\n\nfn write_timestamp_into_payload(\n    payload: &mut [u8],\n    encoding: PayloadEncoding,\n    timestamp: Duration,\n) {\n    match encoding {\n        PayloadEncoding::Le64 => {\n            payload[0..8].copy_from_slice(&timestamp.as_secs().to_le_bytes());\n            payload[8..16].copy_from_slice(&u64::from(timestamp.subsec_micros()).to_le_bytes());\n        }\n        PayloadEncoding::Be64 => {\n            payload[0..8].copy_from_slice(&timestamp.as_secs().to_be_bytes());\n            payload[8..16].copy_from_slice(&u64::from(timestamp.subsec_micros()).to_be_bytes());\n        }\n        PayloadEncoding::Le32 => {\n            payload[0..4].copy_from_slice(&(timestamp.as_secs() as u32).to_le_bytes());\n            payload[4..8].copy_from_slice(&timestamp.subsec_micros().to_le_bytes());\n        }\n        PayloadEncoding::Be32 => {\n            payload[0..4].copy_from_slice(&(timestamp.as_secs() as u32).to_be_bytes());\n            payload[4..8].copy_from_slice(&timestamp.subsec_micros().to_be_bytes());\n        }\n    }\n}\n\nfn checksum(bytes: &[u8]) -> u16 {\n    !bytes\n        .chunks(2)\n        .map(|b| u16::from_be_bytes([b[0], if b.len() == 2 { b[1] } else { 0 }]))\n        .reduce(|a, b| {\n            let (sum, carry) = a.overflowing_add(b);\n            sum + carry as u16\n        })\n        .unwrap_or(0)\n}\n"
  }
]