PPPoE ルータ同志で IPsec を構築するときの問題

はてブ数 2007/01/31Linux

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 を設定することは不可能です。

*1 : 時折パケットがロストする

*2 : 詳しい人補足願います

*3 : 出て行くときにはすでにIPsecでカプセル化されているので、書き換えようがない

*4 : IPFilter の機能不足で困ることが多かったのですが、今度から pf 使おう(汗)

さらなる疑問

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のフラグメントパケットを修復する際に、ヘッダ書き換え(?)か何か悪さしてるんじゃないかと思いますが……。

*5 : Linux 側 iptablesで確認しましたが、フラグメントパケットは記録されていませんでした。iptables にESPパケットが届いているのは確認できます