Linux ARP応答のコーナーケースを攻める

## はじめに

最近、Linux でサブネットマスクの typo をしたときの L2 (ARP 応答) がどう動くのかって話を見ておりまして。気になったのでいくつか変なケースでどう動くのかを試してみました。

## 要約

同じ L2 セグメントに同じサブネットのつもりなんだけど実はサブネットマスクを書き間違えました…みたいなノードのがいると、一部通信できるけど一部は通信できない、みたい動きをすることがあります。全部ダメとかではなくて一部ダメみたいな動きはたいへん見つけにくいので要注意です。サブネットマスクはちゃんと設定しましょう。

- ARP Request にはサブネットマスクの情報が入っていません。
- Linux は ARP Reply する前に、ルーティングテーブルをチェックしてから応答するかどうかを決めています。
- サブネットマスクとデフォルトゲートウェイの設定によっては、 L2 セグメントの中で非対称な通信が発生します。

## 検証環境

Mininet を使います。自前でインストールするのが面倒なので Dockerhub で使えそうなものを探しました。それなりに最近のバージョンが動くもので、余計なものの入ってなさそうなもの…ということで [orzohmygodorz/mininet](https://hub.docker.com/r/orzohmygodorz/mininet) ([latest](https://hub.docker.com/layers/orzohmygodorz/mininet/latest/images/sha256-1243aa8d3bd22469dbd140ffbe1d3ae0ab2f9d151a8ea52eae34040f81cb7348?context=explore)) を使っています。が、これシンプルなのはいいのですがシンプルすぎて ping がはいっていないというオチでした。起動したら ping を入れてください (`apt install iputils-ping`)。あと X 関連ツールも入ってないので Mininet の `xterm` コマンドは動きませんがこの後紹介する Mininet 用スクリプトでどうにかなるのでこれはパスで。

docker-compose で起動する場合はこんな感じで。 `scripts` ディレクトリにこの後出すスクリプトを入れています。

```yaml:docker-compose.yml
version: "2"
services:
  lab:
    image: orzohmygodorz/mininet:latest
    hostname: l2test
    volumes:
      - ./scripts:/scripts
      - /lib/modules:/lib/modules
    privileged: true
    environment:
      TERM: screen-256color
      TZ: Asia/Tokyo
    network_mode: host
```

## 検証用NW

コンテナを立てたらこのスクリプトを実行します。(`scripts` ディレクトリに入れておいてコンテナにマウントする。) [スクリプト全文は gist にあります](https://gist.github.com/corestate55/7427ed025de2582726875d66a3e0b71b)。

```python:l2nw.py
# 省略

def run():
    host_config = {
        "rt": {"type": "router", "ip": "192.168.0.254/24", "routes": []},
        "h0": {"type": "host", "ip": "192.168.0.1/24", "routes": []},
        "h1": {"type": "host", "ip": "192.168.0.65/25", "routes": []},
        "h2": {"type": "host", "ip": "192.168.0.129/25", "routes": []},
        "h3": {
            "type": "host",
            "ip": "192.168.0.130/25",
            "routes": [{"dnw": "default", "gw": "192.168.0.254"}],
        },
        "h4": {
            "type": "host",
            "ip": "192.168.0.193/26",
            "routes": [{"dnw": "192.168.0.0/26", "gw": "192.168.0.254"}],
        },
        "h5": {
            "type": "host",
            "ip": "192.168.0.194/26",
            "routes": [{"dnw": "192.168.0.128/26", "gw": "192.168.0.254"}],
        },
    }

    topo = NetworkTopo(host_config=host_config)
    net = Mininet(topo=topo, controller=None)
    net.start()
    config_hosts(net, host_config)
    CLI(net)
    net.stop()


if __name__ == "__main__":
    setLogLevel("info")
    run()
```

これを動かすと、下図のようなネットワークが作られた状態で Mininet が起動します。

![Network Diagram](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154230/c2595e3c-6114-e7dc-2093-199b5e694d9c.png)

Mininet の `net` コマンドでトポロジを確認。

```text
mininet> net
h0 h0-eth0:s1-eth0
h1 h1-eth0:s1-eth1
h2 h2-eth0:s1-eth2
h3 h3-eth0:s1-eth3
h4 h4-eth0:s1-eth4
h5 h5-eth0:s1-eth5
rt rt-eth0:s1-eth6
s1 lo:  s1-eth0:h0-eth0 s1-eth1:h1-eth0 s1-eth2:h2-eth0 s1-eth3:h3-eth0 s1-eth4:h4-eth0 s1-eth5:
h5-eth0 s1-eth6:rt-eth0
```

スクリプト中の `host_config` にルータ (rt) およびホスト 0-5 (h0-h5) の設定が入っています。各ノードは [MininetでL2/L3ネットワークを作るときのTips](https://qiita.com/corestate55/items/353af3aaff53bbb7980a) で紹介した、iproute2 で netns 操作可能なノードとして作っているので、Mininet の外からも操作可能です。(`xterm` コマンドが使えなくても複数ノードを同時に操作できる。)

```text
root@l2test:/# ip netns list
h5 (id: 5)
rt (id: 6)
h0 (id: 0)
h1 (id: 1)
h2 (id: 2)
h3 (id: 3)
h4 (id: 4)
```

この後の動作確認用にいくつかホストをまとめて作っていますが、少しずつ順を追って動作を確認していきましょう。

## サブネットマスクを間違えているノード間通信

通信確認の起点を h0 とします。このスイッチにつながるノードは本来 192.168.0.0/24 のサブネットになるのが正です。h1 - h5 は、ついうっかりサブネットマスク指定を間違えてしまったケースと考えてください。

### Case.1: サブネット範囲がかぶる場合

まず h0 と h1 から見ていきます。h1 はいかにも "サブネットマスクの入力ミス" です。ping が通るかどうかを確認します。

ping

|from\to|h0|h1|
|-------|--|--|
|h0     |--|ok|
|h1     |ok|--|

h1 は h0 と異なるサブネットが設定されていますが、通信できました。

ARP についても確認してみますが特に問題なく解決できています。

```text
mininet> h1 arp
Address                  HWtype  HWaddress           Flags Mask            Iface
192.168.0.1              ether   5e:a2:da:b5:e9:01   C                     h1-eth0
```

[ARP Request のパケット構造](https://ja.wikipedia.org/wiki/Address_Resolution_Protocol)を見るとわかるのですが、ARP Request に含まれる L3 情報: 送信元 (SPA)・送信先 (TPA) は、サブネットマスクの情報を持ちません。h1 は自分のサブネットが 192.168.0.0/25 だと知っていますが、このサブネットには h0 のアドレスも含まれています。そのため、ARP 要求・応答の範囲内ではそれぞれ同一のサブネットにいるように見えています。

### Case.2: サブネット範囲がかぶらない場合

Case.1 は、サブネットマスクを間違えていましたが、運良く送受信ノードが同じ IP アドレス範囲にいました。なので (2) では、サブネットマスクを間違えた結果、異なる IP アドレス範囲にいるノードとの通信を見てみます。

まず h0 と h2 についてです。h2 はマスク指定を間違えた結果、192.168.0.128/25 のブロックにいます。このブロックの範囲には h0 は含まれません。

ping

|from\to|h0|h2|
|-------|--|--|
|h0     |--|Destination Host Unreachable|
|h2     |Network is unreachable|--|

いずれも ping は通りませんでしたが動きがちょっと違います。ARP について確認してみます。

```text
mininet> h0 arp | grep 192.168.0.129
192.168.0.129                    (incomplete)                              h0-eth0
mininet> h2 arp | grep 192.168.0.1
mininet> 
```

h0 からみると、h2 は同じサブネットにいるように見えるので、ARP Request を送信していますが、応答がない状態です。そのため ARP table は incomplete になっています。h2 から見ると h0 は異なるサブネットにいるノードです。そのため、最初から ARP Request を送信していません。

### Case.3: サブネット範囲がかぶらない+デフォルトゲートウェイ設定がある場合

問題はここから。

Case.2 と同じように h0 から h3 の通信確認をしてみます。Case.3 との違いは、h3 がデフォルトゲートウェイを持っているところです。

ping

|from\to|h0|h3|
|-------|--|--|
|h0     |--|ok|
|h3     |ok(Redirect Host)|--|

Case.2 で通信できませんでしたが、**デフォルトゲートウェイを足したら通信できるようになりました。**

Q1. そもそもどのような通信が発生していますか?　h3 から h0 への ping に現れる "Redirect Host" とは何でしょうか?

```text
mininet> h3 ping -c5 h0
PING 192.168.0.1 (192.168.0.1): 56 data bytes
92 bytes from 192.168.0.254: Redirect Host
64 bytes from 192.168.0.1: icmp_seq=0 ttl=64 time=0.510 ms
92 bytes from 192.168.0.254: Redirect Host
64 bytes from 192.168.0.1: icmp_seq=1 ttl=64 time=0.183 ms
92 bytes from 192.168.0.254: Redirect Host
--- 192.168.0.1 ping statistics ---
3 packets transmitted, 2 packets received, 33% packet loss
round-trip min/avg/max/stddev = 0.183/0.347/0.510/0.164 ms
```

Q2. Case.2/h2 (デフォルトルートなし) では、異なるサブネットにいる h0 に対して ARP 要求を出していませんでした。なぜ Case.3/h3 は ARP 要求に応答して ping できるようになったのでしょうか?

```text
mininet> h3 arp
Address                  HWtype  HWaddress           Flags Mask            Iface
192.168.0.254            ether   8e:47:de:e6:80:1d   C                     h3-eth0
192.168.0.1              ether   5e:a2:da:b5:e9:01   C                     h3-eth0
```

## Q1: デフォルトゲートウェイ設定がある場合どう動いているのか?

実際にどう動いているのかを見てみましょう。h0 から h3 に対して ping したときの様子をパケットキャプチャを取って追いかけてみると次の図のようになります。(図中、緑矢印は ARP を, 赤矢印は ICMP(ping) を示します。)

![ARP/ICMP sequence](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154230/61d4c875-e0d8-4335-f250-1d6d81a0325a.png)

* (1) h0 がブロードキャストした ARP Request に h3 が応答しているという点が Q2 です。ひとまず、そう動くというのを受け入れて先に進みます。
* (2) h0 は ARP 応答を受け取って h3 の IP/MAC アドレス解決ができると、ICMP echo request を h3 に送ります。
* (3) h3 は ICMP echo request を樹け取って、echo reply を返そうとします。このとき h3 から見ると h0 は違うサブネットにいるので、いったんデフォルトゲートウェイ (rt) に送ろうとします。rt の MAC アドレスがわからないので ARP で rt の IP/MAC アドレス解決をします。
* (4) h3 は rt の IP/MAC アドレス解決ができると、L2 Destination = rt, L3 Destination = h0 の ICMP echo reply を送信します。
    * ICMP redirect message は適切なパケット転送先を指示するためのメッセージです。
    * ref: [＠IT：Windows TIPS -- Tips：ICMPリダイレクト使用時のファイアウォール設定に注意](https://www.atmarkit.co.jp/fwin2k/win2ktips/613icmpredir/icmpredir.html)
* (5) rt は echo reply を受信しますが、rt から見ると h0 は同じサブネットにいます。そのため ICMP redirect を h3 に送信します。あわせて h0 の IP/MAC 解決をして icmp echo reply を h0 に中継します。

シーケンスをみると、h0-h3, h3-rt, rt-h0 間でそれぞれ ARP 送受信をした後、 h0-h3-rt の三角形のパスで ICMP echo request/reply が流れています。同じ L2 セグメントの中なのに非対称な経路のパケット送受信が発生しています。Case.3 で h3 の ARP table を表示すると、直接 rt との通信を指定していないのに 192.168.0.254 のエントリがある理由はこのような動作によります。

## Q2: なぜデフォルトゲートウェイ設定があるとARP応答するのか?

なぜデフォルトゲートウェイがあると、異なるサブネットに見えるノードからの ARP Request に応答するのでしょうか?

ここから先は Linux のネットワーク処理 (ARP 処理) の実装を追うしかないのですが、Kernel code を読みこなす知識がないのであまり細かく説明できません。ざっくりまとめてしまうと、Linux では ARP 応答するかどうかのロジックにルーティングテーブルのチェックが含まれているためのようです。ARP reply の処理のところを見てみます。

https://github.com/torvalds/linux/blob/master/net/ipv4/arp.c#L815-L863

```c:/net/ipv4/arp.c
	if (arp->ar_op == htons(ARPOP_REQUEST) &&
	    ip_route_input_noref(skb, tip, sip, 0, dev) == 0) {              /* (a) */

		rt = skb_rtable(skb);
		addr_type = rt->rt_type;

		if (addr_type == RTN_LOCAL) {
			int dont_send;

			dont_send = arp_ignore(in_dev, sip, tip);                    /* (b) */
			if (!dont_send && IN_DEV_ARPFILTER(in_dev))
				dont_send = arp_filter(sip, tip, dev);                   /* (c) */
			if (!dont_send) {
				n = neigh_event_ns(&arp_tbl, sha, &sip, dev);
				if (n) {
					arp_send_dst(ARPOP_REPLY, ETH_P_ARP,                 /* (d) */
						     sip, dev, tip, sha,
						     dev->dev_addr, sha,
						     reply_dst);
					neigh_release(n);
				}
			}
			goto out_consume_skb;
		}
        /* 省略 */
	}
```

解説本とあわせて眺めてみます。(ref. [Chapter 7, "Linux Neighbouring Subsystem", Rosen, Rami. Linux Kernel Networking (Expert's Voice in Open Source). Apress, 2014.](https://www.amazon.co.jp/dp/B01HXJH7QA/), )

https://www.amazon.co.jp/dp/B01HXJH7QA/

* (a) `ip_route_input_noref()` でルーティングテーブルのルックアップをして、ARP request がローカルホストに対してのものかどうかを確認している。ローカルホスト宛であれば `RTN_LOCAL` になる。
* (b) `arp_ignore()` はカーネルパラメータとして設定する ARP 挙動設定…ここではデフォルト動作になるので詳細は割愛。
    * ref. [第2章 sysctl およびカーネルの設定項目の使用 Red Hat Enterprise Linux 7 | Red Hat Customer Portal](https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/7/html/kernel_administration_guide/working_with_sysctl_and_kernel_tunables)
* (c) `ip_filter()` のなかで `ip_route_output()` によるルーティングテーブル検索をしています。この検索が失敗した場合、またはルーティングエントリの送信先ノードが ARP Request を受信したノードと異なる場合に `ip_filter()` が失敗 (return 1) する。
* (d) ARP Reply の送信

ルーティングテーブル検索の中身とかを追っていないので、いったんこのへんまで(ほぼ本の受け売り)なんですが、デフォルトルートを足すと動きが変わることについてはこのあたりから理解できそうです。

## 検証

### Case.4: ARP target の経路ルックアップによる動作

さて Q2 の話から、ARP Request を受信して Reply を返すかどうか決めるときに、ルーティングテーブルを検索していることがわかりました。「ARP 要求者のアドレスがルーティングテーブルを検索して解決できるか」に応じて ARP 応答する・しないの動作が変わるようです。なのでこの点も実験してみましょう。

h0, h2 と h4-h5 の通信について確認してみます。

* h0, h2-h3 のサブネット関係と同じ構造が h2-h3, h4-h5 にあります。
* h4-h5 はデフォルトゲートウェイではなく、特定のアドレスブロック宛の static route を持っています。
    * h4: To 192.168.0.0/26 (192.168.0.0/24 を四分割した最初ブロック; h0 を含むが h2-h3 は含まない。)
    * h5: To 192.168.0.128/26 (192.168.0.0/24 を四分割したみっつめのブロック; h0 は含まないが h2-h3 を含む。)

ping

|from\to|h0|h2|h4|h5|
|-------|--|--|--|--|
|h0     |--|Destination Unreachable|**ok**|**Destination Unreachable**|
|h2     |Nework is Unreachable|--|**Destination Unreachable**|**ok**|
|h4     |**ok (Redirect Host)**|**Network is Unreachable**|--|ok|
|h5     |**Network is Unreachable**|**ok (Redirect Host)**|ok|--|

h0-h4/h5, h2-h4/h5 でそれぞれ対象な結果になりました。指定した static route で ARP Target のアドレスが解決できるかどうかに応じて違いが出ていることが見て取れます。

## おまけ: ICMP redirect による一時ルート変更

h0-h3 間の ARP/ICMP シーケンス図あるいは h3 から h0 への ping でもわかりますが、毎回 icmp redirect message が発行されています。ICMP redirect は適切なパケット転送先を指示するもので、受信したノードはこれに基づいて一時的にルーティングテーブルを変更(適切な経路の追加)できます。Linux のデフォルトではこの機能がオフになっています。

初期状態

```text
mininet> h3 ping -c4 h0
PING 192.168.0.1 (192.168.0.1): 56 data bytes
92 bytes from 192.168.0.254: Redirect Host
64 bytes from 192.168.0.1: icmp_seq=0 ttl=64 time=0.510 ms
92 bytes from 192.168.0.254: Redirect Host
64 bytes from 192.168.0.1: icmp_seq=1 ttl=64 time=0.521 ms
--- 192.168.0.1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.510/0.516/0.521/0.000 ms
mininet> h3 ip route
default via 192.168.0.254 dev h3-eth0 
192.168.0.128/25 dev h3-eth0 proto kernel scope link src 192.168.0.130 
mininet> h3 ip route show cache
mininet> 
```

h3 の icmp redirect accept 設定を確認してみます。

```text
mininet> h3 sysctl -a | grep net.ipv4.conf.*.accept_redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 1
net.ipv4.conf.h3-eth0.accept_redirects = 1
net.ipv4.conf.lo.accept_redirects = 1
```

インタフェースごとに ICMP redirect accept 設定ができるのですが、そもそも `all` のところで無効化されています。これを有効化してやります。

```text
mininet> h3 sysctl net.ipv4.conf.all.accept_redirects=1
net.ipv4.conf.all.accept_redirects = 1
mininet> h3 sysctl -a | grep net.ipv4.conf.*.accept_redirects
net.ipv4.conf.all.accept_redirects = 1
net.ipv4.conf.default.accept_redirects = 1
net.ipv4.conf.h4-eth0.accept_redirects = 1
net.ipv4.conf.lo.accept_redirects = 1
```

再度 ping すると redirect は初回だけになりました。このとき route cache が作られていることがわかります。２回目以降の ICMP reply については rt 経由ではなく直接 h0 へ送付されています。

```text
mininet> h3 ping -c4 h0
PING 192.168.0.1 (192.168.0.1): 56 data bytes
92 bytes from 192.168.0.254: Redirect Host
64 bytes from 192.168.0.1: icmp_seq=0 ttl=64 time=0.640 ms
64 bytes from 192.168.0.1: icmp_seq=1 ttl=64 time=0.522 ms
64 bytes from 192.168.0.1: icmp_seq=2 ttl=64 time=0.304 ms
--- 192.168.0.1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.304/0.489/0.640/0.139 ms
mininet> h3 ip route show cache
192.168.0.1 via 192.168.0.1 dev h3-eth0 
    cache <redirected> expires 292sec 
mininet>
```

## まとめ

今回、同一 L2 セグメントに置かれたサブネットの中で、サブネットマスクを間違えて入力するとどういうことが発生するのかを見てみました。h4-h5 は実験用の極端な設定だとして、h1-h3 みたいな状態になるのは手作業だとわりとあり得るケースではないでしょうか。

こうした設定ミスマッチがあると (デフォルトゲートウェイ設定等にもよりますが) 同一セグメント・同一サブネットなのに一部のノードと通信できない、中途半端でわかりにくいトラブルにつながってしまいます。

サブネットマスクはちゃんと設定しましょう!