●はじめに
イエラエセキュリティの中の人一号です。先日、L2TP/IPSec VPN を設定する機会があり、その際に「トンネル内でサイズが大きなパケットを 1 つでも送出した瞬間にトンネル全体の通信が止まって VPN接続が切れる」というトンネル崩壊現象に悩まされました。
調査の結果、やや珍しいと思われる原因が判明し対策方法も分かったので、備忘録的に記しておきます。
●環境
[SEIL/X1 Ver. 5.94 (Client)] <=(L2TP/IPSec)=> [xl2tpd with kernel support (Server)]
●現象
SEIL/X1 が張ったトンネル内で、pppインターフェースの MTU より大きいパケットをクライアント側から一つでも送出した瞬間にトンネル内の全通信が止まり接続が切れる。
ただし、Windows 10 に組み込まれている L2TP/IPSecクライアントが同一環境・同一設定で張ったトンネルでは、通信切断は起きない。
●原因
SEIL/X1 は、L2TP のレイヤーで、Data Packet に対して Sequence Number を付与します。Sequence Number は、L2TP/IPSecパケット 1 つ毎に 1 インクリメントされます。
Data Packet に対する Sequence Number はoptional(https://tools.ietf.org/html/rfc2661#page-44)であり、SEIL/X1 は Sequence Number を付与します。 Sequence Number が付与されている場合、サーバーは Sequence Number を参照しながら L2TP/IPSecパケットを処理します。
トンネル内で送出されるパケットが MTU より大きい場合にはパケットは分割され、それぞれのパケットが別々の L2TP/IPSecパケットに包まれて対向側に向けて送出されます。
分割されるパケットには、それぞれ別の Sequence Number が振られます。 分割されるパケットの直前の Sequence Number が n – 1 で分割数が 2 とすると、分割されるパケットに振られる Sequence Number は n と n + 1 になります。
L2TP/IPSec のパケットは UDP で届けられるため、経路上で順番が入れ替わる可能性があります。
末端の SEIL/X1 は順番通りにパケットを送出するようですが、私が VPN を設定していた経路では(どこで入れ替わっているかは不明ですが)サーバーに到達するまでに分割されたパケットの順番が入れ替わります (1)。
つまり、サーバーから見ると Sequence Number が 1 飛んだ n + 1 のパケットが先に到着します。
サーバーはデフォルトでは Sequence Number が飛んだ場合に対応しておらず、Sequence Number が 1 ずつインクリメントされることを期待しているので、Sequence Number が飛んだ瞬間に状態が壊れて以後のパケットを受け付けなくなります。 Windows 10 の場合も同様にパケット順序の入れ替わりが起こりますが、そもそも Sequence Number が付与れていない (2) ようで、サーバーは入れ替わりに気付かずに処理を続けます。
●現象発生時のパケットとログ
pppインターフェースの MTU が 1400 の設定で、通信が全てトンネル内を通過するようにしたクライアント側の Linux から、次のコマンドを実行してパケットとデバッグログを記録しました。この設定ではペイロード長が 1373 以上になるとパケットが分割されます。
ping -s 1372 (疎通確認先の IPアドレス) (Ctrl+C で打ち切り)
ping -s 1373 (疎通確認先の IPアドレス)
L2TP/IPSec のパケットをサーバー側でキャプチャして Wireshark で復号し L2TPヘッダの内容を表示したものを、時系列で次に示します。Ns が Sequence Number を指していて、30、32、31と順番が飛んでいることが分かります。
———
現象発生直前の ICMP Requestパケットの L2TPヘッダ
Layer 2 Tunneling Protocol
Packet Type: Data Message Tunnel Id=25085 Session Id=42025
0... .... .... .... = Type: Data Message (0)
.1.. .... .... .... = Length Bit: Length field is present
.... 1... .... .... = Sequence Bit: Ns and Nr fields are present
.... ..0. .... .... = Offset bit: Offset size field is not present
.... ...0 .... .... = Priority: No priority
.... .... .... 0010 = Version: 2
Length: 1416
Tunnel ID: 25085
Session ID: 42025
Ns: 30
Nr: 65535
———
分割された ICMP Requestパケットの L2TPヘッダ (先に到達した方)
Layer 2 Tunneling Protocol
Packet Type: Data Message Tunnel Id=25085 Session Id=42025
0... .... .... .... = Type: Data Message (0)
.1.. .... .... .... = Length Bit: Length field is present
.... 1... .... .... = Sequence Bit: Ns and Nr fields are present
.... ..0. .... .... = Offset bit: Offset size field is not present
.... ...0 .... .... = Priority: No priority
.... .... .... 0010 = Version: 2
Length: 41
Tunnel ID: 25085
Session ID: 42025
Ns: 32
Nr: 65535
———
分割された ICMP Requestパケットの L2TPヘッダ (後に到達した方)
Layer 2 Tunneling Protocol
Packet Type: Data Message Tunnel Id=25085 Session Id=42025
0... .... .... .... = Type: Data Message (0)
.1.. .... .... .... = Length Bit: Length field is present
.... 1... .... .... = Sequence Bit: Ns and Nr fields are present
.... ..0. .... .... = Offset bit: Offset size field is not present
.... ...0 .... .... = Priority: No priority
.... .... .... 0010 = Version: 2
Length: 1412
Tunnel ID: 25085
Session ID: 42025
Ns: 31
Nr: 65535
———
サーバー上で採集した l2tp_coreモジュールのデバッグログを次に示します。 パケットが入れ替わって状態が壊れた結果、サーバーは既に到達した ns=32 のパケットを待ち続ける状態になっています。
[68489.340002] l2tp_core: sess 25085/42025: recv data ns=30, nr=65535, session nr=30
[68489.340039] l2tp_core: sess 25085/42025: updated nr to 31
[68491.247541] l2tp_core: sess 25085/42025: recv data ns=32, nr=65535, session nr=31
[68491.247575] l2tp_core: sess 25085/42025: oos pkt 32 len 41 discarded, waiting for 31, reorder_q_len=0
[68491.249427] l2tp_core: sess 25085/42025: recv data ns=31, nr=65535, session nr=31
[68491.249454] l2tp_core: sess 25085/42025: updated nr to 32
[68492.252559] l2tp_core: sess 25085/42025: recv data ns=34, nr=65535, session nr=32
[68492.252594] l2tp_core: sess 25085/42025: oos pkt 34 len 41 discarded, waiting for 32, reorder_q_len=0
●対策
サーバー側で、pppd を介してカーネルの l2tp_coreモジュールに Sequence Number が飛んだパケットを並べ替えるよう伝えるオプションを渡し、適宜並べ替えてもらうようにします。
具体的には、xl2tpd が利用する pppd のオプションファイルに次の行を加えます。
pppol2tp_reorderto 30
このオプションは L2TP Data Packet の並び替えタイムアウトを指定するもので、デフォルトでは 0 です。
https://github.com/paulusmack/ppp/blob/ppp-2.4.7/pppd/plugins/pppol2tp/pppol2tp.c#L100
このオプションが指定されると、L2TPパケットを処理する l2tp_coreモジュールが飛んだパケットを並べ替えてくれるようになります。
https://github.com/torvalds/linux/blob/v4.17/net/l2tp/l2tp_core.c#L512
●Sequence Number の付与
RFC2661 には「LAC(クライアント)が Sequence Number を Data Channel で使いたい場合には Sequencing Required AVP をセットアップ中に送出することで要求でき (3)、その AVP が無ければ付与の有無は LNS(サーバー)が制御して LAC はそれに従う(4)」(意訳)というようなことが書かれています。
今回キャプチャしたパケットや採集したログを見る限り、クライアントがSequencing Required AVP を送出している箇所は見つからず (5)、サーバーが Sequence Number 無しの Data Packet を送ってきてもそのまま Sequence Number を付与し続けている (6) ので、SEIL/X1 の L2TP/IPSecクライアントは無条件で Sequence Number を付与する実装になっている可能性があります。
●謝辞
この記事の執筆にあたっては株式会社レピダムの名古屋さんにご協力を頂きました。この場で感謝申し上げます。
1. 実験の結果「サイズが一定の関係にある大きい UDPパケットと小さい UDPパケットが十分小さい間隔で送出された場合、大きい方が先に送出されていても小さい方が先に届く」という現象を確認しました。MTU を超えて分割されたパケットがちょうどこの現象に当たるようです。↩
2. 手元の環境でキャプチャしたパケットでは付与されていないことを確認しました。↩
3. “The LAC may request that sequence numbers be present in data messages via the Sequencing Required AVP (see Section 4.4.6).”↩
4. “If this AVP is not present, sequencing presence is under control of the LNS.” “Thus, if the LAC receives a data message without sequence numbers present, it MUST stop sending sequence numbers in future data messages.”↩
5. トンネル確立開始時からキャプチャした L2TP/IPsecパケットに Wireshark上でフィルタ
l2tp.avp.type == 39
を適用しても該当するパケットが無く、サーバー側で xl2tpdに
debug avp = yes
を設定した状態でログを確認しても当該AVP を受信している様子がありませんでした。↩
6. トンネル確立開始時からキャプチャした L2TP/IPsecパケットに Wireshark上でフィルタ
(l2tp.seq_bit == 1) && (l2tp.type == 0) && (ip.src == <クライアントのIP>)
とするとパケットが複数該当し、
(l2tp.seq_bit == 1) && (l2tp.type == 0) && (ip.src == <サーバーのIP>)
とすると該当するパケットはありませんでした。↩