WireGuardでAllowedIPsに0.0.0.0/0を指定するとパケットが全てVPNインターフェイスに吸い込まれてしまう件

WireGuardという素晴らしいVPNソフトウェアがあります。シンプルなのでほとんどハマりどころはないのですが、自宅で試していたところ少し意図しない挙動を見つけたのでその理由と対処方法をまとめます。 WireGuard: fast, modern, secure VPN tunnel

何が素晴らしいのか、詳細が気になる方はこちらのスライドをご覧になるのがいいと思います。 [CB16] WireGuard:次世代耐乱用性カーネルネットワークトンネル by Jason Donenfeld

AllowedIPsに0.0.0.0/0を指定するとパケットが全てVPNインターフェイスに吸い込まれる

AllowedIPsに0.0.0.0/0を指定するのはどんなときかと言うと、VPNインターフェイス経由で送信するパケットの宛先をすべて許可したい時です。例えば、カフェのFree Wi-Fiが信用できない時にすべてのトラフィックVPNサーバ経由にしたい時はVPN経由のパケットの宛先IPは何が来るかわからないのですべての宛先のパケットを許可するために0.0.0.0/0を指定しますね。

もしくは、少数派な使い方かもしれませんが、L3 VPNなのでstatic routingをしたり、VPNの両端のインターフェイスでルーティングプロトコル(ospf, bgpなど)を喋るデーモンを起動して動的に経路情報を交換するようにするかもしれません。ルーティングテーブルで宛先を制御する場合も意図せずパケットがフィルタされないようにするためにAllowedIPsは0.0.0.0/0にすると思います。

このような意図で0.0.0.0/0を指定する場合、実際に0.0.0.0/0にマッチするパケット、つまり全てのパケット(後述しますが厳密には全てではありません)がWireGuardのインターフェイスを経由してほしいわけではありません。が、 wg-quick コマンドを使ってWireGuardのインターフェイスを作成すると全てのパケットがWireGuardのインターフェイスに吸い込まれてしまう現象が起きます。

現象の確認

OSはUbuntu18.04です。

ubuntu@c01:~$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
24: eth0@if25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 00:16:3e:a3:9e:eb brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.59.79.145/24 brd 10.59.79.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::216:3eff:fea3:9eeb/64 scope link
       valid_lft forever preferred_lft forever

NICは1つだけあります。

WireGuardの設定ファイルを作ってみました。wg0.confの中身はこんな感じです。

ubuntu@c01:~$ cat /etc/wireguard/wg0.conf
[Interface]
Address = 10.0.0.1/24
SaveConfig = true
ListenPort = 51820
PrivateKey = GIWLRkDQXS/wym2pCWW2VDKTBpUp2a60AB+KSG18SEE=

[Peer]
PublicKey = uKjZpzBmrsp7YGqhpTLajsk2XiKj0HHYoRMkLVOaE2c=
AllowedIPs = 0.0.0.0/0
Endpoint = 192.168.100.61:51820

192.168.100.61:51820 というのがVPNの接続先だと仮定します。(適当なIPなので実際には対向は存在しませんが今回は問題ではないのでこのまま続けます)

WireGuardを起動してみる

wg-quick コマンドを使って起動してみます。

ubuntu@c01:~$ sudo wg-quick up wg0.conf
[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip address add 10.0.0.1/24 dev wg0
[#] ip link set mtu 1420 dev wg0
[#] ip link set wg0 up
[#] wg set wg0 fwmark 51820
[#] ip -4 route add 0.0.0.0/0 dev wg0 table 51820
[#] ip -4 rule add not fwmark 51820 table 51820
[#] ip -4 rule add table main suppress_prefixlength 0

ubuntu@c01:~$ sudo wg
interface: wg0
  public key: M91GRcFHFdXtRL4UXh9d18sv1w9/KYbibOYcGyLzAwk=
  private key: (hidden)
  listening port: 51820
  fwmark: 0xca6c

peer: uKjZpzBmrsp7YGqhpTLajsk2XiKj0HHYoRMkLVOaE2c=
  endpoint: 192.168.100.61:51820
  allowed ips: 0.0.0.0/0

ubuntu@c01:~$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
3: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    link/none
    inet 10.0.0.1/24 scope global wg0
       valid_lft forever preferred_lft forever
24: eth0@if25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 00:16:3e:a3:9e:eb brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.59.79.145/24 brd 10.59.79.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::216:3eff:fea3:9eeb/64 scope link
       valid_lft forever preferred_lft forever

wg0 インターフェイスが追加されましたね。

インターネットに向けてpingを打ってみる

8.8.8.8に向けてpingを打ってみました。

ubuntu@c01:~$ ping -c 3 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.

--- 8.8.8.8 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2055ms

…あれ?パケットが全部落ちていますね。ちなみに同時にtcpdumpをしてみたのですが、

ubuntu@c01:~$ sudo tcpdump -n icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on wg0, link-type RAW (Raw IP), capture size 262144 bytes
12:52:34.626312 IP 10.0.0.1 > 8.8.8.8: ICMP echo request, id 1667, seq 3, length 64
12:52:35.650419 IP 10.0.0.1 > 8.8.8.8: ICMP echo request, id 1667, seq 4, length 64
12:52:36.674427 IP 10.0.0.1 > 8.8.8.8: ICMP echo request, id 1667, seq 5, length 64

wg0 インターフェイスのIPからインターネットに出ようとしていますね。これは変ですね。

WireGuardを落とすとどうなるでしょう?

ubuntu@c01:~$ sudo wg-quick down wg0.conf
[#] wg showconf wg0
[#] ip -4 rule delete table 51820
[#] ip -4 rule delete table main suppress_prefixlength 0
[#] ip link delete dev wg0

ubuntu@c01:~$ ping -c 3 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=118 time=3.37 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=118 time=3.35 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=118 time=3.19 ms

--- 8.8.8.8 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 3.196/3.309/3.378/0.093 ms

パケットは普通にインターネットに届いているようです。これが今回困った事象です。

原因

WireGuardを起動した状態でルーティングテーブルを確認してみます。一見問題がないように見えますね。

ubuntu@c01:~$ ip route
default via 10.59.79.1 dev eth0
10.0.0.0/24 dev wg0  proto kernel  scope link  src 10.0.0.1
10.59.79.0/24 dev eth0  proto kernel  scope link  src 10.59.79.145

WireGuardの起動時の出力を見ると気になるログが2箇所あります。

[#] ip -4 route add 0.0.0.0/0 dev wg0 table 51820
[#] ip -4 rule add not fwmark 51820 table 51820

なにやら怪しいですね。

ubuntu@c01:~$ ip rule
0:      from all lookup local
32764:  from all lookup main suppress_prefixlength 0
32765:  not from all fwmark 0xca6c lookup 51820
32766:  from all lookup main
32767:  from all lookup default

ubuntu@c01:~$ ip route show table 51820
default dev wg0  scope link

fwmark 51820 がパケットにセットされていない場合はルーティングテーブル 51820 を参照するルールで、そのルーティングテーブルはデフォルトルートとして wg0 インターフェイスが設定されています。つまり、何もパケットがマークされていない全てのパケットは wg0 からインターネットに出ていこうとします。これは困りますね。逆にどういう時に fwmark 51820 が付いているのでしょうか?会社の同期がそれっぽいものを見つけてきてくれました。

f:id:kuro_m88:20181104213916p:plain

Routing & Network Namespaces - WireGuard

Improved Rule-based Routing の段落をご覧ください。

we are able to set an fwmark on all packets going out of WireGuard's UDP socket

とあります。WireGuardによって暗号化されたパケットに fwmark をつけてくれるようです。全てのパケットをVPN経由にしたい場合、0.0.0.0/0の宛先をWireGuard経由にしたくなりますが、そうするとVPN自体の暗号化されたパケットもVPNインターフェイス(wg0)にルーティングされてしまいループしてしまって外に出られなくなりますが、 fwmark がついているパケットだけホストに元からあるルーティングテーブルを参照させ、それ以外のパケットは全てVPNインターフェイスにルーティングされるようにしてくれるようです。冒頭で例として挙げた、「カフェのFree Wi-Fiが信用できない時にすべてのトラフィックVPNサーバ経由にしたい時」などにはありがたい挙動ですが、後者の「static routingをしたり、VPNの両端のインターフェイスでルーティングプロトコル(ospf, bgpなど)を喋るデーモンを起動して動的に経路情報を交換する」場合には全てのパケットがVPNインターフェイスに吸い込まれてはこまりますね。

対処方法

原因はわかりました。デフォルトルートを wg0 向ける設定追加がされないようにすればいいだけですね。残念ながら、 wg-quick コマンド経由でこの設定が自動で投入されないようにする方法は見当たりませんでしたのでかっこ悪いですが以下のような設定をしました。

[Interface]
Address = 10.0.0.1/24
SaveConfig = true
PostUp = ip -4 route del 0.0.0.0/0 dev %i table $(wg show %i listen-port)
ListenPort = 51820
FwMark = 0xca6c
PrivateKey = aIG1izlztM2ISSsaLPmQpamqc0FdFnUoYTpeeYNWQm0=

[Peer]
PublicKey = uKjZpzBmrsp7YGqhpTLajsk2XiKj0HHYoRMkLVOaE2c=
AllowedIPs = 0.0.0.0/0
Endpoint = 192.168.100.61:51820

追加したのは PostUp = ip -4 route del 0.0.0.0/0 dev %i table $(wg show %i listen-port) の一行です。 %i というのは実行時にインターフェイス名に置き換えられます。追加された設定を直後に消すということですね(笑)

これで思ったような挙動を得ることができました。よかった。