LoginSignup
8
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-01-11

突然ですが皆様は「Rust」使ってますか?

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

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


Cargo.toml 抜粋

dependenciesには下記3つのパッケージを加えてください(2023/01/05現在のVersion)

[dependencies]
default-net = { version = "0.12.0" }
pnet = { version = "0.31.0" }
pnet_datalink = { version = "0.31.0" }

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

packet_capture.rs
extern crate default_net;
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 default_net::get_default_interface() {
        Ok(_best_interface) => {
            debug!("Best Interface");
            debug!("\tName: {}", _best_interface.name);
            _best_device_name += &_best_interface.name;
            debug!(
                "\tFriendly 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 {
        info!("Found default interface with [{}].", interface.name);
        let _ = thread::Builder::new()
            .name("capture_packet".to_string())
            .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),
                    }
                }
            });
    } else {
        panic!("Error while finding the default interface.");
    }
}

fn handle_ethernet_frame(ethernet: &EthernetPacket) {
    match frame.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を読み込み実行すれば動く(はず)

main.rs
mod packet_capture;

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

こんな感じで送信元IPv4アドレス&ポート + 宛先IPv4アドレス&ポートの出力が流れたら成功
キャプチャ画面


ハマりポイント

  • Npcapは「Install Npcap WinPcap API-compatible Mode」にチェックを入れてインストールすること
    install_npcap_winpcap.png

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

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


これからについて

今はTCPセグメントのヘッダ部分までしか見えてないので、
私はこれからペイロードを処理するプログラムを書きます...

8
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
1