Linux 6.5のnetfilterのNATはTCPのTIME_WAIT状態のポートを使いまわす

TCPでは、自分から接続を切断する際に、外向きのFIN(+ACK)→内向きのFIN+ACK→最後の外向きのACK、という順でパケットがやりとりされます。この外向きのACKを送ったあとしばらくはソケットを使い回さないようにTIME_WAITという状態が設定されます(理由は他のサイトを見てください)。

Linuxのnetfilter(conntrack)によるNATでも、(自端末あるいは他端末からの)TCP接続を管理するにあたってこのTIME_WAITの状態が発生します。TIME_WAIT状態になっているポートはその間はNATのために使用されることはなくなります。

例えば192.168.1.1:2000からのTCP通信が1.2.3.4:3000にNATされて1.1.1.1:80に出ていって、そのTCP通信が終了すると、1.2.3.4:3000はしばらくTIME_WAIT状態になり、その間に他のポート(例えば192.168.1.1:4000)が1.1.1.1:80に通信しようとすると1.2.3.4:3000が使われることはありません(行き先が1.1.1.1:80以外なら普通に使われる)。

このタイムアウト時間はカーネルパラメータのnet.netfilter.nf_conntrack_tcp_timeout_time_waitにて設定されておりデフォルト値は120となっていますが、変更することができます。また、nftables/iptablesで特定の通信のみを対象にタイムアウトを変更することもできるようです(そこそこ新しいカーネルが必要?)。 net.ipv4.tcp_tw_recycle は廃止されました ― その危険性を理解する #Linux - Qiitaにある通りTCPソケット自体に関するTIME_WAITの時間を原則的に変更できないのとは対照的です。

このタイムアウトを短く(例えば5秒程度)変更すると、接続が終了したTCPポートを即座に使い回すことができるようになるため、利用可能なTCPポートが非常に少なくても(例えば16個とか)、ポートを多く消費するサイト(「ニチバンベンチ」で知られるスピール膏™ワンタッチEX|うおのめ・たこ|ニチバン株式会社:製品情報サイトなど)をそれなりに実用的な速度で閲覧できるようになります。

ここまではいいのですが、ここからが本題です。

Linux 6.5で試した結果、上記のようにTIME_WAIT状態になっているTCPポートでも即座にNATに使い回されるという現象が発生しました。例えば、宛先ポート3478の通信のNAT先ポートを1つだけに絞ってStuntman - open source STUN serverTCPモードで使用して試してみると、6.5以前のバージョンであればTIME_WAITの120秒が経過するまで一切通信が通らないのに、6.5以降だと120秒が経過する前に通信が通ります。conntrack -Lで見てみると既存の通信が置き換えられている(NAT先ポートも宛先のIP・ポートも変わらないのにNAT前のポートの部分だけが変わっている)ことがわかります。

しかも、必ず通るというわけでもなく、3回に1回など、確率的に通るようです。

試したカーネルのバージョンは以下の通りです。太字が、今回の現象が発生したものです。ディストリビューションLinux Mint 21.3で、(関係あるかわかりませんが)iptablesはv1.8.7 (nf_tables)、nftは1.0.2、libnftnlは1.2.1-1build1とかそんな感じです(カーネル以外は変えていません)。

5.15.0-102
5.19.0-50
6.2.0-39
6.3 (mainline)
6.4 (mainline)
6.5 (mainline)
6.5.0-14
6.5.0-27
6.8.6 (mainline)

mainlineというのは概念がよくわかっていませんが、GitHub - bkw777/mainline: Install mainline kernel packages from kernel.ubuntu.comを普通に使って入れられるやつのことです。

この挙動は、試した限りでは、以下のようなそれっぽいカーネルパラメータを変更しても変化しませんでした(6.5以降のものが使い回さないようになることも、6.5より前のものが使いまわすようになることもなかった)。

net.ipv4.tcp_timestamps
net.ipv4.tcp_tw_reuse
net.netfilter.nf_conntrack_tcp_be_liberal
net.netfilter.nf_conntrack_tcp_loose
net.netfilter.nf_conntrack_timestamp

Changelogなどを読んでもそれっぽい原因が全く思い当たらないので、superuser.comで質問してみたところ、ドンピシャな関連コミットの情報を教えてもらうことができました。

https://superuser.com/questions/1839024/linux-6-5-netfilter-nat-reuses-tcp-ports-in-time-wait-status
コミットはこちらです。
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=4589725502871e77d06464f731f92fd9173e2be6
これによると、128回の試行のうち、最後の32回は、シーケンス番号が増加していれば既存のTIME_WAITエントリでも構わず再利用するようです。