PPPoE ルータ同志で IPsec を構築するときの問題
IPsecで一部の通信が遮断される謎
IPsec構築時の問題点に書いたことですが、一部の通信が遮断される問題の原因を追求しました。一言で言えば、IPsecパケットが PPPoE(フレッツ網) を通るには大きすぎてフラグメントしていることが原因でした。
より詳細な原因
ネットワーク図 192.168.10.0/24 <--LAN--> FreeBSD <--Internet--> Linux 2.6 <--LAN--> 192.168.20.0/24
192.168.10.0 内のWindowsマシンから出るパケットは MTU(パケット最大サイズ)として 1454 が設定されています。しかし、フレッツ網を PPPoE で抜ける際に PPP でくるまれ、さらに IPsec でくるまれるためにこのサイズのパケットは通過できません(参考)。
次のようにIPパケットの断片化が発生しています。
# tcpdump -i tun0 esp 21:35:40 IP FreeBSD > Linux: ESP(spi=0x01053609,seq=0x450), length 268 21:35:40 IP FreeBSD > Linux: ESP(spi=0x01053609,seq=0x451), length 1432 21:35:40 IP FreeBSD > Linux: esp 21:35:40 IP FreeBSD > Linux: ESP(spi=0x01053609,seq=0x452), length 1044 21:35:40 IP Linux > FreeBSD: ESP(spi=0x0129b031,seq=0x402), length 76 21:35:40 IP FreeBSD > Linux: ESP(spi=0x01053609,seq=0x453), length 1432 21:35:40 IP FreeBSD > Linux: esp
本来、IPパケットが断片化されようと、相手側のLinuxマシンで再構築され復元されるため、断片化により通信速度は低下しますが、通信自体は問題ありません。
生パケット → IPsecカプセル化 → IPフラグメント(分割) → Internet → IPパケット再構築 → IPsecのカプセルを解く → 生パケット
しかし 192.168.20.0 内にはパケットが届いておりません。Linuxマシンのインターフェイスまでは届いているのですが、iptables なのか、その先の IPsec なのか、ともかくうまく行っていないようです。
tcpmss を設定する
tcpはコネクション時に、TCPの中に含まれるデータサイズMSS(Maximum Segment Size、通常 MTU-40byte)をネゴシエーションします。
# tcpdump -i fxp0 port 8080 21:33:25 IP pc11.5955 > pc22.8080: S xxx(0) win 5840 <mss 1414,sackOK,timestamp 4032556 0,nop,wscale 2> 21:33:25 IP pc22.8080 > pc11.5955: S xxx(0) ack yyy win 65535 <mss 1460,nop,nop,sackOK>
このときの小さい方の数字をTCPの通信において使用します。これを tcpmss と言います。
IPsecやPPPoEのトンネル化などによりヘッダサイズが増大してしまうなら、この mss の値を MTU に対して小さく設定してあげればいいことになります。この操作をルータ側で強制的に行ってしまえば、結果的にIPsecにおける断片化(フラグメント)を防ぐことができます。
MTU > mss + IPsec & IP & TCPヘッダサイズ
通信路MTUの調査方法
ping コマンドでパケットサイズを指定することで、通信路のMTUを調べることができます。
# Linux/FreeBSDの場合 $ ping -l 1370 192.168.20.2 # Windowsの場合 > ping -s 1370 192.168.20.2
この場合、どちらも1370バイトのpingパケットを 192.168.20.2 に投げます。無事パケットが返ってくる限界値+28byteが経路MTUになります。mssはMTU-40になります。
Linux側による解決策
必要なmssの値はMTUから計算で出せるハズですが、なぜかそれではうまく行かず*1、結局試行錯誤で次のように設定しました。
iptables -A FORWARD -p tcp -s 192.168.10.0/24 --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1344 iptables -A FORWARD -p tcp -d 192.168.10.0/24 --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1344
試行錯誤は、tcpdump を用いて esp の断片化が起きないサイズを探り出すことにしました。IPsecのフッタの一部がサイズ可変らしく(?*2)、pingによる調査では不十分でした(それよりも低い値になった)。
FreeBSD側による解決策
FreeBSDのpppデーモンには、すでにtcpmssfixupという機能が付いているのですか、pppd を通過する段階ではすでにIPsecでカプセル化されているため無力です*3。同じ理由で、ipnat.conf でも、mssを調整する機能があるのですが、パケットが出て行くときしか書き換えられないためにIPsec では使用出来ません。
最初後述するように ipfw を使っていたのですが、pf できました。*4
デフォルトのカーネルで使用出来ると思います。rc.conf には次のように書きます。
pf_enable="YES"
pf.conf には次のように書きました。LAN側インターフェイスの 入力時 と 出力時 にそれぞれフィルタすることに注意してください。
int_if="fxp0" # LAN interface scrub in on $int_if from 192.168.10.0/24 to 192.168.20.0/24 max-mss 1344 scrub out on $int_if from 192.168.20.0/24 to 192.168.10.0/24 max-mss 1344
「/etc/rc.d/pf start」 する際に ALTQ がどうのと怒られますが、ALTQ関連機能を使用しなければ問題ないので気にしないでください。
# /etc/rc.d/pf start Enabling pf. No ALTQ support in kernel ALTQ related functions disabled
FreeBSD側による別の解決策(非推奨)
今更 ipfw の時代でもないので非推奨の方法です(動作自体は問題ないのですが煩雑です)。
結局、ipfw + tcpmssd を使用しました。tcpmssd は /usr/ports/net/tcpmssd よりインストールしました(パッケージでも構いません)。起動スクリプトがありませんので、tcpmssd.sh.txt を /usr/local/etc/rc.d/tcpmss.sh として保存し使用してください。
カーネルのコンパイルオプションです。
options IPFIREWALL options IPFIREWALL_VERBOSE options IPDIVERT
rc.conf です。mssではなくMTUを与えることに注意。
firewall_enable="YES" firewall_type="/etc/ipfw.sh" tcpmssd_enable="YES" tcpmssd_flags="-p 50 -m 1372"
/etc/ipfw.sh です。IPsecを通過する tcpコネクションパケット を tcpmssd に飛ばし mss を書き換えます。
add 64900 divert 50 tcp from 192.168.10.0/24 to 192.168.20.0/24 tcpflags syn add 64902 divert 50 tcp from 192.168.20.0/24 to 192.168.10.0/24 tcpflags syn
pfによる方法と同様に、FreeBSDマシンから直接 192.168.20.0/24 へ向かうパケットは ipfw のルールを通過せず、tcpmss を設定することは不可能です。
さらなる疑問
IPsecにおいて、カプセル化後にIPパケットのフラグメントが発生した場合、本来ならば受信側がパケットを再構築して通信できるはずだと思うのですが……(速度が遅くなるのは仕方ないとしても)。
- FreeBSD ←→ FreeBSDなら問題ないのか?
- Linux ←→ Linuxなら問題ないのか?
- FreeBSD(6.x) ←→ Linux(2.6.x)で通信可能にする手段は存在しないのか?
そもそも、Linux側には ip_conntrack (コネクション追跡)がインストールされているのでフィルタリングより前にESPパケットが再構築されているハズなのですが*5、フラグメントしたESPパケットの到着は確認できても、IPsecのカプセル化を解いた後のパケットは確認できていません。
色々調べたところ、Linuxはカプセル化する前にパケットを分割しますが、FreeBSDはカプセル化してからパケットを分割しています。この辺のミスマッチが怪しいですね。ip_conntrack がESPのフラグメントパケットを修復する際に、ヘッダ書き換え(?)か何か悪さしてるんじゃないかと思いますが……。