如下PROC文件tcp_syncookies默认值为1,表明在套接口的SYN backlog队列溢出时,将开启SYNCOOKIES功能,抵御SYN泛洪攻击。如果tcp_syncookies设置为2,将会无条件的开启SYNCOOKIES功能。
$ cat /proc/sys/net/ipv4/tcp_syncookies
1
$
$ cat /proc/sys/net/ipv4/tcp_max_syn_backlog
128
$
syncookies开启
如下函数tcp_conn_request,在接收到客户端SYN请求报文之后,如果tcp_syncookies设置为2,或者SYN报文队列已满(tcp_max_syn_backlog),并且ISN等于0,是一个全新的TCP连接。此时,调用tcp_syn_flood_action函数判断一下是否需要开启syncookie功能。
int tcp_conn_request(struct request_sock_ops *rsk_ops,
const struct tcp_request_sock_ops *af_ops, struct sock *sk, struct sk_buff *skb)
{
struct tcp_fastopen_cookie foc = { .len = -1 };
__u32 isn = TCP_SKB_CB(skb)->tcp_tw_isn;
struct tcp_options_received tmp_opt;
struct request_sock *req;
bool want_cookie = false;
/* TW buckets are converted to open requests without
* limitations, they conserve resources and peer is evidently real one.
*/
if ((net->ipv4.sysctl_tcp_syncookies == 2 || inet_csk_reqsk_queue_is_full(sk)) && !isn) {
want_cookie = tcp_syn_flood_action(sk, skb, rsk_ops->slab_name);
if (!want_cookie)
goto drop;
}
如果以上的判断确认要开启syncookies后,使用函数cookie_init_sequence计算初始序号ISN的值。
if (want_cookie) {
isn = cookie_init_sequence(af_ops, sk, skb, &req->mss);
req->cookie_ts = tmp_opt.tstamp_ok;
if (!tmp_opt.tstamp_ok)
inet_rsk(req)->ecn_ok = 0;
}
如果启用了syncookies,将客户端SYN报文中的一些信息保存在了序号中,就不需要保留此连接的request_sock结构了,在发送完SYN+ACK报文之后,将其释放,降低DDos攻击时的资源消耗。
if (fastopen_sk) {
} else {
...
af_ops->send_synack(sk, dst, &fl, req, &foc,
!want_cookie ? TCP_SYNACK_NORMAL : TCP_SYNACK_COOKIE);
if (want_cookie) {
reqsk_free(req);
return 0;
}
}
以下几节将分别介绍上面遇到的syncookies相关函数。
SYN泛洪动作
如下SYN Flood动作判断函数tcp_syn_flood_action,如果tcp_syncookies设置为0,将不启用syncookie,意味着将丢弃报文。否则,tcp_syncookies不为零,启用该功能。synflood_warned控制仅打印一次SYN泛洪警告。
static bool tcp_syn_flood_action(const struct sock *sk,
const struct sk_buff *skb, const char *proto)
{
struct request_sock_queue *queue = &inet_csk(sk)->icsk_accept_queue;
const char *msg = "Dropping request";
bool want_cookie = false;
struct net *net = sock_net(sk);
#ifdef CONFIG_SYN_COOKIES
if (net->ipv4.sysctl_tcp_syncookies) {
msg = "Sending cookies";
want_cookie = true;
__NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPREQQFULLDOCOOKIES);
} else
#endif
__NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPREQQFULLDROP);
if (!queue->synflood_warned && net->ipv4.sysctl_tcp_syncookies != 2 &&
xchg(&queue->synflood_warned, 1) == 0)
net_info_ratelimited("%s: Possible SYN flooding on port %d. %s. Check SNMP counters.\n",
proto, ntohs(tcp_hdr(skb)->dest), msg);
return want_cookie;
syncookie生成序号
如下函数cookie_init_sequence,首先是调用函数tcp_synq_overflow记录下最近一次SYN队列溢出的时间戳。之后,调用协议注册的cookie初始化函数,对于IPv4,此为函数cookie_v4_init_sequence,而对于IPv6,为函数cookie_v6_init_sequence。
static inline __u32 cookie_init_sequence(const struct tcp_request_sock_ops *ops,
const struct sock *sk, struct sk_buff *skb, __u16 *mss)
{
tcp_synq_overflow(sk);
__NET_INC_STATS(sock_net(sk), LINUX_MIB_SYNCOOKIESSENT);
return ops->cookie_init_seq(skb, mss);
}
函数tcp_synq_overflow记录最近syn队列溢出的时间戳,对于端口重用的套接口,将时间戳记录在sock_reuseport结构的synq_overflow_ts中。否则,将其记录在TCP接收选项结构的ts_recent_stamp成员中。每一秒做一次更新。
static inline void tcp_synq_overflow(const struct sock *sk)
{
unsigned int last_overflow;
unsigned int now = jiffies;
if (sk->sk_reuseport) {
struct sock_reuseport *reuse;
reuse = rcu_dereference(sk->sk_reuseport_cb);
if (likely(reuse)) {
last_overflow = READ_ONCE(reuse->synq_overflow_ts);
if (time_after32(now, last_overflow + HZ))
WRITE_ONCE(reuse->synq_overflow_ts, now);
return;
}
}
last_overflow = tcp_sk(sk)->rx_opt.ts_recent_stamp;
if (time_after32(now, last_overflow + HZ))
tcp_sk(sk)->rx_opt.ts_recent_stamp = now;
}
与以上的函数tcp_synq_overflow对应,函数tcp_synq_no_recent_overflow负责在接收到ACK握手报文时,检查记录的SYN队列溢出时间戳与当前时间的差值,如果小于2分钟(TCP_SYNCOOKIE_VALID),则认为还在有效期内。
#define MAX_SYNCOOKIE_AGE 2
#define TCP_SYNCOOKIE_PERIOD (60 * HZ)
#define TCP_SYNCOOKIE_VALID (MAX_SYNCOOKIE_AGE * TCP_SYNCOOKIE_PERIOD)
static inline bool tcp_synq_no_recent_overflow(const struct sock *sk)
{
unsigned int last_overflow;
unsigned int now = jiffies;
if (sk->sk_reuseport) {
struct sock_reuseport *reuse;
reuse = rcu_dereference(sk->sk_reuseport_cb);
if (likely(reuse)) {
last_overflow = READ_ONCE(reuse->synq_overflow_ts);
return time_after32(now, last_overflow + TCP_SYNCOOKIE_VALID);
}
}
last_overflow = tcp_sk(sk)->rx_opt.ts_recent_stamp;
return time_after32(now, last_overflow + TCP_SYNCOOKIE_VALID);
以下为IPv4协议注册的syncookie初始化函数cookie_v4_init_sequence,主要功能由封装的函数__cookie_v4_init_sequence完成。
__u32 cookie_v4_init_sequence(const struct sk_buff *skb, __u16 *mssp)
{
const struct iphdr *iph = ip_hdr(skb);
const struct tcphdr *th = tcp_hdr(skb);
return __cookie_v4_init_sequence(iph, th, mssp);
}
static const struct tcp_request_sock_ops tcp_request_sock_ipv4_ops = {
...
#ifdef CONFIG_SYN_COOKIES
.cookie_init_seq = cookie_v4_init_sequence,
#endif
.init_ts_off = tcp_v4_init_ts_off,
以下为函数__cookie_v4_init_sequence,首先,将客户端的MSS值近似到msstab中最近的较小值,作为编码到cookie中的值,之后,调用secure_tcp_syn_cookie函数,根据报文中的源/目的IP地址,TCP源/目的端口号,TCP序号和MSS索引值,计算cookie。
static __u16 const msstab[] = {
536,
1300,
1440, /* 1440, 1452: PPPoE */
1460,
};
u32 __cookie_v4_init_sequence(const struct iphdr *iph, const struct tcphdr *th, u16 *mssp)
{
int mssind;
const __u16 mss = *mssp;
for (mssind = ARRAY_SIZE(msstab) - 1; mssind ; mssind--)
if (mss >= msstab[mssind])
break;
*mssp = msstab[mssind];
return secure_tcp_syn_cookie(iph->saddr, iph->daddr,
th->source, th->dest, ntohl(th->seq), mssind);
根据msstab代码中的注释,MSS值1460为最常用的通告值,概率在30%-46%之间,概率第二高的为1300-1349长度的MSS值,概率为15%-20%,而MSS为537-1299的概率小于1.5%,最后MSS值小于536的概率低于0.2%。此数据来自于S. Alcock 和 R. Nelson的论文’An Analysis of TCP Maximum Segement Sizes’。据此,msstab按需排列,由最高的概率开始检测。
ACK报文处理
在接收到客户端回复的ACK报文后,处理函数tcp_v4_do_rcv判断套接口处于TCP_LISTEN状态时,由于SYNCOOKIE开启,释放了TCP_NEW_SYN_RECV状态的请求套接口,故运行至此,调用tcp_v4_cookie_check检查报文中的cookie,并创建子套接口。返回值nsk与sk不相同,表明子套接口创建成功。
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
if (sk->sk_state == TCP_LISTEN) {
struct sock *nsk = tcp_v4_cookie_check(sk, skb);
if (!nsk)
goto discard;
if (nsk != sk) {
if (tcp_child_process(sk, nsk, skb)) {
rsk = nsk;
goto reset;
}
return 0;
如下tcp_v4_cookie_check函数,对于SYNCOOKIE,仅处理ACK报文,所以要求SYN标志不能置位,调用子函数cookie_v4_check处理。
static struct sock *tcp_v4_cookie_check(struct sock *sk, struct sk_buff *skb)
{
#ifdef CONFIG_SYN_COOKIES
const struct tcphdr *th = tcp_hdr(skb);
if (!th->syn)
sk = cookie_v4_check(sk, skb);
#endif
return sk;
如下cookie_v4_check函数,首先,将报文中的确认序号ack_seq减去一,还原为本端原本的cookie值;之后,检查本地是否启用了SYNCOOKIES功能,如果报文的TCP头部没有设置ACK位,或者设置了RST未,不进行后续处理。函数tcp_synq_no_recent_overflow检查套接口记录的SYN队列溢出时间戳是否还在有效期内。
函数__cookie_v4_check对cookie进行验证,如果是正确的,返回值为编码的MSS。
struct sock *cookie_v4_check(struct sock *sk, struct sk_buff *skb)
{
__u32 cookie = ntohl(th->ack_seq) - 1;
if (!sock_net(sk)->ipv4.sysctl_tcp_syncookies || !th->ack || th->rst)
goto out;
if (tcp_synq_no_recent_overflow(sk))
goto out;
mss = __cookie_v4_check(ip_hdr(skb), th, cookie);
if (mss == 0) {
__NET_INC_STATS(sock_net(sk), LINUX_MIB_SYNCOOKIESFAILED);
goto out;
}
__NET_INC_STATS(sock_net(sk), LINUX_MIB_SYNCOOKIESRECV);
接下来,解析ACK报文中的TCP选项字段,如果存在TCP时间戳选项,并且内核启用了RFC1323定义的时间戳编码,使用cookie_timestamp_decode函数由时间戳编码中解析出客户端SYN报文指定的TCP选项。
由于在函数tcp_synack_options中(稍后介绍),将时间戳的tsval值增加了ts_off的值,这里做响应的减法。
/* check for timestamp cookie support */
memset(&tcp_opt, 0, sizeof(tcp_opt));
tcp_parse_options(sock_net(sk), skb, &tcp_opt, 0, NULL);
if (tcp_opt.saw_tstamp && tcp_opt.rcv_tsecr) {
tsoff = secure_tcp_ts_off(sock_net(sk), ip_hdr(skb)->daddr, ip_hdr(skb)->saddr);
tcp_opt.rcv_tsecr -= tsoff;
}
if (!cookie_timestamp_decode(sock_net(sk), &tcp_opt))
goto out;
以下,正是分配请求套接口结构request_sock,并且初始化接收和发送的初始序号ISN,以及初始化相关TCP选项。之后,保存ACK报文中携带的IP选项,这里寄希望于ACK报文中的IP选项与最初的SYN报文中的IP选项相同。
req = inet_reqsk_alloc(&tcp_request_sock_ops, sk, false); /* for safety */
if (!req) goto out;
ireq = inet_rsk(req);
treq = tcp_rsk(req);
treq->rcv_isn = ntohl(th->seq) - 1;
treq->snt_isn = cookie;
req->mss = mss;
ireq->snd_wscale = tcp_opt.snd_wscale;
ireq->sack_ok = tcp_opt.sack_ok;
ireq->wscale_ok = tcp_opt.wscale_ok;
ireq->tstamp_ok = tcp_opt.saw_tstamp;
/* We throwed the options of the initial SYN away, so we hope
* the ACK carries the same options again (see RFC1122 4.2.3.8)
*/
RCU_INIT_POINTER(ireq->ireq_opt, tcp_v4_save_options(sock_net(sk), skb));
以下将在此初始化本地的接收窗口,这里假定在发送SYNACK和接收客户端ACK之间,我们的接收窗口没有改变。与处理客户端SYN报文时调用的接收窗口函数tcp_openreq_init_rwin类似。
flowi4_init_output(&fl4, ireq->ir_iif, ireq->ir_mark,
RT_CONN_FLAGS(sk), RT_SCOPE_UNIVERSE, IPPROTO_TCP,
inet_sk_flowi_flags(sk), opt->srr ? opt->faddr : ireq->ir_rmt_addr,
ireq->ir_loc_addr, th->source, th->dest, sk->sk_uid);
security_req_classify_flow(req, flowi4_to_flowi(&fl4));
rt = ip_route_output_key(sock_net(sk), &fl4);
req->rsk_window_clamp = tp->window_clamp ? :dst_metric(&rt->dst, RTAX_WINDOW);
tcp_select_initial_window(sk, tcp_full_space(sk), req->mss,
&req->rsk_rcv_wnd, &req->rsk_window_clamp, ireq->wscale_ok, &rcv_wscale,
dst_metric(&rt->dst, RTAX_INITRWND));
ireq->rcv_wscale = rcv_wscale;
ireq->ecn_ok = cookie_ecn_ok(&tcp_opt, sock_net(sk), &rt->dst);
最终,函数tcp_get_cookie_sock创建子套接口。
ret = tcp_get_cookie_sock(sk, skb, req, &rt->dst, tsoff);
/* ip_queue_xmit() depends on our flow being setup
* Normal sockets get it right from inet_csk_route_child_sock()
*/
if (ret)
inet_sk(ret)->cork.fl.u.ip4 = fl4;
out: return ret;
SYNCOOKIE算法
首先看一下cookie时间函数tcp_cookie_time,宏TCP_SYNCOOKIE_PERIOD的值为60秒(60*HZ),所以cookie时间的单位为60秒。
static inline u32 tcp_cookie_time(void)
{
u64 val = get_jiffies_64();
do_div(val, TCP_SYNCOOKIE_PERIOD);
return val;
cookie的生成由函数secure_tcp_syn_cookie完成,其将32位序号分成两部分,前24位(COOKIEBITS)将编码客户端的序号以及cookie的时间值count,由以上函数tcp_cookie_time可知,count没一分钟增加一。后8位将编码MSS的索引值(msstab数组索引)。
#define COOKIEBITS 24 /* Upper bits store count */
#define COOKIEMASK (((__u32)1 << COOKIEBITS) - 1)
具体的,安全序号的生成由两次哈希完成,算法由siphash完成。
static __u32 secure_tcp_syn_cookie(__be32 saddr, __be32 daddr, __be16 sport, __be16 dport, __u32 sseq, __u32 data)
{
/*
* 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));
cookie的检查工作由函数__cookie_v4_check完成,在接收到客户端的ACK报文后,将TCP的确认序号减去一即得到原有的cookie值,并且将ACK报文的序号减去一,即得到计算cookie时所使用的序号。具体的检查由函数check_tcp_syn_cookie完成,其返回cookie中编码的MSS索引值。
/* Check if a ack sequence number is a valid syncookie.
* Return the decoded mss if it is, or 0 if not.
*/
int __cookie_v4_check(const struct iphdr *iph, const struct tcphdr *th, u32 cookie)
{
__u32 seq = ntohl(th->seq) - 1;
__u32 mssind = check_tcp_syn_cookie(cookie, iph->saddr, iph->daddr,
th->source, th->dest, seq);
return mssind < ARRAY_SIZE(msstab) ? msstab[mssind] : 0;
}
cookie验证函数正是以上函数secure_tcp_syn_cookie的反向操作。分为两个步骤进行,第一检查编码在cookie中的count时间值,如果与当前系统的时间值相比大于2分钟(MAX_SYNCOOKIE_AGE),即认为超时出错。第二,记录编码的MSS索引值。
static __u32 check_tcp_syn_cookie(__u32 cookie, __be32 saddr, __be32 daddr, __be16 sport, __be16 dport, __u32 sseq)
{
u32 diff, count = tcp_cookie_time();
/* 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 */
TCP时间戳
在回复客户端的SYN报文时,函数tcp_make_synack判断SYN报文中是否携带了TCP的时间戳选项,即cookie_ts是否为真,为真的话,说明可以使其中的tsval字段,调用函数cookie_init_timestamp进行处理。
struct sk_buff *tcp_make_synack(const struct sock *sk, struct dst_entry *dst,
struct request_sock *req,...)
{
memset(&opts, 0, sizeof(opts));
#ifdef CONFIG_SYN_COOKIES
if (unlikely(req->cookie_ts))
skb->skb_mstamp_ns = cookie_init_timestamp(req);
else
#endif
函数cookie_init_timestamp将客户端SYN报文中的TCP选项编码在时间戳选项中,方法是使用后六位(TSBITS)编码窗口扩展系数、SACK支持和ECN。此操作需要确保最终的时间戳小于当前时间戳,如果大于当前TCP时间戳,将当前时间戳由第7位开始,减去一。TCP以毫秒为单位递增时间戳,以上相当于减去了64毫秒。
u64 cookie_init_timestamp(struct request_sock *req)
{
struct inet_request_sock *ireq;
u32 ts, ts_now = tcp_time_stamp_raw();
u32 options = 0;
ireq = inet_rsk(req);
options = ireq->wscale_ok ? ireq->snd_wscale : TS_OPT_WSCALE_MASK;
if (ireq->sack_ok)
options |= TS_OPT_SACK;
if (ireq->ecn_ok)
options |= TS_OPT_ECN;
ts = ts_now & ~TSMASK;
ts |= options;
if (ts > ts_now) {
ts >>= TSBITS;
ts--;
ts <<= TSBITS;
ts |= options;
}
return (u64)ts * (NSEC_PER_SEC / TCP_TS_HZ);
最终的时间戳tsval的值等于tcp_skb_timestamp函数的返回值,加上ts_off的值,而ts_off的值在接收到客户端SYN报文时,由函数secure_tcp_ts_off计算而得。
static inline u32 tcp_skb_timestamp(const struct sk_buff *skb)
{
return div_u64(skb->skb_mstamp_ns, NSEC_PER_SEC / TCP_TS_HZ);
}
static unsigned int tcp_synack_options(..., struct request_sock *req,
unsigned int mss, struct sk_buff *skb,...)
{
if (likely(ireq->tstamp_ok)) {
opts->options |= OPTION_TS;
opts->tsval = tcp_skb_timestamp(skb) + tcp_rsk(req)->ts_off;
opts->tsecr = req->ts_recent;
remaining -= TCPOLEN_TSTAMP_ALIGNED;
}
如下函数secure_tcp_ts_off可见,ts_off的值由源和目的IP地址,以及秘钥值ts_secret进行哈希计算而得到。
u32 secure_tcp_ts_off(const struct net *net, __be32 saddr, __be32 daddr)
{
if (net->ipv4.sysctl_tcp_timestamps != 1)
return 0;
ts_secret_init();
return siphash_2u32((__force u32)saddr, (__force u32)daddr, &ts_secret);
内核需要打开tcp_timestamps功能,如下PROC文件所示,默认为开启状态。
$ cat /proc/sys/net/ipv4/tcp_timestamps
1
内核版本 5.0