内核TCP收到SYN报文后,会根据报文的目的IP和Port在本地匹配处于LISTEN状态的socket进行握手过程。4.17版本之前的listensocket查找Thecurrentlistenerhashtableishashedonlybyport。当一个进程正在侦听具有相同端口的多个IP地址时(例如[IP1]:443、[IP2]:443...[IPN]:443),inet[6]_lookup_listener()性能会降级为一个链接列表。它很容易受到同步攻击。在4.17版本之前,TCPlistenersocket是按端口进行哈希处理,然后插入到相应的冲突列表中。这使得如果多个监听套接字监听同一个端口,它会使链表变长。这种情况在3.9版本引入REUSEPORT后更加严重。比如宿主机上启动了6个listener,它们都监听21端口,所以放在同一个链表上(这里sk_B用的是REUSEPORT)。如果此时收到目标位为1.1.1.4:21的SYN连接请求,内核在查找listenr时,会一直从头到尾遍历,直到找到匹配的sk_D。版本4.17:在两个哈希表中搜索版本4.17添加了一个新的哈希表(lhash2)来组织侦听套接字。这个lhash2使用port+addr作为key进行哈希,而原来的hashtable按端口进行哈希不变。改变。也就是说,同一个listensocket会同时放到两个hashtable中(例外是如果它绑定的本地地址是0.0.0.0,只会放到原来的hashtable中)lhash2添加addr为key,增加了hash的随机性。还是以上面的例子为例。这时,原来的sk_A~C可能会散列到其他冲突链上。当然,同时也有可能将原来在其他冲突链上的sk_E散列到lhash2[0]冲突链上。因此,在寻找listensocket时,内核会根据SYN报文中的port+addr,计算出满足条件的socket在两个hashtable中应该属于的链表,然后比较两个链表的长度列出。如果第一个链表的长度不长或小于第二个链表的长度,则按原路在第一个链表中查找,否则,在第二个链表中查找。structinet_hashinfo*hashinfo,structsk_buff*skb,intdoff,@@-217,10+306,42@@structsock*__inet_lookup_listener(structnet*net,unsignedinthash=inet_lhashfn(net,hnum);structinet_listen_hashbucket*ilb=&hashinfo->listening_hash[hash];boolexact_dif=inet_exact_dif_match(net,skb);+structinet_listen_hashbucket*ilb2;structsock*sk,*result=NULL;intscore,hiscore=0;+unsignedinthash2;u32phash=0;+if(ilb->count<=10||!hashinfo->lhash2)+gotoport_lookup;++/*ilbbucket中的sk太多(单独按端口散列)。+*尝试lhash2(这改为按端口和地址进行哈希处理。+*/++hash2=ipv4_portaddr_hash(net,daddr,hnum);+ilb2=inet_lhash2_bucket(hashinfo,hash2);+if(ilb2->count>ilb->count)+gotoport_lookup;++结果=inet_lhash2_lookup(net,ilb2,skb,doff,+saddr,sport,daddr,hnum,+dif,sdif);+if(result)+returnresult;++/*使用INADDR_ANY查找lhash2*/++hash2=ipv4_portaddr_hash(net,htonl(INADDR_ANY),hnum);+ilb2=inet_lhash2_bucket(hashinfo,hash2);+if(ilb2->count>ilb->count)+gotoport_lookup;++returninet_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版本:在5.0版本中只在第2个hashtable中查找内核之所以这样修改是因为按照原来的查找方式,如果选择在第1个hashtable中查找,可能会出现在通配符地址(0.0.0.0)而具体地址(如1.1.1.1)当都监听同一个Port时,改为用通配符地址匹配监听器的问题。其实这不是4.17版本的锅,而是在3.9版本引入SO_PORTREUSE后就有了!让我们看看发生了什么:sk_A和sk_B与SO_REUSEPORT被设置为同时监听端口21。如果后面启动sk_A,就会把它加到链表的头部,这样内核在收到1.1.1.2:21消息的时候,会发现sk_A已经匹配上了,就不会再去尝试匹配了准确的sk_B!这显然不好。要知道在SO_REUSEPORT进入内核之前,内核会遍历整个链表,对每个socket的匹配度(compute_score)打分。5.0版本修改为只在第2个hashtable中查找,修改了compute_score的实现。如果监听地址与数据包的目的地址不相同,则直接计为匹配失败。以前,通配符地址可以直接通过此检查。查找方法的修改:structsock*__inet_lookup_listener(structnet*net,const__be32daddr,constunsignedshorthnum,constintdif,constintsdif){-unsignedinthash=inet_lhashfn(net,hnum);-structinet_listen_hashbucket*ilb=&hashinfo->listening_hash[hash];-boolexact_dif=inet_exact_dif_match(net,skb);structinet_listen_hashbucket*ilb2;-structsock*sk,*result=NULL;-intscore,hiscore=0;+structsock*result=NULL;unsignedinthash2;-u32phash=0;--if(ilb->count<=10||!hashinfo->lhash2)-gotoport_lookup;--/*ilb桶中的sk太多(按端口散列)单独)。-*改为尝试lhash2(按端口和地址散列)。-*/hash2=ipv4_portaddr_hash(net,daddr,hnum);ilb2=inet_lhash2_bucket(hashinfo,hash2);-if(ilb2->count>ilb->count)-gotoport_lookup;结果=inet_lhash2_lookup(net,ilb2,skb,doff,saddr,sport,daddr,hnum,@@-335,34+313,12@@structsock*__inet_lookup_listener(structnet*net,gotodone;/*使用INADDR_ANY查找lhash2*/-hash2=ipv4_portaddr_hash(net,htonl(INADDR_ANY),hnum);ilb2=inet_lhash2_bucket(hashinfo,hash2);-if(ilb2->count>ilb->count)-gotoport_lookup;result=inet_lhash2_lookup(net,ilb2,skb,doff,-saddr,sport,daddr,hnum,+saddr,sport,htonl(INADDR_ANY),hnum,dif,sdif);-gotodone;--端口查找:-sk_for_each_rcu(sk,&ilb->head){-分数=compute_score(sk,net,hnum,daddr,-dif,sdif,exact_dif);-if(score>hiscore){-if(sk->sk_reuseport){-phash=inet_ehashfn(net,daddr,hnum,-saddr,运动);-结果=reuseport_select_sock(sk,phash,-skb,doff);-if(result)-gotodone;-}-result=sk;-hiscore=score;-}-}打分部的修改@@-234,24+234,16@@静态内联intcompute_score(structsock*sk,structnet*net,constintdif,constintsdif,boolexact_dif){intscore=-1;-structinet_sock*inet=inet_sk(sk);-booldev_match;-如果(net_eq(sock_net(sk),net)&&inet->inet_num==hnum&&+如果(net_eq(sock_net(sk),net)&&sk->sk_num==hnum&&!ipv6_only_sock(sk)){-__be32rcv_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))返回-1;-得分+=4;+分数=sk->sk_family==PF_INET?2:1;如果(sk->sk_incoming_cpu==raw_smp_processor_id())分数++;}附件:完整补丁inet:Adda2ndlistenerhashtable(port+addr)inet_connection_sock.hinet:Adda2ndlistenerhashtable(port+addr)inet_hashtables.hinet:Adda2ndlistenerhashtable(port+addr)inet_hashtables.cnet:tcp:更喜欢绑定到地址的侦听器inet_hashtables.c
