🦀

Rust で ICMP パケットをキャプチャする

2025/03/04に公開

概要

ネットワークの勉強のために ICMP パケットをキャプチャするプログラムを作成しました。
作成したプログラムについて、説明を記載します。

作成したプログラムは以下のリポジトリにあります。
https://github.com/shu-kitamura/icmp-packet-capture

依存関係

今回作成したプログラムでは、以下の4つのクレートを利用しています。

  • clap
  • pnet
  • pnet_datalink
  • thiserror

Cargo.toml の dependencies に以下の内容を記載します。

[dependencies]
clap = { version = "4.5.31", features = ["derive"] }
pnet = "0.35.0"
pnet_datalink = "0.35.0"
thiserror = "1.0"

作成したプログラム

作成したプログラムの main 関数は以下のようになりました。

fn main() {
    // コマンドライン引数をパースする
    let args = Args::parse();
    let interface_name: String = args.network_interface;

    // ネットワークインタフェースを取得する
    let network_interface: NetworkInterface = match get_network_interface(&interface_name) {
        Ok(i) => i,
        Err(e) => panic!("{e}"),
    };

    // チャネルを開く
    let (_, mut rx) = match pnet_datalink::channel(&network_interface, Default::default()) {
        Ok(Ethernet(tx, rx)) => (tx, rx),
        Ok(_) => panic!("Unhandled channel type."),
        Err(e) => panic!("Failed to create channel. {}", e),
    };

    println!("Listening started on {}.", network_interface.name);

    // パケットを受信し、処理する
    loop {
        match rx.next() {
            Ok(bytes) => {
                let ethernet_frame = match create_ethernet_frame(bytes) {
                    Ok(p) => p,
                    Err(e) => panic!("{e}")
                };
                if let Err(e) = handle_ethernet_frame(ethernet_frame) {
                    eprintln!("{e}");
                }
            },
            Err(e) => panic!("Failed to read packet. {}", e),
        }
    }
}

上記の関数は、以下の処理を行っています。

  1. コマンドライン引数をパースする
  2. ネットワークインタフェースを取得する
  3. チャネルを開く
  4. パケットを受信し、表示する

それぞれの処理について、説明を記載していきます。

コマンドライン引数をパースする

コマンドライン引数のパースには clap を使用しています。

// コマンドライン引数の定義
#[derive(Debug, Parser)]
#[command(version, about, long_about = None)]
struct Args {
    /// Network interface name
    #[arg(value_name = "NETWORK_INTERFACE")]
    network_interface: String,
}

ネットワークインタフェースを取得する

ネットワークインタフェースは、以下の関数で取得します。

pnet_datalink::interfaces() でインタフェースの一覧を取得し、引数に指定されたインタフェースと一致するものを返します。
一致するものが無い場合、エラーを返します。

/// 指定された名前のネットワークインタフェースを取得します。
pub fn get_network_interface(interface_name: &str) -> Result<NetworkInterface, PacketCaptureError> {
    // ネットワークインタフェースの一覧を取得し、名前が一致するものを返す
    for iface in pnet_datalink::interfaces() {
        if iface.name == interface_name {
            return Ok(iface);
        }
    }
    return Err(PacketCaptureError::FailedToGetInterface(interface_name.to_string()))
}

チャネルを開く

チャネルを開く処理は main 関数の以下の部分で行っています。
pnet_datalink::channelを実行しています。

let (_, mut rx) = match pnet_datalink::channel(&network_interface, Default::default()) {
        Ok(Ethernet(tx, rx)) => (tx, rx),
        Ok(_) => panic!("Unhandled channel type."),
        Err(e) => panic!("Failed to create channel. {}", e),
};

パケットを受信し、表示する

受信したデータを以下の順で処理します。

バイトデータ -> Ethernet フレーム -> IPv4 パケット -> ICMP パケット

以下の2点を表示しています。

  • IPv4 を処理する際に、送信元のIPアドレスを表示
  • ICMP パケットは特に処理をせず表示
/// 指定されたバイト列から Ethernet フレームを生成する関数
pub fn create_ethernet_frame(bytes: &[u8]) -> Result<EthernetPacket, PacketCaptureError> {
    match EthernetPacket::new(bytes) {
        Some(p) => Ok(p),
        None => Err(PacketCaptureError::FailedToCreateEthernetPacket),
    }
}

/// 受信した Ethernet フレームを処理する関数
pub fn handle_ethernet_frame(ethernet_packet: EthernetPacket) -> Result<Option<()>, PacketCaptureError> {
    match ethernet_packet.get_ethertype() {
        EtherTypes::Ipv4 => {
            match Ipv4Packet::new(ethernet_packet.payload()) {
                Some(p) => handle_ipv4_packet(p),
                None => return Err(PacketCaptureError::FailedToCreateIpv4Packet),
            }
        },
        _ => Ok(None), // IPv4 以外のパケットには何もしない
    }
}

/// 受信した IPv4 パケットを処理する関数
fn handle_ipv4_packet(ipv4_packet: Ipv4Packet) -> Result<Option<()>, PacketCaptureError> {
    match ipv4_packet.get_next_level_protocol() {
        IpNextHeaderProtocols::Icmp => {
            // 送信元IPアドレスの表示
            print!("Source address: {:?}, ", ipv4_packet.get_source());
            match IcmpPacket::new(ipv4_packet.payload()) {
                Some(p) => handle_icmp_packet(p),
                None => return Err(PacketCaptureError::FailedToCreateIcmpPacket),
            }
        },
        _ => Ok(None) // ICMP 以外のパケットには何もしない
    }
}

/// 受信した ICMP パケットを処理する関数
fn handle_icmp_packet(icmp_packet: IcmpPacket) -> Result<Option<()>, PacketCaptureError> {
    println!("ICMP packet: {:?}", icmp_packet);
    Ok(Some(()))
}

おわりに

今回実装してみて、パケットのカプセル化について、理解が深まったような気がします。
ICMP のパケット以外無視するようにしましたが、余裕があれば TCP/UDP などもキャプチャする処理を書いてみたいと思いました。

Discussion