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

测了一次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://blog.csdn.net/dog250/article/details/115783320

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

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

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/405731

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月26日 上午9:23
下一篇 2023年4月26日 上午9:23

相关推荐