Rust pnet 庫的使用

簡介

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 的主要功能和作用包括:

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),在滿足以下條件的情況下打印接口的名稱:

  1. 如果滿足條件,將打印出滿足條件的接口的名稱。

用於獲取本地計算機上的活躍網絡接口,並輸出滿足特定條件的接口的名稱。這在諸如網絡監控、網絡配置等應用場景非常有用。

關於 "eth0" 和 "tun3", 這是兩種不同類型的網絡接口,簡言之,"eth0" 是一種物理以太網接口,通常用於常規的網絡通信,而 "tun3" 是一種虛擬網絡接口,通常用於建立安全的隧道連接。

二者詳細的不同功能和特點:

  1. eth0:
  1. 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/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Xm1JmxvqWMtsbW1heScH0Q