使用 Rust 捕獲和解析網絡包

大家好,我是鳥窩。

前兩篇文章介紹了 C++ 和 Go 中利用 TCP Option 中的時間戳實現計算網絡時延。基於 “用 Rust 重寫一切” 的哲學,今天我們來看看 Rust 中如何做這個事情。夜深人靜,再肝一篇關於網絡編程的文章。

Rust 中還沒有和 gopacket 一樣功能強大的包,它的 pcap[1] 用來捕獲網絡包沒有問題,但是缺乏解析的能力,所以我們使用另外一個包 pdu[2] 來實現網絡包的解析。

當然 rust 生態圈中還有其他的包捕獲庫如 pnet[3]、包解析庫如 etherparse[4] 等,但是我選擇了 pcap 和 pdu,因爲針對這篇文章的場景,它們用起來很順手。

爲了簡單起見,我們不像前兩篇文章那樣的程序那麼複雜,還要解析參數,針對參數做不同的處理,這次 Rust 實現的程序中,我們主要實現其最核心的功能:

我是在 Mac mini 的進行開發和運行的,理論在 Linux 上也是可以運行的。

你可能需要安裝libpcap庫。

Mac 上可能你需要臨時設置權限,纔有可能正常運行程序:

sudo chmod 666 /dev/bpf*

首先看看程序運行的效果:

那麼程序一開始,我們開始要使用 pcap 捕獲包:

use std::net::{Ipv4Addr,Ipv6Addr};
use std::ops::Sub;
use std::time::{Duration, UNIX_EPOCH};
use chrono::{DateTime, Local};

use macaddr::MacAddr;
use pcap;
use pdu::*;
use libc;

fn main() {
    // 這個用來記錄flow已經它被捕獲的時間
    let mut map = std::collections::HashMap::new();

    // 在Mac上,使用en1網卡
    let mut cap = pcap::Capture::from_device("en1")
        .unwrap()
        .immediate_mode(true)
        .open()
        .unwrap();

    // 你可以設置filter,這裏我們簡化不進行設置了
    // cap.filter("host 127.0.0.1"true).unwrap();

    while let Ok(packet) = cap.next_packet() {
        // 得到捕獲的包信息

        ......
    }
}

目前我們只能得到捕獲的包信息,包括 pcap 增加的頭信息 (捕獲時間、包長度等) 和包的數據。

我們需要解析包的數據,得到 TCP 包,然後解析 TCP 選項中的時間戳。目前 pcap 不能幫助我們了。

我們在那個 while 循環中一步一步補充省略的代碼:

        let ethernet = EthernetPdu::new(&packet.data).unwrap();

        // 實現代碼,輸出源和目的MAC地址,轉換成MacAddr類型
        let _src_mac = MacAddr::from(ethernet.source_address());
        let _dst_mac = MacAddr::from(ethernet.destination_address());

        // println!("ethernet: src_mac={}, dst_mac={}", src_mac, dst_mac);

        let ei = ethernet.inner();
        let (src_ip,dst_ip, tcp) = match ei {
            Ok(Ethernet::Ipv4(ref ip)) ={
                let src_ip = Ipv4Addr::from(ip.source_address()).to_string();
                let dst_ip = Ipv4Addr::from(ip.destination_address()).to_string();

                let tcp = match ip.inner() {
                    Ok(Ipv4::Tcp(tcp)) => Some(tcp),
                    _ => None
                };

                (src_ip,dst_ip,tcp)
            }
            Ok(Ethernet::Ipv6(ref ip)) ={
                let src_ip = Ipv6Addr::from(ip.source_address()).to_string();
                let dst_ip = Ipv6Addr::from(ip.destination_address()).to_string();

                let tcp = match ip.inner() {
                    Ok(Ipv6::Tcp(tcp)) => Some(tcp),
                    _ => None
                };

                (src_ip,dst_ip,tcp)
            }
            _ =(String::new(),String::new(),None)

        };

        ......

首先解析出ethernet層, 和 gopacket 調用方法不同,但是一樣很簡潔。

ethernet中包含源目的 Mac 地址,如果你需要,你可以調用相應的方法獲取它們。本程序不需要這兩個信息,忽略即可。

接下來解析IP層, 這會涉及到 ipv4 和 ipv6 兩種情況,我們分別處理。

        let ei = ethernet.inner();
        let (src_ip,dst_ip, tcp) = match ei {
            Ok(Ethernet::Ipv4(ref ip)) ={
                let src_ip = Ipv4Addr::from(ip.source_address()).to_string();
                let dst_ip = Ipv4Addr::from(ip.destination_address()).to_string();

                let tcp = match ip.inner() {
                    Ok(Ipv4::Tcp(tcp)) => Some(tcp),
                    _ => None
                };

                (src_ip,dst_ip,tcp)
            }
            Ok(Ethernet::Ipv6(ref ip)) ={
                let src_ip = Ipv6Addr::from(ip.source_address()).to_string();
                let dst_ip = Ipv6Addr::from(ip.destination_address()).to_string();

                let tcp = match ip.inner() {
                    Ok(Ipv6::Tcp(tcp)) => Some(tcp),
                    _ => None
                };

                (src_ip,dst_ip,tcp)
            }
            _ =(String::new(),String::new(),None)

        };

        if tcp.is_none() {
            continue;
        }
        let tcp = tcp.unwrap();

調用inner方法就可以得到IP層的信息,我們處理 ipv4 和 ipv6 兩種情況,分別獲取源目的 IP 地址和 TCP 層這三個數據。

因爲一開始我們沒有設置 filter, 所以這裏捕獲的包很多,比如 UDP 的包、ARP 的包,我們在這裏檢查包是否是 TCP 包,如果不是,我們忽略這個包。當然最好是一開始就設置 filter,性能會更好。

接下來我們解析 TCP 選項中的時間戳:

        let ts = tcp.options().find_map(|option| {
            match option {
                TcpOption::Timestamp{val,ecr} ={
                    Some((val, ecr))
                }
                _ => None
            }
        });

        if ts.is_none() {
            continue;
        }

        if ts.unwrap().1 == 0 && !tcp.syn(){
            continue;
        }

pdu庫的好處是方便解析 TCP 以及它的選項。TCP 的選項可能有好幾個,我們只 match 時間戳的那個,得到時間戳的值和 echo reply 的值。

接下來我們處理數據。首先根據五元組和tval爲 key, 將這個 flow 的信息存儲到 map 中:

        let key = format!("{}:{}->{}:{}-{}",  src_ip, tcp.source_port(),dst_ip,tcp.destination_port(),ts.unwrap().0);
        if !map.contains_key(key.as_str()) {
            map.insert(key, packet.header.ts);
        }

然後我們找反向的 key, 如果存在,就說明有去向,當前處理的是迴向,我們計算兩個捕獲的值的差,就是時延:

        let reverse_key = format!("{}:{}->{}:{}-{}", dst_ip, tcp.destination_port(),src_ip,tcp.source_port(),ts.unwrap().1);
        if map.contains_key(reverse_key.as_str()) {
            map.get(reverse_key.as_str()).map(|ts| {
                let rtt = timeval_diff_str(ts,&packet.header.ts);
                println!("{} {} {}:{}->{}:{}", timeval_to_current_time_str(&packet.header.ts), rtt,dst_ip, tcp.destination_port(),src_ip,tcp.source_port());
            });
        }

當然爲了避免map中的數據越積越多,我們可以定期清理一下,這裏我們根據 map 中的元素的數量決定要不要清理:

        if map.len() > 10_000 {
            map.retain(|_,v| {
                let now = std::time::SystemTime::now();
                let duration = now.duration_since(UNIX_EPOCH).unwrap();
                let ts = Duration::new(v.tv_sec as u64, v.tv_usec as u32 * 1000);
                duration.sub(ts).as_secs() < 60
            });
        }

然後補充兩個計算時間的輔助程序,這就是這個程序的全部代碼了:

fn timeval_to_current_time_str(tv: &libc::timeval) -> String {
    let secs = tv.tv_sec as u64;
    let nsecs = (tv.tv_usec as u32 * 1000) as u64;

    let duration = UNIX_EPOCH + std::time::Duration::new(secs, nsecs as u32);
    let datetime = DateTime::<Local>::from(duration);

    datetime.format("%H:%M:%S").to_string()
}

fn timeval_diff_str(start: &libc::timeval, end: &libc::timeval) -> String {
    let secs = end.tv_sec as i64 - start.tv_sec as i64;
    let usecs = end.tv_usec as i64 - start.tv_usec as i64;
    let (secs, usecs) = if usecs < 0 {
        (secs - 1, usecs + 1_000_000)
    } else {
        (secs, usecs)
    };

    format_duration(secs, usecs as u32)
}

fn format_duration(secs: i64, usecs: u32) -> String {
    let duration = secs * 1_000_000 + usecs as i64;
    match duration {
        0..=999_999 => format!("{:.3}ms", duration as f64 / 1_000.0),
        _ => format!("{:.6}s", duration as f64 / 1_000_000.0),
    }
}

你對 Rust 實現的 pping 有什麼看法,歡迎在評論區留下你寶貴的意見。

參考資料

[1]

pcap: https://crates.io/crates/pcap

[2]

pdu: https://docs.rs/pdu/latest/pdu/

[3]

pnet: https://github.com/libpnet/libpnet

[4]

etherparse: https://crates.io/crates/etherparse

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