实践和原则,哪个更重要?tcp syncookie的问题和解法

测了一次tcp syncookie的抗D性能,发现了一件有趣的事情,周末写一篇随笔出来。

请看下面的时序:
实践和原则,哪个更重要?tcp syncookie的问题和解法_第1张图片

简单讲就是在syncookie被触发的时候,客户端可能会被静默丢掉最多3个字节,所谓静默就是客户端认为这些字节被收到了(因为它们被确认了),然而服务端真真切切没有收到。

关于这个POC也非常简单:

//$ cat poc.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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 
#include 
#include 
#include 
#include 

#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埃里克说的那样,用一点小代价换一点小收益这种买卖在内核社区还少吗? 能不能做成这笔买卖的核心在于看摆摊的是谁。 有点意思。换个人摆摊,买卖就做成了。


浙江温州皮鞋湿,下雨进水不会胖。

你可能感兴趣的:(syncookie,tcp)