conntrack がロードされたサーバで LVS-DR を構成する場合の注意点

こんにちは。インフラ開発チームの深谷です。

Linux の conntrack モジュールがロードされたサーバーで、レイヤー4 (L4) ロードバランサを LVS-DR で構成したところ、 パケットフィルタが正常に働かず、通信はできるものの、一部のパケットは落ちるという挙動に悩まされました。 今回はこの問題について詳細に解析し、原因と対策がわかったのでご紹介いたします。

発生した問題

今回の問題は、Ubuntu を使って L4 ロードバランサを実装している時に遭遇しました。 構築したロードバランサには、セキュリティ対策のため iptables のラッパーである UFW を使って、 パケットフィルタをかけていました。

ロードバランサの実装には、Linux カーネルに実装されている LVS を使いました。 LVS ではいくつかのパケット転送方式をサポートしており、今回はパフォーマンスに優れる Direct Routing (DR) 方式を利用しました。

LVS-DR は、以下の図のようにクライアントからのパケットの Ether ヘッダアドレスのみを書き換え、 リアルサーバに転送する方式です。 この方法では、戻りパケットがロードバランサを経由しないので、ロードバランサに負荷が集中しないというメリットが有ります。

lvs-dr

しかし、構築したロードバランサで検証していたところ、設定されているステートフルパケットフィルタが、クライアント側から返答される ICMP を落とすという問題に遭遇しました。

conntrack とステートフルパケットフィルタについて

Linux でパケットフィルタを実装する場合、iptables を利用することが出来ます。 iptables には、いくつかの 拡張モジュールが実装されています。 conntrack はその一つで、コネクションの接続状態をトラッキングし、アドレスや TCP フラグではなく、 コネクションのステートに基づいてパケットフィルタをかけることが出来ます。

例えば、TCP 接続の最初の SYN が観測された時、そのパケットのステートは NEW と扱われます。 また、3-way ハンドシェイクが完了した後のパケットは ESTABLISHED となります。 ICMP は接続済みのコネクションが存在する場合 RELATED となり、 未接続のパケットや不正なフラグが立ったパケットは INVALID として扱われます。

パケットフィルタの設定

Ubuntu では、UFW という iptables のラッパーソフトウェアが存在します。 UFW は簡単な設定で、iptables の設定を作成してくれるので、今回はこれを利用してパケットフィルタをかけていました。

以下に、UFW が生成した iptables のルールを簡略化したものを示します。 

-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -p tcp --dport 80 -j ACCEPT

iptables の設定は、上から順番に評価されます。 最初のフィルタは、接続済みのコネクションを通す設定です。 2番めのフィルタは、接続していないコネクションなどを通さない設定です。 最後のフィルタは、ステートは明示的には指定していませんが、新規コネクションで TCP 80番ポート向けのパケットを通す設定になっています。

この設定が正しく動作していれば、ICMP は RELATED として扱われるため、ロードバランサでパケットが落とされることはないはずです。

問題点と疑問点

勘の良い方は気づいたと思いますが、前述したとおり LVS-DR 構成では返答パケットがロードバランサを通過しないため、 コネクション接続状態である ESTABLISHED に到達することはありません。 そのため ICMP が INVALID として扱われたと考えれば辻褄が合います。

しかし、ICMP はトラッキング情報が ESTABLISHED にならなくても以前観測したパケットの情報があれば、RELATED として扱われます。 従ってこの洞察は正しくありません。

加えてもう一つ疑問が生じます。 ESTABLISHED にならないならば、3-way ハンドシェイク後のパケットも INVALID となるはずなので、そもそも通信できないはずなのです。 ところが予想に反しこの設定でリアルサーバのコンテンツを閲覧することが出来ます。 一体何が起きているのでしょうか?

Netfilter について

この現象についていくらか検索したのですが、納得の行く答えが見つかりませんでした。 そのため、Linux Kernel のコードを追うことにしました。

LVS や iptables は netfilter というカーネルで実装されたパケットフィルタフレームワーク上で実装されています。 コードはソースツリー上の net/netfilter/ 以下に配置されています。

Netfilter の各モジュールは、以下の図のように用意されたフックに対してコールバックを登録します。 それらのコールバックは優先度を指定でき、フックポイントでその順番に呼び出されます。

netfilter

conntrack は入りのパケットについては、PRE_ROUTING でトラッキング情報をパケットに付与します。 また、LOCAL_IN にて、パケットに付与されたトラッキング情報をテーブルに登録します。 ここで重要な事は、トラッキング情報の付与と保存は別のフックで行われることです。

LVS は入りのパケットに対して LOCAL_IN で転送対象であれば、後続のフックをスキップして、 LOCAL_OUT にバイパスします。

パケットフィルタは LOCAL_IN で動作します。

パケット処理の動作

実際のパケットの処理の様子は以下のようになります。

processing-hooks

まず、PRE_ROUTING で conntrack がパケットに対してトラッキング情報を付与します。 この時、トラッキング情報がテーブルに存在しない場合、SYN または ACK が立っている場合は NEW ステートに、それ以外のパケットは ICMP を含めて INVALID ステートになります。

次に、LOCAL_IN のフックですが、優先度の関係で図のようにパケットフィルタ、LVS、conntack の順に処理が行われます。 LVS は、パケットを LOCAL_OUT に転送し、またトラッキング情報を保存しないように設定してしまうので、決してコネクションの情報がテーブルに登録されることはありません。 また、パケットフィルタは、conntrack テーブルではなく、パケットに付与されたトラッキング情報を参照するため、NEW と INVALID の両方を観測しえます。

ここで、モジュールの動作と上のパケットフィルタの設定とを突き合わせてみます。

  • TCP で 3-way ハンドシェイクを行う場合 (SYN が立っている場合)
    NEW ステートのため最後のフィルタが適用されパケットが通ります。
  • TCP で 3-way ハンドシェイク後の送信パケットの場合 (ACK が立っている場合)
    NEW ステートのため最後のフィルタが適用されパケットが通ります。ESTABLISHED で通っているわけではないことに注意してください。
  • TCP でコネクション終了させる場合 (FIN や RST が立っている場合)
    INVALID ステートとなるため、2番めのフィルタに合致し、パケットが落とされます。実は ICMP だけでなく、これらのパケットも落とされてしまします。
  • ICMP パケットがクライアント側から帰ってきた場合
    INVALID ステートとなるため、パケットが落とされます。

この動作を見ると確かに、通信はできるにも関わらず、一部パケットが落とされるということが説明できています。

対策

ステートフルパケットフィルタは LVS-DR 構成では正しく動作しないことがわかったので、 静的フィルタで必要なパケットを通す必要があります。

例えば以下の様にすればよいでしょう。 ここでは INVALID になるパケットを追加で ACCEPT しています。 リアルサーバでは正常に conntrack が使えるので、ロードバランサで INVALID なものを通しても、リアルサーバ側では正しくパケットフィルタをかけることは可能です。 

-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -p tcp --tcp-flags SYN,ACK,FIN,RST,URG ACK,FIN -m conntrack --ctstate INVALID -j ACCEPT
-A INPUT -p tcp --tcp-flags SYN,FIN,RST,URG,PSH RST -m conntrack --ctstate INVALID -j ACCEPT
-A INPUT -p icmp --icmp-type destination-unreachable -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -p tcp --dport 80 -j ACCEPT

まとめ

今回の問題は conntrack を利用しているサーバで、LVS-DR を有効にすると conntrack の処理が妨げられ、一部パケットだけが通信を許可されるというものでした。

conntrack を利用していると、LVS-DR 用に静的フィルタを追加しなくても、不完全ですが通信が出来てしまうので注意しましょう。

おまけ

今回、上記の実験を LXC 上で行いました。 コンテナ技術を利用すると手軽にファイアーウォールやロードバランサの検証ができるので便利ですね。