1、原理:
通过synproxy,由netfilter来代理客户端的三次握手过程;优点是netfilter的连接跟踪表可以等到三次握手完成后才创建,而不是收到客户端的syn后就立即创建;可以减少syn flood攻击导致的性能开销;
2、规则:
1)、iptables -t raw -A PREROUTING -I eth0 -p tcp --dport 80 --syn -j NOTRACK ---- 对eth0收到的sync报文标记notrack
2)、iptables -A INPUT -I eth0 - ptcp --dport 80 -mstate --state UNTRACKED,INVALID -j SYNPROXY--sack-perm--timestamp--mss1480--wscale7--ecn 对eth0收到的untrack的包,将其导向synproxy模块处理;
3)、echo 0 > /proc/sys/net/netfilter/nf_conntrack_tcp_loose ----- 如果没有这条,client的第三个ack回来时会被丢弃
3、过程
3.1、收到客户端第一个sync报文
1)、首先上面的第一条iptable规则将报文设置notrack标记;
2)、报文进入input流程时,由上面的第二条iptable规则将其做synproxy处理;
ipt_do_table
synproxy_tg4
synproxy_parse_options(先解析tcp option)
synproxy_send_client_synack(分配报文,初始化ip头部、tcp头部等信息)
synproxy_build_options(填充tcp option)
synproxy_send_tcp(回复客户端)
3.2 收到客户端的3ardack
由于上面收到客户端第一个syn包的时候,没有创建连接跟踪;因此这里收到客户端的3ard ack时,内核会先创建一个连接跟踪;由于配置了echo 0 > /proc/sys/net/netfilter/nf_conntrack_tcp_loose;因此这个3ardack在tcp_new的时候可以通过;但是此时tcp_new会返回-NF_ACCEPT;这样nf_conntrack_handle_packet处理完成后会执行nf_conntrack_put释放新创建的连接跟踪;也就是说这时候skb的ct状态为INVALID;因此这个报文也会匹配上面添加的第二条iptable规则;进入synproxy处理流程里;
ipt_do_table
synproxy_tg4
synproxy_recv_client_ack(收到客户端的3ard ack)
synproxy_send_server_syn(由synproxy向server发送syn)
synproxy_send_tcp
ip_local_out(这里面由于目的ip是本机ip,因此最终走的是lo口的发送流程)
loopback_xmit
netif_rx
由于iptable规则里并没有对lo口的syn报文做notrack处理;因此synproxy通过lo口向server发送的syn报文按正常的创建连接跟踪的逻辑处理;然后执行的netfilter的LOCAL_IN的chain的时候,进入ipv4_synproxy_hook处理流程;
针对synproxy发送给server的第一个报文,此时的ct状态为TCP_CONNTRACK_SYN_SENT,在synproxy里针对TCP_CONNTRACK_SYN_SENT状态没有做什么特殊处理,最终这个syn报文发给了server;然后由server回复syn ack;
3.3、收到server回复的syn ack
server回复的syn ack在报文走到POST_ROUTING的时候,触发ipv4_synproxy_hook的kook函数;此时连接跟踪的状态已经变成TCP_CONNTRACK_SYN_RECV;
ipv4_synproxy_hook
ct状态为TCP_CONNTRACK_SYN_RECV,即收到server回复的syn ack的处理逻辑:
synproxy_parse_options
synproxy_send_server_ack(synproxy向server 回复3ard ack)
synproxy_send_client_ack(synproxy向client回复ack ,通告client接收窗口更新)
4、其它:
1、synproxy如何保证client回复3ard ack收,不会立即向server请求数据? 因为当client回复3ard ack后,synproxy还需要进一步与server建立三次握手过程;如果与server的三次握手还没建立完成就收到clinet的正常请求,那连接会出现异常;
1)、 当synproxy收到client的syn报文时,会回复一个synack,这个synack里面,会将接收窗口设置为0,这样clinet在收到synproxy回复的synack后,由于server端接收窗口的限制,只会回复3ardack,并不会发送其它的请求数据;
static void
synproxy_send_client_synack(struct net *net,
const struct sk_buff *skb, const struct tcphdr *th,
const struct synproxy_options *opts)
{
struct sk_buff *nskb;
struct iphdr *iph, *niph;
struct tcphdr *nth;
unsigned int tcp_hdr_size;
u16 mss = opts->mss_encode;
iph = ip_hdr(skb);
tcp_hdr_size = sizeof(*nth) + synproxy_options_size(opts);
nskb = alloc_skb(sizeof(*niph) + tcp_hdr_size + MAX_TCP_HEADER,
GFP_ATOMIC);
if (nskb == NULL)
return;
skb_reserve(nskb, MAX_TCP_HEADER);
niph = synproxy_build_ip(net, nskb, iph->daddr, iph->saddr);
skb_reset_transport_header(nskb);
nth = skb_put(nskb, tcp_hdr_size);
nth->source = th->dest;
nth->dest = th->source;
nth->seq = htonl(__cookie_v4_init_sequence(iph, th, &mss));
nth->ack_seq = htonl(ntohl(th->seq) + 1);
tcp_flag_word(nth) = TCP_FLAG_SYN | TCP_FLAG_ACK;
if (opts->options & XT_SYNPROXY_OPT_ECN)
tcp_flag_word(nth) |= TCP_FLAG_ECE;
nth->doff = tcp_hdr_size / 4;
// 这里synproxy给client回复synack时,将接收创建设置为0,这样可以保证client在回复
// 3ardack后不会继续向server请求数据;因为client回复3ardack后,synproxy还需要进一步
// 和server建立三次握手才能保证client和server之间可以正常通信;
// synproxy在与server建立三次握手的过程中,当收到server的synack后,处理向server
// 回复3ardack外,synproxy还会向client发送一个正常的ack报文,这个报文相当于是一个通过
// 接收窗口的报文.
nth->window = 0;
nth->check = 0;
nth->urg_ptr = 0;
synproxy_build_options(nth, opts);
synproxy_send_tcp(net, skb, nskb, skb_nfct(skb),
IP_CT_ESTABLISHED_REPLY, niph, nth, tcp_hdr_size);
}
2)、当synproxy与server建立三次握手时,收到server的synack时,synproxy除了向server回复3ardack外,还会向client发送一个正常的ack报文,并且填充好正确的接收窗口,相当于这个ack报文是server端的一个窗口更新通知;这样client收到这个报文后就可以继续向server发送请求数据了;
2、当存在synproxy时,存在两次的三次握手情况,即synproxy和client之间以及synproxy和server之间,synproxy回复clinet synack的时候,synproxy会通过__cookie_v4_init_sequence生成一个序列号;synproxy和server之间建立三次握手的时候,server回复synack时也会生成一个序列号;那么后面client和server之间通信的时候,如何保证序列号一致性?
1)、当synproxy收到client的3ardack后,会向server发起syn请求;同时将之前与client交互时使用的序列号保存到该syn包的ack_seq里;
2)、该syn包经过lo口发送给server,并在prerouting的conntrack阶段创建新的ct连接跟踪;在创建连接跟踪的时候,通过nf_ct_add_synproxy为连接跟踪添加synproxy;
3)、syn请求在经过LOCAL_IN的hook点时,会触发ipv4_synproxy_hook的hook函数;在ipv4_synproxy_hook里,首先从ct里获取synproxy,然后将ack_seq作为synproxy的isn序列号保存下来;
4)、当server向synproxy回复的synack时,报文经过postrouting时,触发ipv4_synproxy_hook,在ipv4_synproxy_hook里,snyproxy计算isn序列号与server端当前使用的seq号的差异,然后通过nf_ct_seqadj_init将这两个序列号的差异记录下来,同时对ct->status设置IPS_SEQ_ADJUST_BIT标志位;
int nf_ct_seqadj_init(struct nf_conn *ct, enum ip_conntrack_info ctinfo,
s32 off)
{
// synproxy的场景,这里是synproxy收到server的synack时候初始化的;因此这里的dir是reply
// 方向
enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
struct nf_conn_seqadj *seqadj;
struct nf_ct_seqadj *this_way;
if (off == 0)
return 0;
set_bit(IPS_SEQ_ADJUST_BIT, &ct->status);
// offset为synproxy、clinet交互使用的序列号与synproxy、server交互使用的序列号差值;
// 后面server发给clinet的时候,将tcp->seq的值加上这个offset;
// client发给server的时候,将tcp->ack_seq的值加上这个offset;
seqadj = nfct_seqadj(ct);
this_way = &seqadj->seq[dir];
this_way->offset_before = off;
this_way->offset_after = off;
return 0;
}
5)、synproxy向client回复ack,通知client窗口更新,这个报文通过ip_local_out发送出去,因此会重新经历走一遍LOCAL_OUT、postrouting流程,在ipv4_conntrack_local的tcp_in_window流程里,通过nf_ct_seq_offset获取序列号的offset值,然后根据结合这个offset与当前的序列号判断序列号是否正常(这里只是判断序列号是否在窗口范围内,并没有去修改报文的序列号);
6)、报文走到postrouting的时候,进入ipv4_confirm流程;在ipv4_confirm里判断ct->state设置了IPS_SEQ_ADJUST_BIT标志,进入nf_ct_seq_adjust,根据ct记录的offset值修改报文的seq序列号;同样的道理当client发给server的时候,也会进入到这个流程,然后修改tcp的ack_seq;这样client、server之间的序列号就能正常对的上了;
/* TCP sequence number adjustment. Returns 1 on success, 0 on failure */
int nf_ct_seq_adjust(struct sk_buff *skb,
struct nf_conn *ct, enum ip_conntrack_info ctinfo,
unsigned int protoff)
{
enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
struct tcphdr *tcph;
__be32 newseq, newack;
s32 seqoff, ackoff;
struct nf_conn_seqadj *seqadj = nfct_seqadj(ct);
struct nf_ct_seqadj *this_way, *other_way;
int res = 1;
// nf_ct_seqadj_init的时候,将offset记录在reply方向上;
// 当server发给clinet的时候,进来这里的dir为reply方向;因此这里是this_way里才有offset
// 的值,然后根据this_way的值修改tcph->seq的值;
// 当client发给server的时候,进来这里的dir为origin方向;因此这里other_way里才有offset
// 的值,然后根据oterh_way的值修改tcph->ack_seq;
this_way = &seqadj->seq[dir];
other_way = &seqadj->seq[!dir];
if (!skb_make_writable(skb, protoff + sizeof(*tcph)))
return 0;
tcph = (void *)skb->data + protoff;
spin_lock_bh(&ct->lock);
if (after(ntohl(tcph->seq), this_way->correction_pos))
seqoff = this_way->offset_after;
else
seqoff = this_way->offset_before;
newseq = htonl(ntohl(tcph->seq) + seqoff);
inet_proto_csum_replace4(&tcph->check, skb, tcph->seq, newseq, false);
pr_debug("Adjusting sequence number from %u->%u\n",
ntohl(tcph->seq), ntohl(newseq));
tcph->seq = newseq;
if (!tcph->ack)
goto out;
if (after(ntohl(tcph->ack_seq) - other_way->offset_before,
other_way->correction_pos))
ackoff = other_way->offset_after;
else
ackoff = other_way->offset_before;
newack = htonl(ntohl(tcph->ack_seq) - ackoff);
inet_proto_csum_replace4(&tcph->check, skb, tcph->ack_seq, newack,
false);
pr_debug("Adjusting ack number from %u->%u, ack from %u->%u\n",
ntohl(tcph->seq), ntohl(newseq), ntohl(tcph->ack_seq),
ntohl(newack));
tcph->ack_seq = newack;
res = nf_ct_sack_adjust(skb, protoff, ct, ctinfo);
out:
spin_unlock_bh(&ct->lock);
return res;
}