Rust 研究：パケット・スニフィング

パケット・スニフィングの Rust での実装について調べましたのでメモを残します。

普通だったら tcpdump とか wireshark を使いそうなところですがあえて Rust でやりました。
Rust 修行中ですので。

本稿では Windows10 での実行を前提にしております。

# 実装例

以下、実装例を示します。
pnet クレイトを使用しました。
さほど難しいコードでもないので解説なしです。

工夫点というほどでもありませんが、

* windows でのインターフェース名は linux よりも面倒なのでインターフェース名を表示するコマンドラインオプションを用意。
* なるべく不格好な unwrap の使用を回避し、Rust の流儀でエラー処理。

```rust
// main.rs
const VERBOSE:bool = false; // true にすると TCP/UDP/ICMP 以外のパケットの存在も確認できる

use pnet::datalink;
use pnet::datalink::Channel::Ethernet;
use pnet::packet::ethernet::EtherTypes;
use pnet::packet::ethernet::EthernetPacket;
use pnet::packet::ipv4::Ipv4Packet;
use pnet::packet::tcp::TcpPacket;
use pnet::packet::udp::UdpPacket;
use pnet::packet::icmp::IcmpPacket;
use pnet::packet::ip::IpNextHeaderProtocols;
use pnet::packet::Packet;
use pnet::packet::tcp::TcpFlags;
use std::net::Ipv4Addr;
use std::env;

fn main() {
    if let Err(e) = run() {
        eprintln!("[Error] {:?}", e);
    }
}

fn run() -> Result<(), MyError> {
    let args: Vec<String> = env::args().collect();
    if args.len() < 2 {
        eprintln!("Usage: {} args", args[0]);
        eprintln!("\tinterfaces ... display interfaces");
        eprintln!("\tsniff interface ... sniff at interface");
        return Ok(());
    }

    let cmd: &str = &args[1];

    match cmd {
        "interfaces" => {
            interfaces();
        },
        "sniff" => {
            if args.len() > 2 {
                let name: &str = &args[2];
                sniff(name)?;
            } else {
                return Err(MyError::TooFewArgumentError);
            }
        },
        _ => {
            return Err(MyError::UnknownCommandError);
        },
    }
    Ok(())
}

// インターフェースを表示する関数
fn interfaces () {
    println!("Interfaces:");
    for interface in datalink::interfaces() {
        println!("{}", interface.name);
        println!("\tdescription: {}", interface.description);
        if let Some(mac) = interface.mac {
            println!("\tmac: {}", mac);
        }
        print!("\tips: ");
        for ip in &interface.ips {
            print!("{} ", ip);
        }
        println!();
    }
}

// パケット・スニッフィングを行う関数
fn sniff(interface_name: &str) -> Result<(), MyError>{
    // 保持するインターフェースの中から該当するものを見つける
    let result = datalink::interfaces().into_iter()
        .find(|interface|interface.name == interface_name);
    // 該当するインターフェースがないときはエラー
    if result.is_none() {
        return Err(MyError::UnknownInterfaceError);
    }
    let interface = result.unwrap();

    println!("Inteface: {}", interface.name);

    // イーサネットフレームを受信するチャネル rx を取得
    if let Ethernet(_, mut rx) = datalink::channel(&interface, 
        Default::default())? {
        loop {
            // rx からイーサネットフレームを取り出して処理する
            let frame = rx.next()?;
            if let Some(packet) = EthernetPacket::new(frame) {
                ethernet_frame_handler(&packet);
            }
        }
    }

    Ok(())
}

// イーサネットフレームを処理する関数
fn ethernet_frame_handler(packet: &EthernetPacket) {
  //　イーサタイプにより上にのるプロトコルの種類を確認
  match packet.get_ethertype() {
    EtherTypes::Ipv4 => {
      if let Some(ipv4) = Ipv4Packet::new(packet.payload()) {
        // TCP か UDP か ICMP のときのみ処理
        match ipv4.get_next_level_protocol() {
          IpNextHeaderProtocols::Tcp|IpNextHeaderProtocols::Udp => {
            tcp_udp_handler(ipv4);
          },
          IpNextHeaderProtocols::Icmp => {
            icmp_handler(ipv4);
          },
          _ => {
            if VERBOSE {
                println!("[{}]", ipv4.get_next_level_protocol());
            }
          },
        }
      }
    },
    _ => {
        if VERBOSE {
            println!("[{}]", packet.get_ethertype());
        }
    },
  }
}

// TCP と UDP を処理する関数
fn tcp_udp_handler(ipv4: Ipv4Packet) {
    if let Some(tcp) = TcpPacket::new(ipv4.payload()) {
        print_ip_port("[TCP]", ipv4.get_source(), tcp.get_source(), 
            ipv4.get_destination(), tcp.get_destination(), tcp.get_flags());
    } else if let Some(udp) = UdpPacket::new(ipv4.payload()) {
        print_ip_port("[UDP]", ipv4.get_source(), udp.get_source(), 
            ipv4.get_destination(), udp.get_destination(), 0);
    }
}

// IP と PORT を表示する関数
fn print_ip_port (tag: &str, 
    src_ip:Ipv4Addr, src_port:u16, dst_ip:Ipv4Addr, dst_port:u16, flags:u8) {
    print!("{} {}:{} -> {}:{}", tag, src_ip, src_port, dst_ip, dst_port);
    if flags != 0 {
        print!(" flags:{}", conv_flags(flags));
    }
    println!();
}

// ICMP を処理する関数
fn icmp_handler (ipv4: Ipv4Packet) {
    if let Some(icmp) = IcmpPacket::new(ipv4.payload()) {
        println!("[ICMP] {} -> {} type:{:?}, code:{:?} ", 
            ipv4.get_source(), ipv4.get_destination(),
            icmp.get_icmp_type(), icmp.get_icmp_code()
        );
    }
}

// TCPフラグを文字列化する関数
fn conv_flags (flags: u8) -> String {
    let mut r:Vec<&str> = vec![];
    if flags&TcpFlags::SYN != 0 {
        r.push("SYN");
    }
    if flags&TcpFlags::ACK != 0 {
        r.push("ACK");
    }
    if flags&TcpFlags::RST != 0 {
        r.push("RST");
    }
    if flags&TcpFlags::FIN != 0 {
        r.push("FIN");
    }
    if flags&TcpFlags::PSH != 0 {
        r.push("PSH");
    }
    if flags&TcpFlags::CWR != 0 {
        r.push("CWR");
    }
    if flags&TcpFlags::ECE != 0 {
        r.push("ECE");
    }
    if flags&TcpFlags::URG != 0 {
        r.push("URG");
    }
    r.join(",").to_string()
}

#[derive(thiserror::Error, Debug)]
enum MyError {
    #[error("Unknown Command")]
    UnknownCommandError,

    #[error("Too Few Argument")]
    TooFewArgumentError,

    #[error("Unknown Interface")]
    UnknownInterfaceError,

    #[error("IOError({0})")]
    IOError (#[from] std::io::Error),
}
```

# 手順等

- NPCAP をダウンロードしインストールする
  https://npcap.com/dist/npcap-1.76.exe

- コンパイルに Packet.lib が必要になるので SDK をダウンロードし、
  https://npcap.com/dist/npcap-sdk-1.13.zip
この中の "Lib" を適切なフォルダ配下（例：C:\work\pnet）にコピーし、次のように環境変数 LIB を設定する
set LIB=C:\work\pnet\Lib\x64

- プロジェクトの作成
  cargo new sniff
- cd sniff して src/main.rs の内容を上のコードで上書き
- クレイトの設定
  cargo add pnet thiserror ipnetwork
- コンパイル
  cargo build

# 実行例

* 引数なしで実行

```
c:\work>sniff.exe
Usage: sniff.exe args
        interfaces ... display interfaces
        sniff interface ... sniff at interface

```

* インターフェースの表示

```
c:\work>sniff.exe interfaces
Interfaces:
\Device\NPF_{8E67E254-B03D-4CF0-B734-2B940458YYYY}
        description: Qualcomm ABCDE 802.11ac Wireless Adapter
        mac: 64:6c:XX:XX:XX:XX
        ips: 192.168.111.7/24
\Device\NPF_{E26BF5DC-AFE0-48DC-9DE6-2552A068ZZZZ}
        description: VirtualBox Host-Only Ethernet Adapter
        mac: 0a:00:XX:XX:XX:XX
        ips: 192.168.56.111/24
…
```

* スニフィングする際には使用するインターフェース名を指定する。

```
c:\work>sniff.exe sniff \Device\NPF_{8E67E254-B03D-4CF0-B734-2B940458YYYY}
Inteface: \Device\NPF_{8E67E254-B03D-4CF0-B734-2B940458YYYY}
[TCP] 64.38.119.27:443 -> 192.168.111.7:56642 flags:ACK
[TCP] 64.38.119.27:443 -> 192.168.111.7:56642 flags:ACK,FIN
[TCP] 192.168.111.7:56642 -> 64.38.119.27:443 flags:ACK
[TCP] 192.168.111.7:56289 -> 107.178.254.65:443 flags:ACK
[TCP] 107.178.254.65:443 -> 192.168.111.7:56289 flags:ACK
[TCP] 192.168.111.7:55240 -> 192.168.111.1:53 flags:FIN
[ICMP] 192.168.111.7 -> 183.181.99.39 type:IcmpType(8), code:IcmpCode(0)
[ICMP] 183.181.99.39 -> 192.168.111.7 type:IcmpType(0), code:IcmpCode(0)
[TCP] 192.168.111.1:53 -> 192.168.111.7:55240 flags:FIN
[TCP] 192.168.111.7:56477 -> 23.106.127.165:443 flags:ACK,FIN
^C ← Ctrl-C で終了
```

# 改造メモ

* TCP / UDP のパケットの処理を変えたいときは tcp_udp_handler 関数を改造する。
* ICMP の処理を変えたいときは icmp_handler 関数を改造する。
* ARP とか IPv6 に対応したい場合は ethernet_frame_handler 関数の match 文を改造する。

以上。