Rust でのEthernet/IPパケットキャプチャ 実践編

# 突然ですが皆様は「Rust」使ってますか？
私は普段Pythonをメインとしているのですが、高速・安定なパケットキャプチャが欲しくなり
ついでにWindows環境にてRustで作ってみようと思いました

年末年始の時間で概ね動くようになったので、ここに記録します

---
## Cargo.toml 抜粋
dependenciesには下記3つのパッケージを加えてください(2024/08/25現在のVersion)
```toml
[dependencies]
netdev = "0.30.0"
pnet = "0.35.0"
pnet_datalink = "0.35.0"
```

netdev(旧 default-net) は普段メインで利用しているdefault interface情報を取得してくれる便利な奴です
(これが無いとHyper-VとかVMwareで複数のinterfaceが付いている時は取得するinterfaceを手動指定する必要があった...)

libpnet(pnet / pnet_datalink)は interfaceからパケットキャプチャするためのパッケージです
なお、__Windowsでは別途Npcapが必要となります__(後述)

---

## Rustに実装してみた
実際にコードを書いてみるとこんな感じになります
ここではIPv4 + TCPセグメント のデータだけ欲しいので大分端折ってます

IPv6やICMP, UDPも必要といった場合は下記libpnetのサンプルコードを元に拡張して下さい
https://github.com/libpnet/libpnet/blob/master/examples/packetdump.rs

```rust:packet_capture.rs
extern crate netdev;
extern crate pnet;
extern crate pnet_datalink;

use std::{net::IpAddr, thread};

use pnet::packet::{
    ethernet::{EtherTypes, EthernetPacket},
    ip::{IpNextHeaderProtocol, IpNextHeaderProtocols},
    ipv4::Ipv4Packet,
    tcp, Packet,
};
use pnet_datalink::{Channel::Ethernet, DataLinkReceiver, DataLinkSender, NetworkInterface};

pub fn ignition() {
    let mut _best_device_name: String = "\\Device\\NPF_".to_string();

    match netdev::get_default_interface() {
        Ok(_best_interface) => {
            println!("Best Interface Name: {}", _best_interface.name);
            _best_device_name += &_best_interface.name;
            println!(
                "Friendly Name: {:?}",
                _best_interface.friendly_name.unwrap()
            );
        }
        Err(ex) => {
            panic!("Error while find the best interface. {}", ex);
        }
    }

    // すべてのネットワークインターフェースを取得
    let _all_interfaces: Vec<NetworkInterface> = pnet_datalink::interfaces();

    let _default_interface: Option<NetworkInterface> = _all_interfaces
        .iter()
        .find(|iface| iface.is_loopback() == false && iface.name == _best_device_name)
        .cloned();

    if let Some(interface) = _default_interface {
        println!("Found default interface with [{}].", interface.name);

        let builder = thread::Builder::new().name("ro-indexer-packet-caputure".into());

        let handle = builder
            .spawn(move || {
                let (_, mut rx): (Box<dyn DataLinkSender>, Box<dyn DataLinkReceiver>) =
                    match pnet_datalink::channel(&interface, Default::default()) {
                        Ok(Ethernet(tx, rx)) => (tx, rx),
                        Ok(_) => panic!("Unhandled channel type"),
                        Err(ex) => panic!("Unable to create channel: {}", ex),
                    };
                loop {
                    match rx.next() {
                        Ok(_packet) => match EthernetPacket::new(_packet) {
                            Some(frame) => {
                                handle_ethernet_frame(&frame);
                            }
                            _ => {}
                        },
                        Err(ex) => panic!("Unable to recive frame: {}", ex),
                    }
                }
            })
            .unwrap();

        match handle.join() {
            Ok(_) => {
                println!("")
            }
            Err(ex) => {
                println!("Error: {:?}", ex)
            }
        }
    } else {
        panic!("Error while finding the default interface.");
    }
}

fn handle_ethernet_frame(ethernet: &EthernetPacket) {
    match ethernet.get_ethertype() {
        EtherTypes::Ipv4 => handle_ipv4_packet(&ethernet),
        _ => (),
    }
}

fn handle_ipv4_packet(ethernet: &EthernetPacket) {
    let ip_packet: Option<Ipv4Packet> = Ipv4Packet::new(ethernet.payload());

    match ip_packet {
        Some(ip_packet) => {
            handle_transport_protocol(
                IpAddr::V4(ip_packet.get_source()),
                IpAddr::V4(ip_packet.get_destination()),
                ip_packet.get_next_level_protocol(),
                ip_packet.payload(),
            );
        }
        _ => (),
    }
}

fn handle_transport_protocol(
    source: IpAddr,
    destination: IpAddr,
    protocol: IpNextHeaderProtocol,
    packet: &[u8],
) {
    match protocol {
        IpNextHeaderProtocols::Tcp => {
            handle_tcp_segment(source, destination, packet);
        }
        _ => (),
    }
}

fn handle_tcp_segment(source: IpAddr, destination: IpAddr, packet: &[u8]) {
    let segment: Option<tcp::TcpPacket> = tcp::TcpPacket::new(packet);
    match segment {
        Some(segment) => {
            println!(
                "{}:{} -> {}:{}",
                source,
                segment.get_source(),
                destination,
                segment.get_destination()
            );
            //ここでTCPパケットの送信元IPv4アドレス&ポート + 宛先IPv4アドレス&ポートが見える
        }
        _ => (),
    }
}
```

あとはmain.rsを作成し、上記のpacket_capture.rsを読み込み実行すれば動く(はず)
```rust:main.rs
mod packet_capture;

fn main() {
    packet_capture::ignition();
}
```

コンパイルする際に下記のPowerShell実行すると良い
```powershell:build.ps1
rustup default stable-x86_64-pc-windows-gnu
cargo clean
$env:RUSTFLAGS = "-L .\lib\npcap\x64"
cargo build --release
```

こんな感じで送信元IPv4アドレス&ポート + 宛先IPv4アドレス&ポートの出力が流れたら成功
![キャプチャ画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15104/379ce018-391c-56be-30aa-38282c325da4.png)

---
## ハマりポイント
- Npcapは「Install Npcap WinPcap API-compatible Mode」にチェックを入れてインストールすること
![install_npcap_winpcap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15104/9836ac59-a7f1-0c36-c2dc-6942b18fefd6.png)

- libpnetの exsampleが間違ってる
pnet::datalinkが無い...って悩んでたらpnet_datalinkに改名されてた

- Rustの仕様がまだ分かってないのである！
これから精進していきます！

---

2024年になって再度触ってみたら動かなかったので、更新しました