LinuxにおけるTCP listenソケットの検索の変化


カーネルTCPはSYNメッセージを受信すると,メッセージの宛先IPとPortに基づいてLISTEN状態にあるソケットにローカルマッチングして握手を行う.
4.17バージョン以前のlistenソケット検索
The current listener hashtable is hashed by port only. When a process is listening at many IP addresses with the same port (e.g.[IP1]:443, [IP2]:443... [IPN]:443), the inet[6]_lookup_listener() performance is degraded to a link list. It is prone to syn attack.
4.17リリース以前、TCPのlistener socketはportでhashされ、対応する競合チェーンテーブルに挿入されました.これにより、多くのlistenソケットが同じportをリスニングすると、チェーンテーブルが長くなり、3.9バージョンでREUSEPORTが導入された後、さらに深刻になります.
栗を挙げると、ホスト上に6つのlistenerが起動し、いずれも21ポートをリスニングしているため、同じチェーンテーブルに置かれている(sk_BREUSEPORTを使用している).ターゲットビット1.1.1.4:21のSYN接続要求がこの時点で受信されると、カーネルはlistenerを検索するとき、一致するsk_Dが見つかるまで、常に最初から最後までループします.
4.17バージョン:2つのhashtableで検索
4.17バージョンでは、listenソケットを組織するために新しいhashtable(lhash2)が追加されました.このlhash2は、port+addrをkeyとしてhashを行いましたが、portを押してhashを行ったhashtableは変更されませんでした.すなわち、同じlistenソケットは2つのhashtableに同時に配置されます(例外的に、バインドされたローカルアドレスが0.0.0.0の場合、元のhashtableにのみ配置されます).lhash2は、keyとしてaddrを増加させ、hashのランダム性を増加させる.また、上記の例を例にとると、この場合、従来のsk_A~Cが他の衝突チェーンにhashされる可能性があり、もちろん同時に、他の衝突チェーン上のsk_Eがhashからlhash2[0]という衝突チェーンにhashされる可能性もある.
したがってlistenソケットの検索では、カーネルはSYNメッセージのport+addrに基づいて、条件を満たすソケットが2つのhashtableに属するチェーンテーブルを算出し、この2つのチェーンテーブルの長さを比較し、1 stチェーンテーブルの長さが2 ndチェーンテーブルの長さ以下であれば、元の方法で1 stチェーンテーブルで検索し、そうでなければ2 ndチェーンテーブルで検索します.
                     struct inet_hashinfo *hashinfo,
                     struct sk_buff *skb, int doff,
@@ -217,10 +306,42 @@ struct sock *__inet_lookup_listener(struct net *net,
     unsigned int hash = inet_lhashfn(net, hnum);
     struct inet_listen_hashbucket *ilb = &hashinfo->listening_hash[hash];
     bool exact_dif = inet_exact_dif_match(net, skb);
+    struct inet_listen_hashbucket *ilb2;
     struct sock *sk, *result = NULL;
     int score, hiscore = 0;
+    unsigned int hash2;
     u32 phash = 0;
 
+    if (ilb->count <= 10 || !hashinfo->lhash2)
+        goto port_lookup;
+
+    /* Too many sk in the ilb bucket (which is hashed by port alone).
+     * Try lhash2 (which is hashed by port and addr) instead.
+     */
+
+    hash2 = ipv4_portaddr_hash(net, daddr, hnum);
+    ilb2 = inet_lhash2_bucket(hashinfo, hash2);
+    if (ilb2->count > ilb->count)
+        goto port_lookup;
+
+    result = inet_lhash2_lookup(net, ilb2, skb, doff,
+                    saddr, sport, daddr, hnum,
+                    dif, sdif);
+    if (result)
+        return result;
+
+    /* Lookup lhash2 with INADDR_ANY */
+
+    hash2 = ipv4_portaddr_hash(net, htonl(INADDR_ANY), hnum);
+    ilb2 = inet_lhash2_bucket(hashinfo, hash2);
+    if (ilb2->count > ilb->count)
+        goto port_lookup;
+
+    return inet_lhash2_lookup(net, ilb2, skb, doff,
+                  saddr, sport, daddr, hnum,
+                  dif, sdif);
+
+port_lookup:
     sk_for_each_rcu(sk, &ilb->head) {
         score = compute_score(sk, net, hnum, daddr,
                       dif, sdif, exact_dif);

5.0バージョン:2 nd hashtableでのみ検索
カーネルは5.0バージョンで、2 nd hashtableのみで検索するように変更されました.このように修正された理由は、従来のルックアップ方式において、1 st hashtableでルックアップを選択した場合、共通アドレス(0.0.0.0)と特定のアドレス(例えば1.1.1.1)が同じPortをリスニングした場合に、逆に共通アドレスのlistenerに一致するという問題が発生する可能性があるからである.これは実は4.17バージョンの鍋ではなく、3.9バージョンでSO_PORTREUSEを導入してすでに存在しています!
何が起こっているのか見てみましょう.SO_REUSEPORTが設定されたsk_Ask_Bは同時に21ポートをリスニングし、sk_Aが後起動であればチェーンヘッダーに追加され、1.1.1.2:21のメッセージを受信すると、カーネルはsk_Aが一致していることを発見し、より正確なsk_Bに一致しようとしません.これは明らかによくありません.SO_REUSEPORTがカーネルに入る前に、カーネルはチェーンテーブル全体を遍歴し、各ソケットの整合度を点数化することを知っておく必要があります(compute_score).
5.0バージョンは2 nd hashtableのみで検索するように変更され、compute_scoreの実装形態が変更され、リスニングアドレスがメッセージの宛先アドレスと異なる場合、直接一致を計算できません.以前は、このチェックを直接通過することができました.
検索方法の変更:
struct sock *__inet_lookup_listener(struct net *net,
                     const __be32 daddr, const unsigned short hnum,
                     const int dif, const int sdif)
 {
-    unsigned int hash = inet_lhashfn(net, hnum);
-    struct inet_listen_hashbucket *ilb = &hashinfo->listening_hash[hash];
-    bool exact_dif = inet_exact_dif_match(net, skb);
     struct inet_listen_hashbucket *ilb2;
-    struct sock *sk, *result = NULL;
-    int score, hiscore = 0;
+    struct sock *result = NULL;
     unsigned int hash2;
-    u32 phash = 0;
-
-    if (ilb->count <= 10 || !hashinfo->lhash2)
-        goto port_lookup;
-
-    /* Too many sk in the ilb bucket (which is hashed by port alone).
-     * Try lhash2 (which is hashed by port and addr) instead.
-     */
 
     hash2 = ipv4_portaddr_hash(net, daddr, hnum);
     ilb2 = inet_lhash2_bucket(hashinfo, hash2);
-    if (ilb2->count > ilb->count)
-        goto port_lookup;
 
     result = inet_lhash2_lookup(net, ilb2, skb, doff,
                     saddr, sport, daddr, hnum,
@@ -335,34 +313,12 @@ struct sock *__inet_lookup_listener(struct net *net,
         goto done;
 
     /* Lookup lhash2 with INADDR_ANY */
-
     hash2 = ipv4_portaddr_hash(net, htonl(INADDR_ANY), hnum);
     ilb2 = inet_lhash2_bucket(hashinfo, hash2);
-    if (ilb2->count > ilb->count)
-        goto port_lookup;
 
     result = inet_lhash2_lookup(net, ilb2, skb, doff,
-                    saddr, sport, daddr, hnum,
+                    saddr, sport, htonl(INADDR_ANY), hnum,
                     dif, sdif);
-    goto done;
-
-port_lookup:
-    sk_for_each_rcu(sk, &ilb->head) {
-        score = compute_score(sk, net, hnum, daddr,
-                      dif, sdif, exact_dif);
-        if (score > hiscore) {
-            if (sk->sk_reuseport) {
-                phash = inet_ehashfn(net, daddr, hnum,
-                             saddr, sport);
-                result = reuseport_select_sock(sk, phash,
-                                   skb, doff);
-                if (result)
-                    goto done;
-            }
-            result = sk;
-            hiscore = score;
-        }
-    }

採点部分の修正
@@ -234,24 +234,16 @@ static inline int compute_score(struct sock *sk, struct net *net,
                 const int dif, const int sdif, bool exact_dif)
 {
     int score = -1;
-    struct inet_sock *inet = inet_sk(sk);
-    bool dev_match;
 
-    if (net_eq(sock_net(sk), net) && inet->inet_num == hnum &&
+    if (net_eq(sock_net(sk), net) && sk->sk_num == hnum &&
             !ipv6_only_sock(sk)) {
-        __be32 rcv_saddr = inet->inet_rcv_saddr;
-        score = sk->sk_family == PF_INET ? 2 : 1;
-        if (rcv_saddr) {
-            if (rcv_saddr != daddr)
-                return -1;
-            score += 4;
-        }
-        dev_match = inet_sk_bound_dev_eq(net, sk->sk_bound_dev_if,
-                         dif, sdif);
-        if (!dev_match)
+        if (sk->sk_rcv_saddr != daddr)
+            return -1;
+
+        if (!inet_sk_bound_dev_eq(net, sk->sk_bound_dev_if, dif, sdif))
             return -1;
-        score += 4;
 
+        score = sk->sk_family == PF_INET ? 2 : 1;
         if (sk->sk_incoming_cpu == raw_smp_processor_id())
             score++;
     }

付録:完全パッチ
inet: Add a 2nd listener hashtable (port+addr) inet_connection_sock.hinet: Add a 2nd listener hashtable (port+addr) inet_hashtables.hinet: Add a 2nd listener hashtable (port+addr) inet_hashtables.cnet: tcp: prefer listeners bound to an address inet_hashtables.c