<input id="0qass"><u id="0qass"></u></input>
  • <input id="0qass"><u id="0qass"></u></input>
  • <menu id="0qass"><u id="0qass"></u></menu>

    實踐和原則,哪個更重要?tcp syncookie的問題和解法

    測了一次tcp syncookie的抗D性能,發現了一件有趣的事情,周末寫一篇隨筆出來。

    請看下面的時序:
    在這里插入圖片描述

    簡單講就是在syncookie被觸發的時候,客戶端可能會被靜默丟掉最多3個字節,所謂靜默就是客戶端認為這些字節被收到了(因為它們被確認了),然而服務端真真切切沒有收到。

    關于這個POC也非常簡單:

    //$ cat poc.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <pthread.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    
    void *serverfunc(void *arg)
    {
            int sd = -1;
            int csd = -1;
            struct sockaddr_in servaddr, cliaddr;
            int len = sizeof(cliaddr);
    
            sd = socket(AF_INET, SOCK_STREAM, 0);
            servaddr.sin_family = AF_INET;
            servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
            servaddr.sin_port = htons(1234);
            bind(sd, (struct sockaddr *)&servaddr, sizeof(servaddr));
            listen(sd, 1);
    
            while (1) {
                    char buf[2];
                    int ret;
                    csd = accept(sd, (struct sockaddr *)&cliaddr, &len);
                    memset(buf, 0, 2);
                    ret = recv(csd, buf, 1, 0);
                    // but unexpected char is 'b'
                    if (ret && strncmp(buf, "a", 1)) {
                            printf("unexpected:%s\n", buf);
                            close(csd);
                            exit(0);
                    }
                    close(csd);
            }
    }
    
    void *connectfunc(void *arg)
    {
            struct sockaddr_in addr;
            int sd;
            int i;
    
            for (i = 0; i < 500; i++) {
                    sd = socket(AF_INET, SOCK_STREAM, 0);
                    addr.sin_family = AF_INET;
                    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
                    addr.sin_port = htons(1234);
    
                    connect(sd, (struct sockaddr *)&addr, sizeof(addr));
    
                    send(sd, "a", 1, 0); // expected char is 'a'
                    send(sd, "b", 1, 0);
                    close(sd);
            }
            return NULL;
    }
    
    int main(int argc, char *argv[])
    {
            int i;
            pthread_t id;
    
            pthread_create(&id, NULL, serverfunc, NULL);
            sleep(1);
            for (i = 0; i < 500; i++) {
                    pthread_create(&id, NULL, connectfunc, NULL);
            }
            sleep(5);
    }
    
    //$ sudo gcc poc.c -lpthread
    //$ sudo sysctl -w net.ipv4.tcp_syncookies=1
    //$ sudo sysctl -w net.ipv4.tcp_max_syn_backlog=2 # just for triggering problems easily.
    //$ sudo ./a.out # please try as many times.
    

    我是怎么發現這個問題的呢?也比較有趣。

    一開始我是想替換syncookie的hash算法的,我知道以前這個是SHA-1,性能比較低,所以我們自己在3.10內核上換成了jhash,現在我們用5.4內核,我又手癢了,也想換成jhash,在換之前review代碼的時候發現已經變成siphash了,所以我就想測下siphash和jhash的性能對比,于是我把syncookie這塊邏輯整個拷貝到了用戶態程序:

    #include <stdio.h>
    #include <stdlib.h>
    #include <linux/types.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    
    #define COOKIEBITS 24	/* Upper bits store count */
    #define COOKIEMASK (((__u32)1 << COOKIEBITS) - 1)
    #define MAX_SYNCOOKIE_AGE	2
    
    static __u32 cookie_hash(__be32 saddr, __be32 daddr, __be16 sport, __be16 dport,
    		       __u32 count, int c)
    {
    	// jhash or siphash
    	return saddr + daddr + sport + dport + count + c;
    }
    
    static __u32 secure_tcp_syn_cookie(__be32 saddr, __be32 daddr, __be16 sport,
    				   __be16 dport, __u32 sseq, __u32 data, __u32 count)
    {
    	/*
    	 * Compute the secure sequence number.
    	 * The output should be:
    	 *   HASH(sec1,saddr,sport,daddr,dport,sec1) + sseq + (count * 2^24)
    	 *      + (HASH(sec2,saddr,sport,daddr,dport,count,sec2) % 2^24).
    	 * Where sseq is their sequence number and count increases every
    	 * minute by 1.
    	 * As an extra hack, we add a small "data" value that encodes the
    	 * MSS into the second hash value.
    	 */
    	//__u32 count = tcp_cookie_time();
    	return (cookie_hash(saddr, daddr, sport, dport, 0, 0) +
    		sseq + (count << COOKIEBITS) +
    		((cookie_hash(saddr, daddr, sport, dport, count, 1) + data)
    		 & COOKIEMASK));
    }
    
    static __u32 check_tcp_syn_cookie(__u32 cookie, __be32 saddr, __be32 daddr,
    				  __be16 sport, __be16 dport, __u32 sseq, __u32 count)
    {
    	__u32 diff;
    
    	/* Strip away the layers from the cookie */
    	cookie -= cookie_hash(saddr, daddr, sport, dport, 0, 0) + sseq;
    
    	/* Cookie is now reduced to (count * 2^24) ^ (hash % 2^24) */
    	diff = (count - (cookie >> COOKIEBITS)) & ((__u32) -1 >> COOKIEBITS);
    	if (diff >= MAX_SYNCOOKIE_AGE)
    		return (__u32)-1;
    
    	return (cookie -
    		cookie_hash(saddr, daddr, sport, dport, count - diff, 1))
    		& COOKIEMASK;	/* Leaving the data behind */
    }
    
    int main(int argc, char **argv)
    {
    	__u32 saddr, daddr;
    	__be16 sport, dport;
    	__u32 seq;
    	__u32 count;
    	__u32 mssid;
    	struct in_addr in_saddr, in_daddr;
    	int drop_count;
    	int cookie;
    	int result;
    
    
    	if (argc != 9) {
    		printf("./a.out saddr daddr sport dport seq count mssid drop_count(<=3)\n");
    		exit(1);
    	}
    
    	saddr = inet_addr(argv[1]);
    	in_saddr.s_addr = saddr;
    	daddr = inet_addr(argv[2]);
    	in_daddr.s_addr = daddr;
    	sport = atoi(argv[3]);
    	dport = atoi(argv[4]);
    	seq = atoi(argv[5]);
    	count = atoi(argv[6]);
    	mssid = atoi(argv[7]);
    	drop_count = atoi(argv[8]);
    
    	printf("syn:%s:%d-->%s:%d with mssid %d\n",
    				inet_ntoa(in_saddr),
    				sport,
    				inet_ntoa(in_daddr),
    	     			dport,
    	     			mssid);
    	cookie = secure_tcp_syn_cookie(saddr, daddr, sport, dport, seq, mssid, count);
    	printf("cookie:%d\n", cookie);
    	result = check_tcp_syn_cookie(cookie, saddr, daddr, sport, dport, seq + drop_count, count);
    	printf("result:%d\n", result);
    }
    

    當mssid是3的時候,seq可以越過最多3個字節。按照syncookie算法,mssid和seq都是直接加法拼接到cookie上去的,如果seq增加了1,2或者3字節,那么mssid相應減去1,2或者3就是了,而如果mss是1460(大概率是這個),它的index是3,那么當seq越過3個字節后,mssid就成了0,依然是符合的,這就是問題所在。

    見招拆招的解法很簡單,把seq也加入到hash運算里就是了:

    -	return (cookie_hash(saddr, daddr, sport, dport, 0, 0) +
    +	return (cookie_hash(saddr, daddr, sport, dport, sseq, 0) +
    ...
    -	cookie -= cookie_hash(saddr, daddr, sport, dport, 0, 0) + sseq;
    +	cookie -= cookie_hash(saddr, daddr, sport, dport, sseq, 0) + sseq;
    

    如此一來,只有保序到達的才能成功建立連接,即便是客戶端發出的前3個字節沒有丟失但是亂序了,也無法建立連接,服務端收到任何seq錯誤的報文,均會RST掉連接。

    這個解法有問題嗎?跟社區的maintainer埃里克聊,埃里克站在practice的視角,認為這是用一個小代價換取了一個小收益,雖然靜默丟字節不存在了,但也會誤傷僅僅由于亂序而試圖創建連接的session。所以字節丟失的問題應該由高層協議校驗。

    可我仔細一想,這不對呀,RST是一個明確的信息,客戶端收到一個很明確的信息并沒有什么問題,它知道自己建連失敗了,然后它可能會重試,或者走人,但如果客戶端發出了3個字節,并且服務端還都確認了,按照TCP的語義,這3個字節就是確實被服務端接收了的,然而事實上服務端并沒有接收了,this could cause confusion。

    字節丟失當然能由高層協議校驗,事實上TCP連保序重傳都不用做,這些都可以通過高層協議完成。事實上,這里無關HTTPS,SSL,TLS,這里和安全攻擊無關,這里僅僅是在說, 在syncookie觸發的時候,該不該兌現TCP的承諾。

    我認為任何時候都應該兌現承諾,可以明確RST掉session,但不能有歧義。

    在想到將seq參與hash運算解決這個問題之前,還有另一個解法,事實上是一個緩解方法。僅僅針對mss為1460字節的連接防靜默丟棄:

    1460 is the single most frequently announced mss value (30 to 46% depending on monitor location).

    修改很簡單, 只需要把msstab倒序就好了 。因為我們只需要讓1460在msstab中的index是0就可以了,當然如果syn報文中的mss是536,那還是可能丟失最多3個字節的。但還是會有reorder后被RST的問題。

    So the question is, when syncookie is triggered, which is more important, the practice or the principle?

    埃里克說用sysctl來控制會比較好,但我還是覺得,這是一個feature嗎?這并不是非此即彼的,在我看來運維并沒有能力去控制這個開關。

    反轉到另一個話題,如果syncookie被觸發了,抗D的責任,在內核協議棧嗎?

    我傾向于syncookie只是一個告警機制,而不是常態,一旦syncookie被觸發,運維應該第一時間獲取信息,然后采取動作,而不是空留內核自己在那里抗D,基于此,我認為hash算法的安全性并不重要,jhash完全可以勝任,SHA-1,MD5這種完全就沒有必要,至于siphash,和jhash還是沒法比。

    有篇文章希望在內核推廣siphash:
    https://lwn.net/Articles/711167/

    很明顯,事情過頭了,jhash目前并沒有看出有什么大的問題,僅僅是因為siphash 被證明更安全 就要被替換,那效率呢?好吧,談到效率,halfsiphash出來了,總之都是買賣,直接jhash不好嗎?想想也是夠了。大衛米勒的態度多少顯得有點被迫。

    就像maintainer埃里克說的那樣,用一點小代價換一點小收益這種買賣在內核社區還少嗎? 能不能做成這筆買賣的核心在于看擺攤的是誰。 有點意思。換個人擺攤,買賣就做成了。


    浙江溫州皮鞋濕,下雨進水不會胖。

    相關推薦
    ??2020 CSDN 皮膚主題: 編程工作室 設計師:CSDN官方博客 返回首頁
    多乐彩