結局キャリアグレードNATに敗北し5Gルータを入れた話

# きっかけ

[この話(OpenVPNでキャリアNAT超えで自宅LANに接続できるようにする)](https://qiita.com/daredeshow/items/b85e029169ec35add387)の続き

クラウドに出島を作り幸せなOpenVPN生活を送っていた筆者であったが、マンションキャリアの非情な仕様変更が襲いかかる！

# どういうこと？

キャリアグレードNATはその仕様上、グローバルIPアドレスは不定になる。最近さらに one-way connection(≒ステートレスNAT) と思われる仕様変更が入ったと思われ、OpenVPNが追従できなくなってしまった。

# one-way connection ってなに？

要するに中から外には出れるがその逆はできないネットワーク(接続)のこと。

通常 OpenVPN は以下のように接続が行われる

(1). `SERVER-IP:1194 <-UDP-> CLIENT-IP:A` 間で TLS Negotiation を行い、通信用の共通鍵や各種設定を交換する
(2). `SERVER-IP:B <-UDP-> CLIENT-IP:C` 間で実際の暗号化通信を行う

上記の (1), (2) はそれぞれ "コントロールチャンネル", "データチャンネル" と呼ばれる。
NATを挟んだ環境では実際には以下のようになる。`(G)` はグローバルIPアドレス、`(P)` はプライベートIPアドレスを指す。

(1). `SERVER-IP(G):1194 <-UDP-> INTERMEDIATE-IP(G):A <-UDP-> CLIENT-IP(P):B`
(2). `SERVER-IP(G):C <-UDP-> INTERMEDIATE-IP(G):D <-UDP-> CLIENT-IP(P):E`

ここであれ？と思った人もいるかもしれない。UDP は一方向通信なので `SERVER-IP:1194 -UDP-> INTERMEDIATE-IP(G):A -UDP-> CLIENT-IP(P):B` の通信はできないんじゃないの？と思う人もいると思う。

それは新規接続の話であって、通常は `SERVER-IP:1194 <-UDP- INTERMEDIATE-IP(G):A <-UDP- CLIENT-IP(P):B` が成立したあとであれば `SERVER-IP:1194 -UDP-> INTERMEDIATE-IP(G):A -UDP-> CLIENT-IP(P):B` も同様に(クライアント側が許可していれば)成立することが多い。これはステートフルNAT(SNAT)と呼ばれる機能によって実現されている。「(通常) `A<-B` の通信が発生したら `A->B` の戻りが発生するよね。だから `IP-A:PORT-M <-> IP-B:PORT-N` の関係性はしばらくの間維持しておこう」という塩梅でNAT上に一定の間ルーティングが維持される。
実は OpenVPN のUDP通信はこの特性を暗黙のうちに利用することで、複数の NAT が挟まった環境であってもサーバ〜クライアント間のVPN接続ができてしまう。

しかしキャリアグレードNATのような大規模NATの環境では、ステートフルNATを維持するためのコネクション追跡テーブルが大きくなり、しばしば事前に定義した最大サイズにヒットした結果、新規接続を受け付けられなくなるという事象が起きることがある。
またそうでなくても、コネクション追跡テーブルは定期的にリフレッシュされ、(主に通信の実績がない)コネクションはテーブルから削除され、結果として `IP-A:PORT-M <-> IP-B:PORT-N` の紐付けはある程度の時間で失われてしまう。
参考: あなたの大量配信サーバ、ip_conntrack溢れていませんか？ - https://www.e-agency.co.jp/column/20121225.html

OpenVPNは定期的にデータをサーバ〜クライアント間で交換することで、データチャンネルの接続を極力維持するように動作する([`--keepalive`オプション](https://community.openvpn.net/openvpn/wiki/Openvpn23ManPage))。データチャンネルの接続が切れたと思われる場合には、上記の (1) のシーケンスをやり直し、再接続を行う。再接続は環境にもよるが、大体2,3秒程度で終わる。
また余談になるが、OpenVPNは通信の安全性確保のために定期的にデータチャンネルの暗号鍵のリフレッシュやコントロールチャンネルのTLS再ネゴシエーションを行っているため、全く問題ない構成であっても定期的に接続がソフトリセットされている。この場合はIPアドレスやポート番号の再マッピングは行われない。
参考: OpenVPN tunnel session management options - https://openvpn.net/vpn-server-resources/openvpn-tunnel-session-management-options/

つまりOpenVPNは、コネクション追跡テーブルの動作程度のIPアドレスやポートの再割当てに対しては即座に再接続を行うことで、VPN接続を維持するような耐性を有している。

しかし大規模NATの管理者の視点ではこの動作はストレスフルなものになる。これを説明するために、TCPとUDPの挙動の違いについて説明する必要がある。

TCPの場合コネクション型の双方向通信であり、明確な接続終了という概念がある。TCPパケット中の FIN ビットによって接続が継続するのか終了するのかを判断することができ、NAT管理者の視点ではコネクションが "`CLOSED`" のステータスに変化したら、コネクション追跡テーブルから対応を削除すればよい。TCP通信は長時間維持されるものは少なく、コネクション追跡テーブルに存在するエントリはほぼ全てが実際に通信を行っているとみなしてよい。つまり必ず維持しなければいけない対応マップということになる。
一方UDP通信にはコネクションという概念がなく、それゆえ明確な接続終了という概念も存在しない。そもそも双方向通信が行われるのかどうかもUDPパケットだけ見ても判別することはできない。UDP通信が終了したかどうかを判別するには、最後に通信が行われてからの無通信時間を見て、タイムアウト処理をさせるしかない。NAT管理者の視点では、コネクション追跡テーブル(=サーバのメモリ)を消費するUDPのエントリというものは、そもそも通信してるのかどうか怪しい、これ維持する必要ないんじゃないの、という発想になる。
参考: TCPとUDPについてその② - https://www.kenschool.jp/blog/?p=5546

そこでリソースも逼迫しがちなキャリアグレードNATの管理者としては「メモリも逼迫してるしメモリ増設も高いからNG。サーバのメモリ節約のためにUDP通信はコネクション追跡する必要ないでしょ。UDPはそもそもコネクションレスの通信なんだから。」という結論に至り、コネクション追跡テーブルからUDP通信が除外されることになる。

そうすると、クライアントからサーバにUDP通信を行う場合、その場限りのマッピングが作成されるが、UDPパケットがNATを通過した後その対応は即座に消えてしまう。従ってUDPを使った戻り通信は既に存在しないマッピングを使用しようとして、当然ないわけだからルーティングされず、通信はタイムアウトする。これがいわゆる "one-way connection" ということになる。

# one-way connection下におけるOpenVPNの挙動

one-way connectionのNATが間にある環境におけるOpenVPNの通信はどうなるか。当然成立しなくなるわけだが、その挙動を順を追って解説する。

1. すでにOpenVPNのセッションが確立している状態で one-way connection NAT が間にはさまると、データチャンネルの `SERVER-IP(G):C -UDP-> INTERMEDIATE-IP(G):D -UDP-> CLIENT-IP(P):E` が成立しなくなるので、データ通信がタイムアウトする。
1. OpenVPNはコントロールチャンネルの再ネゴシエーションを試みる。クライアントからの `SERVER-IP(G):1194 <-UDP- INTERMEDIATE-IP(G):A <-UDP- CLIENT-IP(P):B` 、および戻りの `SERVER-IP(G):1194 -UDP-> INTERMEDIATE-IP(G):A -UDP-> CLIENT-IP(P):B` が成立して、一見再接続が成功したように見える。
    - ここ。つまり一見再接続が成功したように見えるところが落とし穴で、おそらくNAT実装の一部はコネクション追跡テーブルに作成されたマッピングを破棄するまでに若干の時間差があり、その間にOpenVPNが TLS Negotiation を完了させてしまうため、一見再接続が成功したように見え、事象をわかりにくくしている
    - もしくは一部の人気のあるアプリケーションのために(ゲームとか？)、UDPも引き続きコネクション追跡テーブルに記録するものの、生存時間を極端に短くしている場合でも起こりうる。この場合ももちろん再接続は成功するが、実際の通信はまともに疎通しないという結果になる。
1. その後、実際のデータチャンネルで通信を行おうとすると、コネクション追跡テーブルに既に対応が存在しないため、まともに通信できないという事象が発生し、上記の2. に戻り、再ネゴシエーションが始まる。この2.~3. を無限ループすることになる。
    - ここのデータチャンネルの通信不具合はどのような通信をしようとしていたかによって事象が変わる。例えば動画再生をしようとして、クライアントからデータを送信中だった場合は、TLS Negotiation 後確立したデータチャンネルによって若干OpenVPNの相手方にデータが流れてくるため、一瞬動画が再生される。しかしkeep-aliveの通信が成功しないため、上記 2. に戻ることになる

従って事象の見え方としてはひどいパケ詰まりのような、通信できてるようなできていないような中途半端な状態になる。OpenVPNを使っている環境だと、LANの名前解決のためにLAN内のDNSサーバを参照させてることも多いと思うので、その場合は名前解決に失敗した、というような見え方になる。

上記の事象が起きた場合のOpenVPNサーバには以下のようなログが延々と記録される。数十秒〜数分の間に再接続が繰り返され、再接続のたびにポート番号が変わるのがポイントだ。

```text:openvpn.log
Thu Jul 14 08:32:10 2022 INTERMEDIATE-IP-A:PORT-M TLS: Initial packet from [AF_INET]INTERMEDIATE-IP-A:PORT-M, sid=xxxxxxxx yyyyyyyy
Thu Jul 14 08:32:10 2022 INTERMEDIATE-IP-A:PORT-M VERIFY OK: depth=1, CN=servername
Thu Jul 14 08:32:10 2022 INTERMEDIATE-IP-A:PORT-M VERIFY OK: depth=0, CN=clientname
Thu Jul 14 08:32:10 2022 INTERMEDIATE-IP-A:PORT-M peer info: IV_VER=2.5.3
Thu Jul 14 08:32:10 2022 INTERMEDIATE-IP-A:PORT-M peer info: IV_PLAT=linux
Thu Jul 14 08:32:10 2022 INTERMEDIATE-IP-A:PORT-M peer info: IV_PROTO=6
Thu Jul 14 08:32:10 2022 INTERMEDIATE-IP-A:PORT-M peer info: IV_NCP=2
Thu Jul 14 08:32:10 2022 INTERMEDIATE-IP-A:PORT-M peer info: IV_CIPHERS=AES-256-GCM:AES-128-GCM
Thu Jul 14 08:32:10 2022 INTERMEDIATE-IP-A:PORT-M peer info: IV_LZ4=1
Thu Jul 14 08:32:10 2022 INTERMEDIATE-IP-A:PORT-M peer info: IV_LZ4v2=1
Thu Jul 14 08:32:10 2022 INTERMEDIATE-IP-A:PORT-M peer info: IV_LZO=1
Thu Jul 14 08:32:10 2022 INTERMEDIATE-IP-A:PORT-M peer info: IV_COMP_STUB=1
Thu Jul 14 08:32:10 2022 INTERMEDIATE-IP-A:PORT-M peer info: IV_COMP_STUBv2=1
Thu Jul 14 08:32:10 2022 INTERMEDIATE-IP-A:PORT-M peer info: IV_TCPNL=1
Thu Jul 14 08:32:10 2022 INTERMEDIATE-IP-A:PORT-M Control Channel: TLSv1.2, cipher TLSv1/SSLv3 ECDHE-RSA-AES256-GCM-SHA384, 2048 bit RSA
Thu Jul 14 08:32:10 2022 INTERMEDIATE-IP-A:PORT-M [clientname] Peer Connection Initiated with [AF_INET]INTERMEDIATE-IP-A:PORT-M
(略)
Thu Jul 14 08:34:13 2022 INTERMEDIATE-IP-A:PORT-N TLS: Initial packet from [AF_INET]INTERMEDIATE-IP-A:PORT-N, sid=xxxxxxxx yyyyyyyy
Thu Jul 14 08:34:13 2022 INTERMEDIATE-IP-A:PORT-N VERIFY OK: depth=1, CN=servername
Thu Jul 14 08:34:13 2022 INTERMEDIATE-IP-A:PORT-N VERIFY OK: depth=0, CN=clientname
Thu Jul 14 08:34:13 2022 INTERMEDIATE-IP-A:PORT-N peer info: IV_VER=2.5.3
Thu Jul 14 08:34:13 2022 INTERMEDIATE-IP-A:PORT-N peer info: IV_PLAT=linux
Thu Jul 14 08:34:13 2022 INTERMEDIATE-IP-A:PORT-N peer info: IV_PROTO=6
Thu Jul 14 08:34:13 2022 INTERMEDIATE-IP-A:PORT-N peer info: IV_NCP=2
Thu Jul 14 08:34:13 2022 INTERMEDIATE-IP-A:PORT-N peer info: IV_CIPHERS=AES-256-GCM:AES-128-GCM
Thu Jul 14 08:34:13 2022 INTERMEDIATE-IP-A:PORT-N peer info: IV_LZ4=1
Thu Jul 14 08:34:13 2022 INTERMEDIATE-IP-A:PORT-N peer info: IV_LZ4v2=1
Thu Jul 14 08:34:13 2022 INTERMEDIATE-IP-A:PORT-N peer info: IV_LZO=1
Thu Jul 14 08:34:13 2022 INTERMEDIATE-IP-A:PORT-N peer info: IV_COMP_STUB=1
Thu Jul 14 08:34:13 2022 INTERMEDIATE-IP-A:PORT-N peer info: IV_COMP_STUBv2=1
Thu Jul 14 08:34:13 2022 INTERMEDIATE-IP-A:PORT-N peer info: IV_TCPNL=1
Thu Jul 14 08:34:13 2022 INTERMEDIATE-IP-A:PORT-N Control Channel: TLSv1.2, cipher TLSv1/SSLv3 ECDHE-RSA-AES256-GCM-SHA384, 2048 bit RSA
Thu Jul 14 08:34:13 2022 INTERMEDIATE-IP-A:PORT-N [clientname] Peer Connection Initiated with [AF_INET]INTERMEDIATE-IP-A:PORT-N
```

ちょうど時期的に[KDDIの大規模障害](https://news.kddi.com/kddi/corporate/newsrelease/2022/07/29/6183.html)のあとだったため、上記のログに気付けず、「KDDIめ・・・まだ通信制限してるのか？もしくはコアルータ交換時にあわせてVPN通さないように仕様変更したか？」と疑いをかけてしまった。KDDI無罪でした。大変申し訳ない。

で、当然OpenVPN公式もこの話はわかっていて、解決した後に FAQ ページに気づきました。
"2x HOW TO - Troubleshooting" : https://openvpn.net/community-resources/how-to/

> The connection stalls on startup when using a proto udpconfiguration, the server log file shows this line:
> ```
> TLS: Initial packet from x.x.x.x:x, sid=xxxxxxxx xxxxxxxx
> ```
> however the client log does not show an equivalent line.
> 
> Solution: You have a one-way connection from client to server. The server to client direction is blocked by a firewall, usually on the client side. The firewall can either be (a) a personal software firewall running on the client, or (b) the NAT router gateway for the client. Modify the firewall to allow returning UDP packets from the server to reach the client.

# OpenVPNの通信をtcpに変えればいいのでは？

それはそう。ただしそれはベストケースの場合のみ成立する。Lossy Networkにおいてtcp-over-tcpの通信をすると再送地獄になるので望ましくない。詳細は以下の記事を参照のこと。
What is TCP Meltdown? - https://openvpn.net/faq/what-is-tcp-meltdown/

# で、5Gルータを入れたって話

で、こりゃ無理だどうしようもならん、と OpenVPN側でなんとかすることをあきらめ、5Gルータを買ってきて、家のルータ(=L3スイッチ)に接続。OpenVPNサーバ向けの通信だけ 5G ルータに振り向けるように設定して、再度OpenVPNが使えるようになりました。

- `123.45.0.1` がマンションキャリアのデフォルトゲートウェイ(その先がキャリアグレードNAT)
- `111.222.33.44`が クラウド上のOpenVPNサーバのIPアドレス
- `192.168.0.1` が 5G ルータのアドレス
- `192.168.10.0/24` が家の中のLAN
です

```shell
root@gw:~# netstat -rn
Kernel IP routing table
Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
0.0.0.0         123.45.0.1      0.0.0.0         UG        0 0          0 eth2
10.9.0.0        10.9.0.9        255.255.255.0   UG        0 0          0 tun0
10.9.0.9        0.0.0.0         255.255.255.255 UH        0 0          0 tun0
111.222.33.44   192.168.0.1     255.255.255.255 UGH       0 0          0 eth1
123.45.0.0      0.0.0.0         255.255.248.0   U         0 0          0 eth2
192.168.0.0     0.0.0.0         255.255.255.0   U         0 0          0 eth1
192.168.10.0    0.0.0.0         255.255.255.0   U         0 0          0 br-lan
```

# 最後に

普段お気楽に使ってるものがトラブるとしんどいですね。参考になればうれしいです。