測了一次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埃里克說的那樣,用一點小代價換一點小收益這種買賣在內核社區還少嗎? 能不能做成這筆買賣的核心在于看擺攤的是誰, 有點意思,換個人擺攤,買賣就做成了,
浙江溫州皮鞋濕,下雨進水不會胖,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/278017.html
標籤:AI
