この記事は
- libpnetのサンプルコードにあるARPリクエストを使ってみる
- サンプルコードの中身を読み解く(自分の勉強のため)
libpnetのARPサンプルコードを実行する
まずは環境づくりから。
-
cargo new
で作ったまっさらな環境に上記サンプルコードをコピーして、main.rs
をsrc
フォルダに入れる -
Cargo.toml
ファイルのdependencies
に1行追加する -
Packet.lib
をプロジェクトフォルダ直下に置く
[dependencies]
pnet = "0.33.0"
Packet.lib
はWinPcapから取得する
\WpdPack_4_1_2\WpdPack\Lib\Packet.lib
をプロジェクトフォルダにコピーする
※4_1_2
の数字は'23年6月時点
実行結果
引数にネットワークアダプタとIPアドレスを与える
launch.json
だとこんな感じ
{
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
...
"args": ["\\Device\\NPF_{????????-????-????-????-????????}", "192.168.10.1"],
...
},
ネットワークアダプタはLinuxであればeth0
といったもの。
Windowsの場合は少しややこしく、Device\\NPF...
形式の長ったらしい文字列となる。
(詳しい内容は付録に記載する)
試しにWindowsマシンでデフォルトゲートウェイのIPアドレスを調べた結果が下記
Sent ARP request 192.168.10.1
Received reply
Target MAC address: **:**:**:**:**:**
3行目でターゲットのMACアドレスが表示された
※オリジナルとの変更点:ログにIPアドレス追記/MACアドレスは伏字
次章からソースコードを読み解いていく
ARP要求パケットの中身
WireSharkで実際にキャプチャした結果が下図
全部で42バイトある(あとでバッファを定義するために、この42を使う)
赤枠で区切った単位で意味をもっている
内容 | 説明 |
---|---|
ff*6 | 相手のMACアドレス(ARPリクエストはブロードキャスト) |
黒塗り | 自分のMACアドレス |
08 06 | ARPは0806 (TCP/IPは0800 ) |
00 01 | ハードウェアタイプ。イーサネットは1
|
08 00 | プロトコルタイプ。IPから調べるので0800 |
06 | ハードウェアアドレス長。MACアドレスは6バイト |
04 | プロトコルアドレス長。IPアドレスは4バイト |
00 01 | ARPリクエスト要求は1
|
黒塗り | 送信元MACアドレス |
c0 a8 0a 65 | 送信元IPアドレス 192.168.10.101 |
00*6 | 送信先MACアドレス |
c0 a8 0a 71 | 送信先IPアドレス |
参考にしたサイトは巻末の"参考"に記載
ARP要求をするには、表にある情報を手元に集めればよい
get_mac_through_arp
関数を紐解いていく
送信元IPアドレスsource_ip
引数で渡したinterface: NetworkInterface
をもとに、IPv4アドレスを探している
let source_ip = interface
.ips
.iter()
.find(|ip| ip.is_ipv4())
.map(|ip| match ip.ip() {
IpAddr::V4(ip) => ip,
_ => unreachable!(),
})
.unwrap();
データリンクレイヤの送受信窓口
- 送信用
sender
- 受信用
receiver
let (mut sender, mut receiver) = match pnet::datalink::channel(&interface, Default::default()) {
Ok(Channel::Ethernet(tx, rx)) => (tx, rx),
Ok(_) => panic!("Unknown channel type"),
Err(e) => panic!("Error happened {}", e),
};
Ethernetパケット
表の上3行分のところに該当する
内容 | 説明 |
---|---|
ff*6 | 相手のMACアドレス(ARPリクエストはブロードキャスト) |
黒塗り | 自分のMACアドレス |
08 06 | ARPは0806 (TCP/IPは0800 ) |
バッファを42バイトとしているのは、ここで使う14バイトにペイロード分の28バイトを加えるため。
最終的に送り出すパケットの長さまで考慮して、ここであらかじめ42バイト確保している
ゼロで初期化しているが、ちゃんとしたデータで上書きするためにmut
キーワードがついている
let mut ethernet_buffer = [0u8; 42];
let mut ethernet_packet = MutableEthernetPacket::new(&mut ethernet_buffer).unwrap();
ethernet_packet.set_destination(MacAddr::broadcast());
ethernet_packet.set_source(interface.mac.unwrap());
ethernet_packet.set_ethertype(EtherTypes::Arp);
ethernet.rx
の中をみると、ちゃんとイーサタイプは0806
で定義されている
pub mod EtherTypes {
use crate::ethernet::EtherType;
/// Internet Protocol version 4 (IPv4) \[RFC7042\].
pub const Ipv4: EtherType = EtherType(0x0800);
/// Address Resolution Protocol (ARP) \[RFC7042\].
pub const Arp: EtherType = EtherType(0x0806);
...(略)
}
ARPパケット
表の残り後半部分を一気にset
している
表とコードのどちらとも9行で合致していることがわかる
内容 | 説明 |
---|---|
00 01 | ハードウェアタイプ。イーサネットは1
|
08 00 | プロトコルタイプ。IPから調べるので0800 |
06 | ハードウェアアドレス長。MACアドレスは6バイト |
04 | プロトコルアドレス長。IPアドレスは4バイト |
00 01 | ARPリクエスト要求は1
|
黒塗り | 送信元MACアドレス |
c0 a8 0a 65 | 送信元IPアドレス 192.168.10.101 |
00*6 | 送信先MACアドレス |
c0 a8 0a 71 | 送信先IPアドレス |
表のバイト数をすべて足すと28バイトになるので、バッファを28としている
let mut arp_buffer = [0u8; 28];
let mut arp_packet = MutableArpPacket::new(&mut arp_buffer).unwrap();
arp_packet.set_hardware_type(ArpHardwareTypes::Ethernet);
arp_packet.set_protocol_type(EtherTypes::Ipv4);
arp_packet.set_hw_addr_len(6);
arp_packet.set_proto_addr_len(4);
arp_packet.set_operation(ArpOperations::Request);
arp_packet.set_sender_hw_addr(interface.mac.unwrap());
arp_packet.set_sender_proto_addr(source_ip);
arp_packet.set_target_hw_addr(MacAddr::zero());
arp_packet.set_target_proto_addr(target_ip);
要求するときのターゲットMACアドレスは、現時点では知りえない(知るためにARP要求を送る)ので、ゼロで埋めてある
ふたたびEthernetパケット
先ほどは宛先/送信元MACアドレスと、タイプだけセットして止まっていた
そのパケットのペイロードにARPパケット部分を埋め込む
ethernet_packet.set_payload(arp_packet.packet_mut());
パケットを送信する
ペイロードに情報を埋め込んだethernet_packet
を送信する
packet()
関数によってバイト型の配列を取得しているっぽい
sender
.send_to(ethernet_packet.packet(), None)
.unwrap()
.unwrap();
libs.rs
のsend_to
関数の説明によると、第二引数は無視されるのでNone
を入れておけばよいとのこと
/// This may require an additional copy compared to
build_and_send
, depending on the
/// operating system being used. The second parameter is currently ignored, however
///None
should be passed.
応答を受信する
next()
関数で、チャネルに届いているイーサネットフレームを取り出す
取り出したバイト配列のうち、最初のminimum_packet_size
分だけを省いた箇所をARPパケットとしてさらに取り出す
送り元が要求したIPアドレスであり、かつ自分のMACアドレス宛てに届いていたら
それを真正なパケットとみなして、送り元のMACアドレス(=知りたかった情報)を表示する
MACアドレス不明だった端末のIPアドレスから応答をもらえた、ということになる
while let buf = receiver.next().unwrap() {
let arp = ArpPacket::new(&buf[MutableEthernetPacket::minimum_packet_size()..]).unwrap();
if arp.get_sender_proto_addr() == target_ip
&& arp.get_target_hw_addr() == interface.mac.unwrap()
{
println!("Received reply");
return arp.get_sender_hw_addr();
}
}
minimum_packet_size
について
minimum_packet_size
は14
らしい
println!("{}", MutableEthernetPacket::minimum_packet_size());
// 出力: 14
MACアドレス6バイト×2と、タイプ2バイト分を足した分に等しい
つまりペイロードの部分とイコールとなる
内容 | 説明 |
---|---|
ff*6 | 相手のMACアドレス(ARPリクエストはブロードキャスト) |
黒塗り | 自分のMACアドレス |
08 06 | ARPは0806 (TCP/IPは0800 ) |
おわりに
Rustでネットワークプログラミングをする上で欠かせないライブラリlibpnetの
サンプルコードにあるARPリクエストを紐解いてみた
参考にリンクを貼っている仕様と比べながら読むと、当たり前ながら、1つ1つ組み立てられているのがわかる
そしてMACアドレスを詐称してARP要求に答えることがいかに容易いかも想像できる
参考
付録:Windowsのインターフェースを調べる方法
上記コードをcargo new
したプロジェクトのdependencies
にpnet_datalink
を追記する
[dependencies]
pnet_datalink = "0.33.0"
Device\NPF
がずらっと5個ぐらい表示された
\Device\NPF_{~~~~~~~~~~~~~~~~~~~~~~~~~~~~}: flags=0
index: 53
ether: ~~~~~~~~
inet: 172.17.80.1/20
...
...
ほかの方法としては、下記記事コードの冒頭部分を実行する
デフォルトで使っているインターフェースが1個だけ表示される