🦀
Rust で ICMP パケットをキャプチャする
概要
ネットワークの勉強のために ICMP パケットをキャプチャするプログラムを作成しました。
作成したプログラムについて、説明を記載します。
作成したプログラムは以下のリポジトリにあります。
依存関係
今回作成したプログラムでは、以下の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),
}
}
}
上記の関数は、以下の処理を行っています。
- コマンドライン引数をパースする
- ネットワークインタフェースを取得する
- チャネルを開く
- パケットを受信し、表示する
それぞれの処理について、説明を記載していきます。
コマンドライン引数をパースする
コマンドライン引数のパースには 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