SYNPROXY是Linux内核基于连接跟踪实现的一个内核模块,可以用来缓解tcp syn flood攻击。
SYNPROXY扮演client端和server端的中间人(代理)角色,先和client端完成三次握手,确认三次握手成功后,才会和server端进行三次握手,这样就能有效的拦截掉无效的连接(如tcp syn flood攻击),避免浪费server端资源。
一、使用
可以通过下面的配置,对本机的tcp的80端口开启SYNPROXY。
# 禁止中间捡起连接,禁止client发送ack给synproxy的时候创建ct,这样ack才会命中第二条iptables规则,然后由synproxy模块处理
# The purpose of disabling loose tracking is to have the final ACK from the
# client not be picked up by conntrack, so it won't create a new conntrack
# entry and will be marked INVALID and also get directed to the target.
echo 0 > /proc/sys/net/netfilter/nf_conntrack_tcp_loose
# 第一条iptables规则
# 不对需要做synproxy的流量进行连接跟踪,这个做法有两个作用:
# 1. 可以让需要做synproxy的报文命中下面那条iptables规则。
# 2. 连接跟踪由synproxy向server进行3次握手的时候创建,这样ipv4_synproxy_hook()函数才可以根
# 据连接跟踪的状态来判断要不要进行SYN报文的重传!
iptables -t raw -A PREROUTING -i eth0 -p tcp --dport 80 --syn -j NOTRACK
# 第二条iptables规则
# 将untracked、invalid的报文导到SYNPROXY模块来处理
# 如果server不是本机,那么规则要添加到FORWARD链,即"iptables -A FORWARD ...."
iptables -A INPUT -i eth0 -p tcp --dport 80 -m state --state UNTRACKED,INVALID \
-j SYNPROXY --sack-perm --timestamp --mss 1480 --wscale 7 --ecn
# 第三条iptables规则
# 丢弃未被SYNPROXY处理的报文,这些报文认为是攻击包
iptables -A INPUT -i eth0 -p tcp --dport 80 -m state --state INVALID -j DROP
二、原理
- 当开启synproxy功能的时候,synproxy相对于客户端是透明的。三次握手首先是在客户端和synproxy之间进行:
- client端发送TCP SYN 给server端。
- 当报文到达防火墙的时候,通过上面的第一条iptables规则将其设置为UNTRACKED,那么该syn报文不会进行连接跟踪。
- 这个UNTRACKED TCP SYN报文将会命中第二条规则,执行SYNPROXY动作。
- SYNPROXY将会捕获该报文,记录报文中的相关信息,然后模仿server发送一个TCP SYN/ACK 给client端(源IP是server端的IP),该报文从OUTPUT hook点出去,由于SYN报文没有进行连接跟踪,并且设置了nf_conntrack_tcp_loose=0,因此SYNACK报文在会被连接跟踪设置为INVALID,不会创建CT(连接跟踪)。
- 这里synack报文会将接收窗口设置为0,禁止client马上发送报文(如http request). 等SYNPROXY与server端的3次握手成功后,再发送ack给client更新接收窗口。
- client端回应一个 TCP ACK,同理该报文也会被设置为INVALID,报文将会命中第二条规则,执行SYNPROXY动作,完成三次握手。
- client端完成了与SYNPROXY的三次握手后, SYNPROXY将会马上自动与真实server端完成三次握手,SYNPROXY伪造一个SYN packet让真实server端以为client端在尝试与其连接:
- SYNPROXY发送一个TCP SYN给真实server端,这是一个新的连接,该报文通过OUTPUT hook点进入netfilter,该报文会创建连接跟踪,状态为NEW。
- SYNPROXY发送的SYN报文的源IP是client的源IP,目的IP是sever端的IP。
- server端收到SYN报文后,发送一个SYN/ACK给client端,该报文会被SYNPROXY处理。
- SYNPROXY收到来自server端的SYN/ACK报文后,将会回应一个ACK报文,CT的状态被标记为ESTABLISHED。
- 一旦连接跟踪进入 ESTABLISHED状态,SYNPROXY将会让客户端与真实服务器直接通信。
- 因此总共的握手过程有6次,如下图
- tcp options的处理:
- 由于SYNPROXY不知道真实服务器支持的tcp选项,所以在配置SYNPROXY的时候需要设置相应的TCP选项参数,这些参数一旦设置之后就不会变了,相当于常量。如上面的规则2设置了--sack-perm --timestamp --mss 1480 --wscale 7 --ecn五个选项。
- synproxy向client回复的synack报文,将client的sack permit,ecn,wscale选项编码在timestamp中,然后会由ack报文带回来的synproxy,synproxy再解码出来,这样synproxy和server握手的时候,才能把client的sack permit,ecn,wscale选项透传给server。
- 这样就要求synprox和client都支持tcp timestamp,才能正常的支持sack permit,ecn,wscale选项!
- client的mss是编码在synack的seq中(syncookie),这样收到ack的时候,就可以从ack的ack_seq中还原出来。
- seq(序号)转换:server端到client的tcp timestamp和tcp seq都需要经过synproxy的转换!因为SYNPROXY和server的初始seq号(即SYN/ACK报文的seq号)都是随机生成的。
- synproxy发送syn报文给server的时候,会用client给SYNPROXY的ack报文的seq-1(就是syn报文的seq)作为发送给服务器的syn报文的发送序列号,那么请求方向就不需要进行序列号调整了。
6次握手的报文处理关键流程
- client -- SYN报文:
- 先命中在prerouting的raw表的NOTRACK规则(优先级为NF_IP_PRI_RAW(-300)),设置skb->nfct = &nf_ct_untracked_get()->ct_general,skb->nfctinfo = IP_CT_NEW(见notrack_tg)。
- 然后经过conntrack的处理(优先级为NF_IP_PRI_CONNTRACK(-200)),ipv4_conntrack_in()->nf_conntrack_in(),由于设置了notrack,因此这里会返回NF_ACCEPT,不会创建真正的连接跟踪ct,用的是skb->nfct = &nf_ct_untracked_get()->ct_general。
- 接着进行路由判决,看是INPUT,还是FORWARD。
- 之后到INPUT/FORWARD点,会命中filter表上的SYNPROXY规则(NF_IP_PRI_FILTER = 0),到synproxy_tg4()->synproxy_send_client_synack()回复SYNACK给client,这里返回NF_DROP,报文就不会往下走了,SYN报文处理结束。
- SYN报文没有走到ipv4_synproxy_hook和ipv4_confirm()!
- synproxy -- SYNACK报文:
- synproxy_send_client_synack() -> ip_local_out(),这里SYNACK的skb继承了SYN报文的ct,因此也是notrack的,不会创建真正的ct,及做nat等。
- 会过postrouting。
- 会走到ipv4_synproxy_hook(),由于nfct_synproxy()返回NULL,会返回NF_ACCEPT,没有做什么处理。
- client -- ACK报文:
- 由于设置了tcp_loose==0,因此ACK报文会被nf_conntrack_in()->tcp_new()判断为INVALID的,不会创建ct,因此skb->ct == NULL。
- 因此会在INPUT/FORWARD点,会命中filter表上的SYNPROXY规则(NF_IP_PRI_FILTER = 0),到synproxy_tg4()->synproxy_recv_client_ack()中发送SYN报文给server。
- synproxy:SYN报文:
- synproxy_recv_client_ack()->synproxy_send_server_syn(),ct设置为&snet->tmpl->ct_general,状态为IP_CT_NEW,这样在ipv4_conntrack_local()->nf_conntrack_in()会根据这个模板创建ct,并且创建synproxy和seqadj的扩展。
- 由于有创建了真正的ct,可以对SYN报文进行做nat。不会匹配到SYNPROXY的iptables规则。
- 在postrouting点,会过ipv4_synproxy_hook()函数,这个时候ct的状态为TCP_CONNTRACK_SYN_SENT,会设置synproxy->isn = ntohl(th->ack_seq),等连接建立后用来初始化seqadj。
- server:synack报文:
- 会找到ct。将ct的状态变成TCP_CONNTRACK_SYN_RECV
- 走到LOCAL_IN点的ipv4_synproxy_hook(),synproxy_send_server_ack()发送ACK给server,nf_ct_seqadj_init()初始化seqadj,synproxy_send_client_ack()发送window update给client。
- 返回NF_STOLEN,不再继续处理。
- client:发送报文(ACK或者数据报文):
- prerouting会找到ct。
- 走到LOCAL_IN/POSTROUTING 的 ipv4_synproxy_hook(优先级NF_IP_PRI_CONNTRACK_CONFIRM-1),synproxy_tstamp_adjust()进行timestamp转换。
- 走到ipv4_confirm(优先级NF_IP_PRI_CONNTRACK_CONFIRM),进行seq转换。