Rust pnet库的使用

Rust
342
0
0
2024-04-03

简介

pcap与libpcap

可以理解为, pcap是一种文件格式(其实是一种接口格式),其名称来源于“抓包”(packet capture)

而libpcap是 类Unix系统中的一个函数库, 可以解析和处理pcap格式的文件. Windows上有类似的实现(WinPcap,npcap)

(pcap 是早期的网络抓包格式, 下一代抓包格式叫pcapng,二者可以通tcpdump或wireshark互转, 使用libpcap解析pcap和pcapng文件[1])

很多语言写的网络工具,底层都需要调用机器上的libpcap库,比如之前用过的流量回放工具goreplay

pnet

pnet是Rust语言中的一个网络库,主要用于构建网络应用程序。

pnet的主要功能和作用包括:

  • 提供低级网络接口,可以直接操作网络包、协议等。
  • 支持常见网络协议如TCP、UDP等。
  • 实现网络数据包的封装和解析功能。
  • 提供跨平台支持,同时支持Linux、Windows和MacOS。
  • 与其他Rust网络库如Tokio等很好集成。

Rust的libpnet库底层使用了libpcap库来实现网络数据包捕获和处理的功能。

libpnet是一个基于Rust语言的网络编程库,提供了对网络协议的解析、构建和发送功能。它建立在libpcap(或者Windows上的WinPcap)之上,通过调用libpcap提供的底层功能来进行网络数据包捕获。

libpcap(Packet Capture Library)是一个跨平台的网络数据包捕获库,广泛用于网络分析和网络安全领域。它提供了一组API,允许开发人员在应用程序中以编程方式捕获和处理网络数据包。

libpnet库在其底层实现中使用libpcap来访问网络接口、捕获数据包、解析协议以及构建和发送数据包。这使得libpnet能够提供强大的网络编程功能,并且可以与现有的网络工具和库进行集成。

使用libpnet库时,需要确保安装了libpcap库及其开发包,以便在编译和运行时能够正确地链接和使用libpcap

使用

github.com/libpnet/libpnet[2]

Rust中非常多的网络工具依赖于pnet[3],

例如,鸟窝老师写的一个类似ping的工具: 使用rust重写: 和Go版本mping比较[4]

Rust 黑客编程 - ICMP 协议 ping 的简单实现[5]

Rust初探: 实现一个Ping[6]

获取本机活跃的网口名称(其实就是网卡,有实体的,也有虚拟的)

本部分内容参考自 rust 使用pnet获取本地活动的网卡[7]

use std::net::Ipv4Addr; // 导入Ipv4Addr结构体
use pnet::datalink; // 导入datalink模块
use pnet::ipnetwork; // 导入ipnetwork模块

fn main() {
    let interfaces = datalink::interfaces(); // 获取所有网络接口信息

    for interface in interfaces {
        let ip: Vec<Ipv4Addr> = interface.ips.iter().map(|ip| match ip {
            ipnetwork::IpNetwork::V4(ref ipv4) => Ok(ipv4.ip()), // 提取IPv4地址
            _ => Err(""), // 其他类型的地址暂时忽略
        }).filter_map(Result::ok).collect(); // 过滤出成功匹配的IPv4地址,并收集到向量中

        #[cfg(unix)] // Unix系统条件编译
        if !ip.is_empty() && !interface.is_loopback() && interface.is_running() && interface.is_up() {
            println!("{}", interface.name); // 打印接口名称
        }

        #[cfg(not(unix))] // 非Unix系统条件编译
        if !ip.is_empty() && !interface.is_loopback() && interface.is_running() && interface.is_up() {
            println!("{}", interface.name); // 打印接口名称
        }
    }
}

上面这段代码的作用是获取本地计算机上的网络接口信息,并打印出满足特定条件的接口的名称。

  1. 使用datalink::interfaces()函数获取本地计算机上的所有网络接口信息,并将其存储在interfaces变量中。
  2. 针对每个网络接口进行迭代处理。
  3. 对于每个接口,提取其中的IPv4地址,并将其存储在ip变量中。
  4. 根据操作系统类型(Unix或非Unix),在满足以下条件的情况下打印接口的名称:
  • 接口的IPv4地址列表非空。
  • 接口不是回环接口(Loopback)。
  • 接口正在运行且处于启用状态。
  1. 如果满足条件,将打印出满足条件的接口的名称。

用于获取本地计算机上的活跃网络接口,并输出满足特定条件的接口的名称。这在诸如网络监控、网络配置等应用场景非常有用。

关于"eth0"和"tun3",这是两种不同类型的网络接口,简言之,"eth0"是一种物理以太网接口,通常用于常规的网络通信,而"tun3"是一种虚拟网络接口,通常用于建立安全的隧道连接。

二者详细的不同功能和特点:

  1. eth0:
  • "eth0"是一种以太网接口,通常用于连接本地计算机与局域网或广域网的物理网络连接。
  • 它是基于以太网协议(Ethernet)的网络接口,支持传输各种类型的数据包,如IP、TCP、UDP等。
  • "eth0"通常用于常规的网络通信,如通过网络访问互联网、与其他计算机进行通信等。
  1. tun3:
  • "tun3"是一种虚拟网络接口,通常用于建立虚拟私有网络(VPN)或隧道连接。
  • 它是在操作系统内核中创建的虚拟接口,可用于在公共网络上创建安全的、私密的通信通道。
  • "tun3"接口通过将数据包封装在其他协议中(如IPsec、OpenVPN等)来实现安全的通信。
  • "tun3"通常用于远程访问、跨网络连接、保护敏感数据等场景。

监听指定网络接口上的网络流量,并对接收到的数据包进行解析和处理

本部分内容参考自 007 Rust 网络编程,libpnet 库介绍[8]

使用pnet库来实现网络数据包的捕获和解析

use pnet::datalink::Channel::Ethernet; // 导入以太网通道
use pnet::datalink::{self, NetworkInterface}; // 导入datalink模块中的相关项
use pnet::packet::ethernet::{EtherTypes, EthernetPacket}; // 导入以太网数据包相关项
use pnet::packet::ip::IpNextHeaderProtocols; // 导入IP协议相关项
use pnet::packet::ipv4::Ipv4Packet; // 导入IPv4数据包相关项
use pnet::packet::tcp::TcpPacket; // 导入TCP数据包相关项
use pnet::packet::Packet; // 导入数据包trait

use std::env; // 导入env模块

fn handle_packet(ethernet: &EthernetPacket) {
    // 对Ipv4的包按层解析
    match ethernet.get_ethertype() {
        EtherTypes::Ipv4 => {
            // 如果是IPv4数据包
            let header = Ipv4Packet::new(ethernet.payload()); // 解析IPv4头部
            if let Some(header) = header {
                match header.get_next_level_protocol() {
                    IpNextHeaderProtocols::Tcp => {
                        // 如果是TCP协议
                        let tcp = TcpPacket::new(header.payload()); // 解析TCP头部
                        if let Some(tcp) = tcp {
                            println!(
                                "Got a TCP packet {}:{} to {}:{}",
                                header.get_source(),
                                tcp.get_source(),
                                header.get_destination(),
                                tcp.get_destination()
                            );
                        }
                    }
                    _ => println!("Ignoring non TCP packet"), // 忽略其他非TCP协议
                }
            }
        }
        _ => println!("Ignoring non IPv4 packet"), // 忽略非IPv4数据包
    }
}

fn main() {
    let interface_name = env::args().nth(1).unwrap(); // 获取命令行参数中的接口名称

    // 获取网卡列表
    let interfaces = datalink::interfaces();
    let interface = interfaces
        .into_iter()
        .filter(|iface: &NetworkInterface| iface.name == interface_name) // 根据接口名称过滤网卡列表
        .next()
        .expect("Error getting interface"); // 如果找不到匹配的接口,打印错误消息并退出

    let (_tx, mut rx) = match datalink::channel(&interface, Default::default()) {
        // 创建数据链路层通道,用于接收和发送数据包
        Ok(Ethernet(tx, rx)) => (tx, rx), // 如果通道类型是以太网通道,则将发送和接收通道分别赋值给_tx和rx
        Ok(_) => panic!("Unhandled channel type"), // 如果是其他类型的通道,抛出错误
        Err(e) => panic!(
            "An error occurred when creating the datalink channel: {}",
            e
        ), // 如果创建通道时发生错误,打印错误消息并退出
    };

    loop {
        // 获取收到的包
        match rx.next() {
            Ok(packet) => {
                let packet = EthernetPacket::new(packet).unwrap(); // 解析以太网数据包
                handle_packet(&packet); // 处理接收到的数据包
            }
            Err(e) => {
                panic!("An error occurred while reading: {}", e); // 如果读取数据包时发生错误,打印错误消息并退出
            }
        }
    }
}

执行sudo cargo run en0(网卡名),可以看到如下输出:

其中en0是要监听的网卡名称,可以通过ifconfig命令,或者第一部分的代码拿到

代码的执行流程如下:

  1. 导入所需的库和模块。
  2. 定义了一个handle_packet函数,用于处理接收到的数据包。在函数内部,它首先检查数据包的以太网类型,如果是IPv4数据包,则进一步解析IPv4头部。如果是TCP协议的数据包,则解析TCP头部,并打印源IP地址、源端口、目的IP地址和目的端口。
  3. main函数中,获取命令行参数中指定的网络接口名称。
  4. 调用datalink::interfaces()函数获取所有可用的网络接口列表,并根据指定的接口名称过滤出匹配的接口。
  5. 使用过滤得到的接口,调用datalink::channel函数创建一个以太网通道,用于接收数据包。
  6. 进入一个无限循环,在循环中不断接收数据包并调用handle_packet函数进行处理。
  7. 如果在接收数据包或处理过程中发生错误,将打印错误消息并退出程序。

通过这些,该代码就可以用于实时监听和分析指定网络接口上的TCP流量。

如果用来监听openvpn创建的隧道,则会报错:

pnet_datalink-0.34.0/src/bpf.rs:416:44:
misaligned pointer dereference: address must be a multiple of 0x4 but is 0x11f809e0e

实现一个ping

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

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

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

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

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

图片来自 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:

[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
}

cargo build
# 因为使用了 pcap, 故而需要 root 权限
sudo ./target/debug/pnet 8.8.8.8

因为之前wireshark侦听的是eth0这个网卡,如果"ping" 127.0.0.1,就看不到任何数据包了.

而修改wireshark的网卡为lo这个网卡后,再"ping" 127.0.0.1,就可以看到数据包

参考资料

[1] 使用libpcap解析pcap和pcapng文件: https://blog.csdn.net/wangzhicheng1983/article/details/113710386

[2] github.com/libpnet/libpnet: https://github.com/libpnet/libpnet

[3] pnet: https://crates.io/crates/pnet

[4] 使用rust重写: 和Go版本mping比较: https://colobu.com/2023/10/09/mping-write-by-rust/

[5] Rust 黑客编程 - ICMP 协议 ping 的简单实现: https://liangdi.me/p/rust-hacking-programing-icmp-ping/

[6] Rust初探: 实现一个Ping: https://qingwave.github.io/rust-ping/

[7] rust 使用pnet获取本地活动的网卡: https://blog.csdn.net/nightwindnw/article/details/133850226

[8] 007 Rust 网络编程,libpnet 库介绍: https://blog.csdn.net/lcloveyou/article/details/105933754

[9] Rust 黑客编程 - ICMP 协议 ping 的简单实现: https://liangdi.me/p/rust-hacking-programing-icmp-ping/

[10] Rust 黑客编程 - ICMP 协议 ping 的简单实现: https://liangdi.me/p/rust-hacking-programing-icmp-ping/

[11] 互联网控制消息协议: https://zh.wikipedia.org/wiki/%E4%BA%92%E8%81%94%E7%BD%91%E6%8E%A7%E5%88%B6%E6%B6%88%E6%81%AF%E5%8D%8F%E8%AE%AE

[12] 什么是 My Traceroute (MTR)?: https://www.cloudflare.com/zh-cn/learning/network-layer/what-is-mtr/