パケット・スニフィングの Rust での実装について調べましたのでメモを残します。
普通だったら tcpdump とか wireshark を使いそうなところですがあえて Rust でやりました。
Rust 修行中ですので。
本稿では Windows10 での実行を前提にしております。
実装例
以下、実装例を示します。
pnet クレイトを使用しました。
さほど難しいコードでもないので解説なしです。
工夫点というほどでもありませんが、
- windows でのインターフェース名は linux よりも面倒なのでインターフェース名を表示するコマンドラインオプションを用意。
- なるべく不格好な unwrap の使用を回避し、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 文を改造する。
以上。