Security blog by Ierae Security,Inc.

脆弱性診断技術や関連情報を発信するブログメディア

L2TP/IPSecトンネルの崩壊と戦った話

はじめに

イエラエセキュリティの中の人一号です。 先日、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>)とすると該当するパケットはありませんでした。