RustでARPリクエストを送る

## この記事は

- [libpnet](https://github.com/libpnet/libpnet)のサンプルコードにあるARPリクエストを使ってみる
- サンプルコードの中身を読み解く（自分の勉強のため）

## libpnetのARPサンプルコードを実行する

まずは環境づくりから。

https://github.com/libpnet/libpnet/blob/master/examples/arp_packet.rs

- `cargo new`で作ったまっさらな環境に上記サンプルコードをコピーして、`main.rs`を`src`フォルダに入れる
- `Cargo.toml`ファイルの`dependencies`に１行追加する
- `Packet.lib`をプロジェクトフォルダ直下に置く

```toml
[dependencies]
pnet = "0.33.0"
```

### `Packet.lib`はWinPcapから取得する

https://www.winpcap.org/install/default.htm

`\WpdPack_4_1_2\WpdPack\Lib\Packet.lib`をプロジェクトフォルダにコピーする

※`4_1_2`の数字は'23年6月時点

### 実行結果

引数にネットワークアダプタとIPアドレスを与える
`launch.json`だとこんな感じ

```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で実際にキャプチャした結果が下図

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272511/da197ca8-8f1e-662d-80d9-97671de87475.png)

全部で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アドレスを探している

```rust
    let source_ip = interface
        .ips
        .iter()
        .find(|ip| ip.is_ipv4())
        .map(|ip| match ip.ip() {
            IpAddr::V4(ip) => ip,
            _ => unreachable!(),
        })
        .unwrap();
```

### データリンクレイヤの送受信窓口

- 送信用`sender`
- 受信用`receiver`

```rust
    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パケット 

表の上３行分のところに該当する

内容|説明
-|-
ff*6|相手のMACアドレス（ARPリクエストはブロードキャスト）
黒塗り|自分のMACアドレス
08 06|ARPは`0806`　（TCP/IPは`0800`）

バッファを42バイトとしているのは、ここで使う14バイトにペイロード分の28バイトを加えるため。
最終的に送り出すパケットの長さまで考慮して、ここであらかじめ42バイト確保している
ゼロで初期化しているが、ちゃんとしたデータで上書きするために`mut`キーワードがついている

```rust
    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`で定義されている

```rust
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`している
表とコードのどちらとも９行で合致していることがわかる

内容|説明
-|-
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としている

```rust
    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パケット部分を埋め込む

```rust
    ethernet_packet.set_payload(arp_packet.packet_mut());
```

### パケットを送信する

ペイロードに情報を埋め込んだ`ethernet_packet`を送信する
`packet()`関数によってバイト型の配列を取得しているっぽい

```rust
    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アドレスから応答をもらえた、ということになる

```rust
    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`らしい

```rust
println!("{}", MutableEthernetPacket::minimum_packet_size());

// 出力: 14
```

MACアドレス６バイト×２と、タイプ２バイト分を足した分に等しい
つまりペイロードの部分とイコールとなる

内容|説明
-|-
ff*6|相手のMACアドレス（ARPリクエストはブロードキャスト）
黒塗り|自分のMACアドレス
08 06|ARPは`0806`　（TCP/IPは`0800`）

## おわりに

Rustでネットワークプログラミングをする上で欠かせないライブラリ[libpnet](https://github.com/libpnet/libpnet)の
サンプルコードにあるARPリクエストを紐解いてみた

参考にリンクを貼っている仕様と比べながら読むと、当たり前ながら、１つ１つ組み立てられているのがわかる

そしてMACアドレスを詐称してARP要求に答えることがいかに容易いかも想像できる

## 参考

- [フレーム形式](https://beginners-network.com/frame_type.html)
- [ARPパケットフォーマット](https://beginners-network.com/supplement/arp_packet_format.html)
- [IEEE 802 Numbers](https://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml#ieee-802-numbers-1)
- [Address Resolution Protocol Parameters](https://www.iana.org/assignments/arp-parameters/arp-parameters.xhtml)

https://github.com/libpnet/libpnet/blob/master/examples/arp_packet.rs

## 付録：Windowsのインターフェースを調べる方法

https://github.com/libpnet/libpnet/blob/master/examples/list_interfaces.rs

上記コードを`cargo new`したプロジェクトの`dependencies`に`pnet_datalink`を追記する

```toml
[dependencies]
pnet_datalink = "0.33.0"
```

`Device\NPF`がずらっと５個ぐらい表示された

```
\Device\NPF_{~~~~~~~~~~~~~~~~~~~~~~~~~~~~}: flags=0
      index: 53
      ether: ~~~~~~~~
       inet: 172.17.80.1/20
...
...
```

ほかの方法としては、下記記事コードの冒頭部分を実行する
デフォルトで使っているインターフェースが１個だけ表示される

https://qiita.com/m10i/items/f8d3db359f150aafc83b#%E3%83%8F%E3%83%9E%E3%82%8A%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88

## 糸冬了！！