新規投稿のお知らせを受信されたい方は、サブスクリプションをご登録ください:

Linuxのファイアウォールの乱用:Spectrumの構築を可能にした裏技について

2018/04/12

6分で読了

Cloudflareは本日、Spectrumの提供を開始します。Spectrumは、TCPベースのプロトコル向けのCloudflareの新たな機能で、DDoS攻撃に対する防御、ロードバランシング、およびコンテンツ高速化を実現します。

13334109713_0b32435032_z
CC BY-SA 2.0 画像作者Staffan Vilcans

Spectrumの構築を開始した直後、私たちは大きな技術的障害にぶつかりました。Spectrumでは、有効なTCPポート(1~65535)で接続を受け入れる必要があります。当社のLinuxエッジサーバーでは、「任意のポート番号で着信接続を受け入れる」ことは不可能です。これはLinux固有の制限ではなく、ほとんどのオペレーティングシステムのネットワークアプリケーションの基礎である、BSDソケットAPIの特性です。実は、Spectrumを提供するために解決しなければならなかった、以下の2つの互いに関わりあう問題がありました。

  • 1~65535のすべてのポート番号でTCP接続を受け入れる方法
  • 単一Linuxサーバーに、きわめて多数のIPアドレスでの接続を受け入れるよう設定する方法(当社のエニーキャスト範囲内には数千のIPアドレスが存在)

サーバーへの数百万IPの割り当て

Cloudflareでは、すべてのエッジサーバーがほぼ同じ設定になっています。当社の初期の頃には、ループバックネットワークインタフェースに一定の/32(および/128)IPアドレスを割り当てていました[1]。IPアドレスの単位が数十のうちは上手くいきましたが、会社の成長に伴って規模の拡張に失敗しました。

同時に「AnyIP」のトリックも問題になりました。AnyIPを使用すると、一定のIPアドレスから拡張し、IPプレフィックス(サブネット)全体のループバックインタフェースへの割り当てが可能になります。すでにAnyIPは広く利用されており、皆さんのコンピューターにはループバックインタフェースに127.0.0.0/8が割り当てられています。ユーザーのコンピューター側から見た場合、127.0.0.1から127.255.255.254までのすべてのIPアドレスはローカルマシンに属しています。

このトリックは、127.0.0.1/8ブロック以外にも適用可能です。192.0.2.0/24の範囲全体をローカルに割り当てたものとして扱うには、次を実行します。

ip route add local 192.0.2.0/24 dev lo

次に、これらのIPアドレスの1つでポート8080にバインドすることができます。

nc -l 192.0.2.1 8080

IPv6を動作させるのは少し難しくなります。

ip route add local 2001:db8::/64 dev lo

残念ながら、これらのアタッチされたv6IPアドレスには、v4の例のようにバインドすることはできません。これを機能させるには、さらに上の権限が必要な IP_FREEBIND ソケットオプションを使用する必要があります。完全性を確保するためにsysctl net.ipv6.ip_nonlocal_bindを使う方法もありますが、ここで触れることはお勧めしません。

このAnyIPのトリックを使用すると、数百万のIPアドレスを各サーバーにローカルに割り当てることができます。

$ ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536
    inet 1.1.1.0/24 scope global lo
       valid_lft forever preferred_lft forever
    inet 104.16.0.0/16 scope global lo
       valid_lft forever preferred_lft forever
...

すべてのポートへのバインド

2つ目の大きな問題は、任意のポート番号に対してTCPソケットを開く機能です。Linuxおよび、一般にBSDソケットAPIをサポートしている任意のシステムでは、特定のTCPポート番号へのバインドは、単一のバインドシステム呼び出しでしか行うことができません。単一操作で複数ポートへのバインドは不可能です。

単純に考えると、65535のポートに、1回ずつ、65535回バインドすることになります。確かにこのような方法もあったかもしれませんが、これは次のような酷い結果につながります。

内部的には、Linuxカーネルは、正確に32バケットを使用して、ポート番号でインデックスを指定したリスニングソケットをハッシュテーブルLHTABLEに保存します。

/* はい、本当に、これが必要なすべてです。 */

#define INET_LHTABLE_SIZE       32

65,000のポートを開いていたら、このテーブルのルックアップには長い長い時間がかかったでしょう。ハッシュテーブルの各バケットには2,000のアイテムが入っているからです。

問題を解決するもう1つの方法は、iptablesの豊富なNAT機能の使用です。一部の特定のアドレス/ポートへの着信パケットの宛先を書き換えて、アプリケーションをそれにバインドさせることができます。

しかし、これにはiptablesのconntrackモジュールを有効にする必要があるため、この方法は採りませんでした。過去にはパフォーマンスエッジケースがあり、conntrackは当社が被った大規模なDDoS攻撃の一部には対処できませんでした。

さらに、NATアプローチでは、宛先IPアドレス情報が失われる可能性があります。これを緩和するためにあまり知られていないSO_ORIGINAL_DSTソケットオプションもありますが、あまり使いたくなるようなコードではありません

幸い、65000のポートすべてをバインドするもしくは conntrackを使用する以外の方法で、目標を達成する方法があります。

ファイアウォールで対応

先に進む前に、オペレーティングシステム上でのネットワークパケットの流れについて復習しておきましょう。

一般に、受信パケットの経路には次の2つの異なる層があります。

  • IPファイアウォール
  • ネットワークスタック

これらは概念的に異なるものです。IPファイアウォールは通常、ステートレスなソフトウェアです(ここでは、conntrackとIPフラグメントの再構築は無視します)。ファイアウォールはIPパケットを分析し、それらを受け入れるかドロップするかを決定します。注意:この層では、アプリケーションやソケットではなく、パケットとポート番号を話題にしています。

次に、ネットワークスタックがあります。ここには多くの状態が維持されています。ネットワークスタックの主なタスクは、受信するIPパケットをソケットにディスパッチすることで、パケットはユーザー空間のアプリケーションによって処理されます。ネットワークスタックは、ユーザー空間と共有される抽象化を管理しています。TCPフローを再構成し、ルーティングを処理し、ローカルなIPアドレスを認識します。

魔法の粉

upload-1
出典:こちらもYouTube

ある時点で、TPROXY iptablesモジュールに偶然行き当たりました。公式ドキュメントは見落としてしまいやすいです。

TPROXY
このターゲットは、PREROUTINGチェーンおよび
ここからしか呼び出せないユーザー定義のチェーン内の
mangleテーブル内でのみ、有効です。 パケットヘッダーを変えることなく、
パケットをローカルソケットにリダイレクトします。また、
マーク値を変更して、高度なルーティングルールで
使用することもできます。

カーネルには、別に、次のドキュメントもあります。

考えるほど、もっと知りたくなりました。

つまり...TPROXYは実際に何をするのかということです。

魔法のトリックを明らかにする

TPROXYコードは驚くほど単純です。

case NFT_LOOKUP_LISTENER:
  sk = inet_lookup_listener(net, &tcp_hashinfo, skb,
				    ip_hdrlen(skb) +
				      __tcp_hdrlen(tcph),
				    saddr, sport,
				    daddr, dport,
				    in->ifindex, 0);

声に出して読み上げます。ファイアウォールの一部であるiptablesモジュールでは、私たちはinet_lookup_listenerと呼んでいます。この機能はsrc/dst port/IP 4-tupleを使い、接続を受け入れることのできるリスニングソケットを戻します。これは、ネットワークスタックのソケットディスパッチのコア機能です。

繰り返します。ファイアウォールコードはソケットディスパッチルーチンを呼び出します。

この後、TPROXYは実際にソケットディスパッチを行います

skb->sk = sk;

この行は、 socket struct sockをインバウンドパケットに割り当てて、ディスパッチを完了します。

帽子からウサギを取り出す

3649474619_3b800400e9_z-1
CC BY-SA 2.0 画像作者Angela Boothroyd

TPROXYで武装することで、bind-to-all-portsのトリックをきわめて容易に実行することができます。構成を次に示します。

# AnyIPで192.0.2.0/24をローカルにルーティングされるように設定します。
# このネットワークに使用される送信元IPを明示的に指定し、
# ローカルでの接続時は127.0.0.0/8の範囲にする必要があります。
# それ以外の場合はフォワードとバックワードの両方のトラフィックにTPROXYルールが
# 一致するため、これが必要です。これで捕捉させたいのは、
# フォワードトラフィックのみです。
sudo ip route add local 192.0.2.0/24 dev lo src 127.0.0.1

# 魔法のTPROXYルーティングを設定
sudo iptables -t mangle -I PREROUTING \
        -d 192.0.2.0/24 -p tcp \
        -j TPROXY --on-port=1234 --on-ip=127.0.0.1

この設定に加えて、魔法のIP_TRANSPARENTソケットオプションでTCPサーバーを起動する必要があります。次の例は、tcp://127.0.0.1:1234でリッスンする必要があります。IP_TRANSPARENTのマニュアルページには、次のように掲載されています。

IP_TRANSPARENT(Linux 2.6.24以降)
このブール値オプションを設定すると、透過プロキシが
このソケットで有効になります。 このソケットオプションは、呼び出しているアプリ
ケーションを非ローカルのIPアドレスにバインドして、
外部アドレスをローカルエンドポイントとして両方を
クライアントおよびサーバーとして動作させることができます。 注:これには、外部アドレスに向かうパケットを
TPROXYボックス経由でルーティングする方法でルーティングを
セットアップする必要があります(つまり、システムが
IP_TRANSPARENTソケットオプションを採用しているアプリケーションをホストしている)。
このソケットオプションを有効にするには、スーパーユーザー特権が必要です。
(CAP_NET_ADMIN機能)。

iptables TPROXYターゲットによるTProxyのリダイレクトにも、
このオプションをリダイレクトされたソケットに設定しておく必要があります。

シンプルなPythonサーバーを次に示します。

import socket

IP_TRANSPARENT = 19

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.setsockopt(socket.IPPROTO_IP, IP_TRANSPARENT, 1)

s.bind(('127.0.0.1', 1234))
s.listen(32)
print("[+] Bound to tcp://127.0.0.1:1234")
while True:
    c, (r_ip, r_port) = s.accept()
    l_ip, l_port = c.getsockname()
    print("[ ] Connection from tcp://%s:%d to tcp://%s:%d" % (r_ip, r_port, l_ip, l_port))
    c.send(b"hello world\n")
    c.close()

サーバーの実行後は、任意のIPアドレスからサーバーに接続できます。

$ nc -v 192.0.2.1 9999
Connection to 192.0.2.1 9999 port [tcp/*] succeeded!
hello world

最も重要なのは、サーバーは接続をレポートし、誰もそのIPアドレスおよびポートをリッスンしていないにも関わらず、192.0.2.1ポート9999にダイレクトされたことです。

$ sudo python3 transparent2.py
[+] Bound to tcp://127.0.0.1:1234
[ ] Connection from tcp://127.0.0.1:60036 to tcp://192.0.2.1:9999

いかがでしょう!これが、conntrackを使用せずにLinux上の任意のポートにバインドする方法です。

これがすべてです

この記事では、元々透過的プロキシ用に設計されたものを少し違う用途で使用するための、目立たないiptablesモジュールの使用方法を説明しました。このモジュールを借りて、標準のBSDソケットAPIを使用して不可能と思われたことを実行することができ、カスタムのカーネルによるパッチを使う必要性も回避することができます。

TPROXYモジュールは非常に珍しく、Linuxのファイアウォールの文脈では基本的にLinuxのネットワークスタックが実行することを実行します。公式ドキュメントが幾分不足しており、このモジュールの本当の力を理解しているLinuxユーザーはそれほど多くないと思われます。

TPROXYにより、vanillaカーネル上で当社のSpectrum製品がスムーズに動作すると言って良いでしょう。。また、iptablesとネットワークスタックについて理解することが、いかに重要かについても覚えておくとよいでしょう。


低レベルのソケットの作業に興味をお持ちですか?ロンドン、オースティン、サンフランシスコの世界的に有名なチーム、ポーランドのワルシャワの精鋭部隊に、ぜひご参加ください。


  1. IPアドレスを、appropriate rp_filterおよびBGP構成でループバックインタフェースに割り当てることで、任意のIP範囲を当社エッジサーバーで処理することができます。↩︎
Cloudflareは企業ネットワーク全体を保護し、お客様がインターネット規模のアプリケーションを効率的に構築し、あらゆるWebサイトやインターネットアプリケーションを高速化し、DDoS攻撃を退けハッカーの侵入を防ぎゼロトラスト導入を推進できるようお手伝いしています。

ご使用のデバイスから1.1.1.1 にアクセスし、インターネットを高速化し安全性を高めるCloudflareの無料アプリをご利用ください。

より良いインターネットの構築支援という当社の使命について、詳しくはこちらをご覧ください。新たなキャリアの方向性を模索中の方は、当社の求人情報をご覧ください。
Product News (JP)Security (JP)日本語DDoS (JP)Spectrum (JP)

Xでフォロー

Marek Majkowski|@majek04
Cloudflare|@cloudflare

関連ブログ投稿

2024年4月12日 13:00

Cloudflareのお客様がLet's Encryptの証明書チェーンの変更の影響を受けないようにする方法

Let's Encryptのクロス署名チェーンは9月に有効期限が切れます。この影響は、古いトラストストアを使用するレガシーデバイス(Androidバージョン7.1.1以前)に生じます。この変更による影響がお客様に及ぶことを防ぐため、CloudflareはLet's Encrypt証明書の更新時に別のCAを使用するように移行します...

2024年3月08日 14:05

Log Explorer:サードパーティのストレージを使用せずにセキュリティイベントを監視します

Security AnalyticsとLog Explorerを組み合わせることで、セキュリティチームはCloudflare内でネイティブにセキュリティ攻撃を分析、調査、監視でき、サードパーティのSIEMにログを転送する必要がなくなるため、解決までの時間を短縮し、お客様の総所有コストを削減できます...

2024年3月05日 14:02

セキュリティセンター内の保護されていないアセットを保護:最高情報セキュリティ責任者(CISO)のためのクイックビュー

本日、共通の課題であるインフラストラクチャ全体への包括的な展開を確実に行うために、セキュリティーセンター内に新しい機能セットを導入することを嬉しく思います。セキュリティ体勢を最適化する場所と方法を正確に把握することができます...