Cloudflareは、インターネットの未知の領域でのサーバー運用に関して豊富な経験があります。それでも、常にこの黒魔術の腕に磨きをかけています。まさにこのブログで、FIN-WAIT-2の理解バッファチューニングを受けるといったインターネットプロトコルの影の部分についてこれまで言及してきました。

1471936772_652043cb6c_b
CC BY 2.0 イメージ 作成者 Isaí Moreno

ただし、これまで十分注目されてこなかったテーマが一つあります。それがSYNフラッドです。当社はLinuxを使用しており、LinuxのSYNパケット処理は実に複雑です。今回はこれをテーマに取り上げます。

2列の物語

all-1

まず、TCPの「LISTENING」状態のバインドされた各ソケットには、2 つの別個のキュー(待ち行列)があることを理解しなければなりません。

  • SYN キュー
  • Accept キュー

文献ではこれらのキューには、「reqsk_queue」、「ACK backlog」、「listen backlog」、そしてときに「TCP backlog」などと様々な名称が使われていますが、混乱を避けるためにここでは前述の名前を使っていきます。

SYN キュー

SYNキューは、インバウンドSYN パケット[1](具体的には、struct inet_request_sock)を格納します。SYN+ACK パケットを送信し、タイムアウト時に再試行を行います。Linux では、再試行の回数は次のように構成されます。

$ sysctl net.ipv4.tcp_synack_retries
net.ipv4.tcp_synack_retries = 5

この切り替えについて文書に記載しています

tcp_synack_retries - INTEGER

	Number of times SYNACKs for a passive TCP connection attempt
	will be retransmitted. Should not be higher than 255. Default
	value is 5, which corresponds to 31 seconds till the last
	retransmission with the current initial RTO of 1second. With
	this the final timeout for a passive TCP connection will
	happen after 63 seconds.

SYN+ACKを送信した後、SYNキューはクライアントからの ACKパケット(3ウェイハンドシェイクの最後のパケット)を待ちます。受信したすべてのACKパケットはまず完全に確立された接続テーブルと比較され、一致すれば関連するSYNキュー内のデータと比較します。SYNキューとの比較では、カーネルは SYNキューから項目を削除し、完全な接続(具体的には、struct inet_sock)を作成し、Acceptキューに追加します。

Accept キュー

Acceptキューには、完全に確立された接続が含まれていて、アプリケーションはいつでも利用できます。プロセスが accept() を呼び出すと、ソケットはキューから取り出され、アプリケーションに渡されます。

これは LinuxでのSYNパケット処理のかなり簡素化した概要です。ソケットの切り替えがTCP_DEFER_ACCEPT[2]やTCP_FASTOPENの場合は若干異なった動作をします。

キューサイズの制限

Accept キューとSYNキューともに許容最大長は、アプリケーションによって listen(2) syscall に渡されるバックログパラメータから取得されます。たとえば、この場合Accept キューと SYN キューのサイズは1,024に設定されます。

listen(sfd, 1024)

注意:4.3以前のカーネルでは、SYN キューの長さのカウント法は異なっていました。

このSYNキュー上限は、以前 net.ipv4.tcp_max_syn_backlog トグルによって設定されていましたが、今は違います。最近では、net.core.somaxconn が両方のキューサイズを制限します。当社のサーバーでは、16kに設定しました。

$ sysctl net.core.somaxconn
net.core.somaxconn = 16384

完璧なバックログ値

これに基づき、理想的なbacklogパラメータ値は?という質問をするかもしれません。

答えは、ケースバイケースです。些細なTCP サーバーの大部分では、重要なことではありません。たとえば、バージョン 1.11 の前は、Golang はバックログ値のカスタマイズをサポートしていなかったことは周知のことです。ただし、この値を増やす正当な理由もあります。

  • 高性能のアプリケーションを使用する場合でも、外部からの接続速度が非常に大きい場合、インバウンドSYN キューは、さらに多くのスロットを必要とするかもしれません。
  • バックログ値は、SYNキューのサイズを制御します。これは結果として「フライト中のACKパケット」と受け取られることがあります。クライアントへの平均往復時間が長くなるほど、使用されるスロットも多くなります。サーバーから数百ミリ秒離れた場所に多数のクライアントがある場合に、バックログ値を増やすのは理にかなっていることです。
  • TCP_DEFER_ACCEPTオプションを使用すると、ソケットはSYN-RECV 状態が長く続き、キューの制限に寄与します。

バックログのオーバーシュートも良くありません。

  • SYNキューの各スロットは、一定のメモリを使用します。SYNフラッドの間は、攻撃パケットの格納にリソースを消耗することは意味をなしません。SYNキュー内の各struct inet_request_sock エントリーは、カーネル 4.14の256バイトのメモリを使用します。

Linux上のSYNキューをのぞくために、ssコマンドを使用してSYN-RECVソケットを探すことができます。たとえば、Cloudflareのサーバーの1つで、tcp/80 SYNキューで使用されている119スロットとtcp/443で使われている78スロットが表示されます。

$ ss -n state syn-recv sport = :80 | wc -l
119
$ ss -n state syn-recv sport = :443 | wc -l
78

同様のデータが、当社のオーバースペックのSystemTapスクリプトではresq.stpと表示されます。

低速アプリケーション

full-accept-1

アプリケーションが accept() の呼び出しに十分な速度で対応できない場合はどうなりますか?

これが、マジカルな瞬間です!Acceptキューがいっぱいになる(バックログ+1のサイズ)と、次のようになります。

  • SYNキューへのインバウンドSYNパケットがドロップされます。
  • SYN キューへのインバウンドACKパケットがドロップされます。
  • TcpExtListenOverflows / LINUX_MIB_LISTENOVERFLOWS カウンターがインクリメントされます。
  • TcpExtListenDrops/ LINUX_MIB_LISTENDROPS カウンターがインクリメントされます。

インバウンドパケットをドロップすることに関する強い論理的根拠は、プッシュバックメカニズムです。相手は、遅かれ早かれSYNパケットかACKパケットを再送信しますし、その時点では、遅いアプリケーションが回復していると期待されます。

これは、ほぼすべてのサーバーにとって望ましい動作です。補足:グローバルな net.ipv4.tcp_abort_on_overflowトグルで調整はできますが、触らない方が良いです。

サーバーが多数の受信接続を処理する必要があり、accept () スループットに苦労している場合は、Nginx チューニング/Epoll 作業の配分の記事と有用な SystemTapスクリプトを説明したフォローアップ記事をご参照ください。

nstat カウンターを調ベると、Acceptキューのオーバーフロー統計を追跡することができます。

$ nstat -az TcpExtListenDrops
TcpExtListenDrops     49199     0.0

これはグローバルカウンターです。理想的ではありません。時々、増加しているにもかかわらず、すべてのアプリケーションは健全なようでした。最初のステップは、常にAcceptキューのサイズを ss でプリントすることです。

$ ss -plnt sport = :6443|cat
State   Recv-Q Send-Q  Local Address:Port  Peer Address:Port
LISTEN  0      1024                *:6443    

Recv-Q の列はAcceptキュー内のソケット数を示し、Send-Q  バックログパラメーターを示します。この場合、accept()される未処理のソケットはありませんが、それでもListenDrops カウンターが増加していました。

当社のアプリケーションがほんの一瞬だけ動かなかったことが判明しました。これは、非常に短い間、Acceptキューをオーバーフローさせるのに十分でした。しばらくすると回復します。このようなケースは、ssでデバッグするのが難しいので、補助手段として、 acceptq.stpSystemTap スクリプトを書きました。カーネルにフックし、ドロップされているSYNパケットをプリントします。

$ sudo stap -v acceptq.stp
time (us)        acceptq qmax  local addr    remote_addr
1495634198449075  1025   1024  0.0.0.0:6443  10.0.1.92:28585
1495634198449253  1025   1024  0.0.0.0:6443  10.0.1.92:50500
1495634198450062  1025   1024  0.0.0.0:6443  10.0.1.92:65434
...

ここでは、どのSYNパケットがListenDropsの影響を受けたかを正確にみることができます。このスクリプトでは、どのアプリケーションが接続を切断しているかを理解することは些末な情報です。

3713965419_20388fb368_b
CC BY 2.0 イメージ作成者 internets_dairy

SYNフラッド

full-syn-1

Acceptキューをオーバーフローできる場合は、SYNキューもオーバーフローできるはずです。その場合、どうなりますか?

これがSYNフラッド攻撃です。過去には、なりすましSYNパケットによるSYNキューへのフラッド攻撃は、深刻な問題でした。1996年以前は、SYNキューをいっぱいにするだけで、帯域幅の少ないTCPサーバーのほとんどすべてにおいて、サービス拒否ができました。

解決策は、SYN Cookieです。SYN Cookieは、実際にインバウンドSYNを保存してシステムメモリを浪費することをせずにSYN+ACKをステートレスに生成できるようにする構成されています。SYN Cookieは正当なトラフィックを中断させません。相手が実在する場合は、反映されたシーケンス番号を含む有効なACKパケットで応答し、これは暗号的に検証可能です。

デフォルトで、SYNキューがいっぱいになったソケットに対して、SYN Cookieは必要に応じて有効になります。Linuxは SYN Cookieのカウンターをいくつか更新します。SYN Cookieが送信される場合:

  • TcpExtTCPReqQFullDoCookies / LINUX_MIB_TCPREQQFULLDOCOOKIES がインクリメントされます。
  • TcpExtSyncookiesSent / LINUX_MIB_SYNCOOKIESSENT がインクリメントされます。
  • Linuxは TcpExtListenDrops をインクリメントするために使用されていましたが、カーネル 4.7以降は使用されません

インバウンドACK が SYN Cookieを使用して SYN キューに向かう場合、

  • 暗号化の検証が成功すると、TcpExtSyncookiesRecv / LINUX_MIB_SYNCOOKIESRECV がインクリメントされます。
  • 暗号化が失敗すると、TcpExtSyncookiesFailed / LINUX_MIB_SYNCOOKIESFAILED がインクリメントされます。

sysctl net.ipv4.tcp_syncookiesは、SYN Cookieを無効にしたり、強制的に有効にしたりできます。デフォルトはよくできていますから、変更不要です。

SYN Cookieという魔法は効果がありますが、欠点がないわけではありません。主要な問題は、SYN Cookieに保存できるデータが非常に少ないということです。具体的には、ACKに返されるのが、シーケンス番号の32ビットのみです。これらのビットは次のように使用されます。

+----------+--------+-------------------+

|  6 bits  | 2 bits |     24 bits       |

| t mod 32 |  MSS   | hash(ip, port, t) |

+----------+--------+-------------------+

4つの異なる値に切り下げたMSS 設定では、Linuxは相手側のオプションのTCPパラメータを認識しません。タイムスタンプ、ECN、選択的ACK、ウィンドウスケーリングに関する情報が失われ、TCP セッションのパフォーマンスが低下するかもしれません。

幸いなことに、Linuxには回避策があります。TCPタイムスタンプが有効になっている場合、カーネルはタイムスタンプフィールドで 32 ビットの別のスロットを再利用できます。これには次のものが含まれます。

+-----------+-------+-------+--------+

|  26 bits  | 1 bit | 1 bit | 4 bits |

| Timestamp |  ECN  | SACK  | WScale |

+-----------+-------+-------+--------+

TCPタイムスタンプをデフォルトで有効にする必要があります。sysctlから確認できます。

$ sysctl net.ipv4.tcp_timestamps

net.ipv4.tcp_timestamps = 1

歴史的に、TCPタイムスタンプの有用性について多くの議論がありました。

  • 過去のタイムスタンプでは、サーバーの稼働時間が漏れました(それが重要かどうかは別の議論です)。これは8ヶ月前に修正済みです。
  • TCPタイムスタンプは、各パケット12バイトの少なくない帯域幅を使用します。
  • 特定の破損したハードウェアに役立つパケットチェックサムに追加のランダム性を追加することができます。
  • 前述のように、TCPタイムスタンプは、SYN Cookieが使用されている場合に TCP接続のパフォーマンスを向上させることができます。

現在Cloudflareでは、TCPタイムスタンプが無効になっています。

最後に、SYN Cookieが使用されると、TCP_SAVED_SYN、TCP_DEFER_ACCEPTやTCP_FAST_OPENといったクールな機能が動作しなくなります。

Cloudflare規模のSYNフラッド

Screen-Shot-2016-12-02-at-10.53.27-1

SYN Cookieは偉大な発明であり、より小規模のSYNフラッドの問題を解決します。ですが、Cloudflareでは、可能であれば利用を避けるようにしています。1 秒間に数千個の暗号検証可能な SYN+ACK パケットを送信することに問題はありませんが、1 秒あたり 2 億パケットを超える攻撃が見られます。この規模では、当社のSYN+ACK応答はインターネットにごみを散らかすだけで、まったくメリットがありません。

代わりに、ファイアウォール層に悪意のある SYNパケットをドロップしようと思っています。BPF にコンパイルされた p0f SYN フィンガープリントを使用します。p0f BPFコンパイラの紹介に関するこのブログ記事を参照してください。軽減策を検出して展開するために、当社は「Gatebot」と呼ぶ自動化システムを開発しました。Gatebotの紹介-ボットのおかげで、安心して夜寝られるの中で説明をしています。

進化する風景

この件についてのデータに関しては、(多少古いですが)2015年のAndreas Veithen執筆による優れた説明2013年のGerald W.Gordon執筆の包括的論文をお読みください。

Linux SYNパケット処理状況は常に進化しています。最近まで、SYN Cookieは低速で、これはカーネルの古いロックのためでした。これが4.4 では修正され、今はカーネルを使用して毎秒数百万のSYN Cookieを送信できるようになったため、ほとんどのユーザーにとって SYNフラッドの問題は事実上解決できています。適切なチューニングにより、正当な接続のパフォーマンスに影響を与えることなく、最も厄介なSYNフラッドでも軽減することができます。

アプリケーションのパフォーマンスも大きな注目を集めています。SO_ATTACH_REUSEPORT_EBPFのような最近のアイデアは、ネットワークスタックに全く新しいレベルのプログラマビリティを実現します。

停滞状態のオペレーティングシステムの世界を変える、イノベーションと新鮮な思考がネットワーキングスタックに注ぎ込まれるのを見るのは素晴らしいことです。

執筆に協力してくれたBinh Leに感謝します。


LinuxとNGINXの内部を扱うことに興味がありますか?ロンドン、オースティン、サンフランシスコの世界的に有名なチーム、ポーランドのワルシャワの精鋭部隊に、ぜひご参加ください。


  1. 単純化した説明です。厳密に言うと、SYNキューストアはまだSYNパケット自体の接続をまだ確立していません。TCP_SAVE_SYNでは、十分それに近くなりますが。
  2. もし TCP_DEFER_ACCEPTに慣れていない場合は、ぜひFreeBSDバージョンをお試しください。accept フィルター↩︎