Skip to content

实现一个ping

本部分内容参考自 Rust 黑客编程 - ICMP 协议 ping 的简单实现

ping是最常用的网络诊断工具之一,用于测试机器之间的连通性。其通过向目标主机发送ICMP(Internet Control Message Protocol)回显请求消息,并等待目标主机返回回显应答消息来判断主机之间是否能够相互通信。ping命令在网络故障排除、网络性能测试以及测量网络延迟和丢包率等方面非常有用。

img

ICMP是一种网络层协议,在网络协议栈中位于IP协议的上层。因此,ping命令作用在网络层(第3层,网络层).

其实准确来说,是3.5层,ICMP协议的报头从IP报头的第160位开始(IP首部20字节)

ICMP是包含在IP数据包中的,但是对ICMP消息通常会特殊处理,会和一般IP数据包的处理不同,而不是作为IP的一个子协议来处理

img

图片来自 Rust 黑客编程 - ICMP 协议 ping 的简单实现[10]

关于ICMP,更多参考 互联网控制消息协议[11]

ping使用ICMP消息作为通信的载体,通过向目标主机发送ICMP Echo Request消息,并等待目标主机返回ICMP Echo Reply消息来测试网络连通性。

很多常用的工具是基于ICMP消息的。ping 和 traceroute 是两个典型.

traceroute 是通过发送包含有特殊的TTL的包,然后接收ICMP超时消息和目标不可达消息来实现的。

ping 则是用ICMP的"Echo request"(类别代码:8)和"Echo reply"(类别代码:0)消息来实现的。

另外mtr其实相当于增强版的traceroute:

My Traceroute (MTR) 是一个结合了traceroute 和ping 的工具,这是测试网络连接和速度的另一个常用方法。 除了网络路径上的跃点外,MTR 还显示到目的地的路线中不断更新的延迟和丢包信息。 可以实时看到路径上发生的情况,协助排除网络问题

什么是 My Traceroute (MTR)?[12]

下面这些代码的作用,是创建和发送 ICMP Echo 请求(通常被称为 ping)并接收响应。程序使用 pnet 库来处理网络通信

程序会不断发送 ICMP Echo 请求到指定的 IP 地址,并等待接收回复。收到回复后,它会打印出从发送到接收回复的往返时间(RTT)。相当于自己实现了常见的网络诊断工具ping,用于测试网络连接的质量和速度。

Cargo.toml:

TOML
[package]
name = "pnet"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
anyhow = "1.0.79"
pnet = "0.34.0"
pnet_transport = "0.34.0"
rand = "0.8.5"
use pnet::packet::{
    icmp::{
        echo_reply::EchoReplyPacket,
        echo_request::{IcmpCodes, MutableEchoRequestPacket},
        IcmpTypes,
    },
    ip::IpNextHeaderProtocols,
    util, Packet,
};
use pnet_transport::icmp_packet_iter;
use pnet_transport::TransportChannelType::Layer4;
use pnet_transport::{transport_channel, TransportProtocol};
use rand::random;
use std::{
    env,
    net::IpAddr,
    sync::{Arc, RwLock},
    time::{Duration, Instant},
};

const ICMP_SIZE: usize = 64; // ICMP数据包的大小

fn main() -> anyhow::Result<()> {
     // 解析命令行参数,获取目标 IP 地址
    let args: Vec<String> = env::args().collect();
    if args.len() < 2 {
        panic!("Usage: icmp-demo target_ip");
    }
    let target_ip: IpAddr = args[1].parse().unwrap();
    println!("icpm echo request to target ip:{:#?}", target_ip);

    // 创建一个传输通道(用于发送和接收 ICMP 数据包)
    // 确定协议 并且创建数据包通道 tx 为发送通道, rx 为接收通道
    let protocol = Layer4(TransportProtocol::Ipv4(IpNextHeaderProtocols::Icmp));
    let (mut tx, mut rx) = match transport_channel(4096, protocol) {
        Ok((tx, rx)) => (tx, rx),
        Err(e) => return Err(e.into()),
    };

    // 将接收通道转换为迭代器,用于处理接收到的 ICMP 数据包
    // 将 rx 接收到的数据包传化为 iterator
    let mut iter = icmp_packet_iter(&mut rx);

    loop {
        let mut icmp_header: [u8; ICMP_SIZE] = [0; ICMP_SIZE];
        let icmp_packet = create_icmp_packet(&mut icmp_header);
        // println!("icmp_packet:{:?}",icmp_packet);
        let timer = Arc::new(RwLock::new(Instant::now()));
        // 发送 ICMP 数据包
        tx.send_to(icmp_packet, target_ip)?;

        match iter.next() {
            // 接收 ICMP Echo 回复,并计算往返时间
            // 匹配 EchoReplyPacket 数据包
            Ok((packet, addr)) => match EchoReplyPacket::new(packet.packet()) {
                Some(echo_reply) => {
                    if packet.get_icmp_type() == IcmpTypes::EchoReply {
                        let start_time = timer.read().unwrap();
                        //let identifier = echo_reply.get_identifier();
                        //let sequence_number =  echo_reply.get_sequence_number();
                        let rtt = Instant::now().duration_since(*start_time);
                        println!(
                            "ICMP EchoReply received from {:?}: {:?} , Time:{:?}",
                            addr,
                            packet.get_icmp_type(),
                            rtt
                        );
                    } else {
                        println!(
                            "ICMP type other than reply (0) received from {:?}: {:?}",
                            addr,
                            packet.get_icmp_type()
                        );
                    }
                }
                None => {}
            },
            Err(e) => {
                println!("An error occurred while reading: {}", e);
            }
        }

        std::thread::sleep(Duration::from_millis(500));
    }

    Ok(())
}

/**
 * 创建 icmp EchoRequest 数据包
 */
fn create_icmp_packet<'a>(icmp_header: &'a mut [u8]) -> MutableEchoRequestPacket<'a> {
    let mut icmp_packet = MutableEchoRequestPacket::new(icmp_header).unwrap();
    icmp_packet.set_icmp_type(IcmpTypes::EchoRequest);
    icmp_packet.set_icmp_code(IcmpCodes::NoCode);
    icmp_packet.set_identifier(random::<u16>());
    icmp_packet.set_sequence_number(1);
    let checksum = util::checksum(icmp_packet.packet(), 1);
    icmp_packet.set_checksum(checksum);

    icmp_packet
}

前端知识体系 · wcrane