Linux SYNパケット取りこぼし (2)

前回の続き。
パケット自体を零さずに処理に入った後にSYNを落とすのは以下3パターン。

  • syncookie無効時にsynのbacklog(tcp_max_syn_backlog)が溢れている
  • listenのbacklogが溢れている(3way-handshake完了後のaccept待ち接続)
  • net.ipv4.tcp_tw_recycleの制限に抵触

で、今回問題になっていたのは最後のtcp_tw_recycleへの抵触だった。


現象として発生しうるのは、以下の条件をすべて満たす場合

  • サーバ側でnet.ipv4.tcp_tw_recycleが有効
  • TCPタイムスタンプオプションを使用
  • 同一IPからの接続でセッションを跨ぐとセットされるTCPタイムスタンプの値が戻る場合がある


最後の条件が微妙だが、TCPタイムスタンプの値としてセットされる値は起動時を
起算時にしていたりと実装によって初期値は異なり、複数台のホストの場合は
同一OSで全く同一タイミングで起動したりしない限りまず一致しない。
そのため、TCPタイムスタンプオプションのフィールドを書き換えないNATや
ロードバランサ経由の接続の場合、セッションを跨ぐとセットされる値の増加が
保証されない。


で、該当箇所の判定処理

@net/ipv4/tcp_ipv4.c
                if (tp.saw_tstamp &&
                    sysctl_tcp_tw_recycle &&
                    (dst = tcp_v4_route_req(sk, req)) != NULL &&
                    (peer = rt_get_peer((struct rtable*)dst)) != NULL &&
                    peer->v4daddr == saddr) {
                        if (xtime.tv_sec < peer->tcp_ts_stamp + TCP_PAWS_MSL &&
                            (s32)(peer->tcp_ts - req->ts_recent) > TCP_PAWS_WINDOW) {
                                NET_INC_STATS_BH(PAWSPassiveRejected);
                                dst_release(dst);
                                goto drop_and_free;
                        }
                }
@include/net/tcp.h
#define TCP_PAWS_MSL    60              /* Per-host timestamps are invalidated
                                         * after this time. It should be equal
                                         * (or greater than) TCP_TIMEWAIT_LEN
                                         * to provide reliability equal to one
                                         * provided by timewait state.
                                         */
#define TCP_PAWS_WINDOW 1               /* Replay window for per-host
                                         * timestamps. It must be less than
                                         * minimal timewait lifetime.
                                         */

判定部分の値は以下の通り

変数 説明
xtime.tv_sec 現在の時刻(サーバ上)
peer->tcp_ts_stamp 前回パケットを受けた時刻(サーバ上)
peer->tcp_ts 前回のセッションにおける最終パケットのTCPタイムスタンプ
req->ts_recent 今回のSYNパケットのTCPタイムスタンプ

この処理により、同一IPから60秒以内に前回のTCPセッションの最終パケットより
前のTCPタイムスタンプを持ったSYNパケットが来ると該当パケットを落としてしまう。


例えば、一週間前から起動しているホストAと一時間前に起動したホストBが、
ロードバランサの下に収容されており、そこから同一IPに接続した場合
以下のような事が起き得る。

  • AからのSYN受信、コネクション確立
  • Aからのコネクション切断
  • BからのSYN受信、上記制限に引っかかりSYNを無視する (TCPタイムスタンプがAより小さい為)
  • ACKが返らないためBからSYNがひたすら再送される
  • TCP_PAWS_MSL(60秒)経過
  • BからのSYN受信、ACKを返してやっとコネクション確立


接続しに来たクライアントからは、サーバ側の問題で接続確立出来ないように見え、
サーバ側からはtcpdumpやPAWSPassiveRejectedカウンタを見ていなければ現象を判別
出来ずに、再送掛かっているだけで普通に繋がっている様に見えてしまっていた。
また、テストをしていても同一ホストからの接続では再現しないため原因箇所を
突き止めるのが結構厄介だった。


教訓
パラメータ弄るときはカーネルソースコードまできちんと読みましょう。