TCP握手协议
在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接.
第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
SYN:同步序列编号(Synchronize Sequence Numbers)
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手.
完成三次握手,客户端与服务器开始传送数据。
A与B建立TCP连接时:首先A向B发SYN(同步请求),然后B回复SYN+ACK(同步请求应答),最后A回复ACK确认,这样TCP的一次连接(三次握手)的过程就建立了!
一、TCP报文格式
TCP/IP协议的详细信息参看《TCP/IP协议详解》三卷本。下面是TCP报文格式图:
图1 TCP报文格式
上图中有几个字段需要重点介绍下:
(1)序号:Seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。
(2)确认序号:Ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,Ack=Seq+1。
(3)标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如下:
(A)URG:紧急指针(urgent pointer)有效。
(B)ACK:确认序号有效。
(C)PSH:接收方应该尽快将这个报文交给应用层。
(D)RST:重置连接。
(E)SYN:发起一个新连接。
(F)FIN:释放一个连接。
需要注意的是:
(A)不要将确认序号Ack与标志位中的ACK搞混了。
(B)确认方Ack=发起方Req+1,两端配对。
二、三次握手
所谓三次握手(Three-Way Handshake)即建立TCP连接,就是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发,整个流程如下图所示:
图2 TCP三次握手
(1)第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。
(2)第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
(3)第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。
SYN攻击:
在三次握手过程中,Server发送SYN-ACK之后,收到Client的ACK之前的TCP连接称为半连接(half-open connect),此时Server处于SYN_RCVD状态,当收到ACK后,Server转入ESTABLISHED状态。SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server回复确认包,并等待Client的确认,由于源地址是不存在的,因此,Server需要不断重发直至超时,这些伪造的SYN包将产时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络堵塞甚至系统瘫痪。SYN攻击时一种典型的DDOS攻击,检测SYN攻击的方式非常简单,即当Server上有大量半连接状态且源IP地址是随机的,则可以断定遭到SYN攻击了,使用如下命令可以让之现行:
#netstat -nap | grep SYN_RECV
下面用抓包工具wireshark
wireshark安装:sudo apt-get install wireshark
下面会出现
选择yes。
如果没有出现,则需要我们手动设置:
#dpkg-reconfigure wireshark-common “Should non-superusers be able to capture packages?” //选择Yes (默认是no)
当前用户获取权限:
sudo vim /etc/group
保存退出即可。
运行直接输入:wireshark
下面开始加入断点,在server.c文件中bind,listen,accpet之前加入break_bind(),break_listen(),break_accpet()
在client.c中connect()之前加入break_connect().分别编译两个文件gcc -o client client.c,gcc -o server server.c,生成可执行程序client,server。
打开两个终端分别输入:
gdb server gdb client
开始打断点 :
在上面右图中打上 b break_bind() b break_listen b break_accept
在上面左图中打上 b break_connect(),
然后开始运行wireshark。
开始调试gdb server中
start
c
c
c
此时accept陷入阻塞。查看wireshark没有任何数据
开始调试gdb client中,运行connect()也没有抓到任何数据,
start
c
之后有数据了。
三次握手的具体过程发生在accept和connect之间。
分析代码:
结构体变量struct proto tcp_prot指定了TCP协议栈的访问接口函数:
首先客户端发送SYN报文:
tcp_v4_connect函数:发送syn,设置TCP_SYN_SENT(tcp_set_state),构造SYN并发送(tcp_connect).
tcp_set_state(sk, TCP_SYN_SENT); err = inet_hash_connect(tcp_death_row, sk); if (err) goto failure; sk_set_txhash(sk); rt = ip_route_newports(fl4, rt, orig_sport, orig_dport, inet->inet_sport, inet->inet_dport, sk); if (IS_ERR(rt)) { err = PTR_ERR(rt); rt = NULL; goto failure;
tcp_connect函数:
/* Build a SYN and send it off. */ int tcp_connect(struct sock *sk) { ... /* Reserve space for headers. */ skb_reserve(buff, MAX_TCP_HEADER); tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN); tp->retrans_stamp = tcp_time_stamp; tcp_connect_queue_skb(sk, buff); tcp_ecn_send_syn(sk, buff); /* Send off SYN; include data in Fast Open. */ err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) : tcp_transmit_skb(sk, buff, 1, sk->sk_allocation); ... /* Timer for repeating the SYN until an answer. */ inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, inet_csk(sk)->icsk_rto, TCP_RTO_MAX); ...
另一头服务端accept等待连接请求
inet_csk_accept函数
/* * This will accept the next outstanding connection. */ struct sock *inet_csk_accept(struct sock *sk, int flags, int *err) { /* We need to make sure that this socket is listening, * and that it has something pending. */ error = -EINVAL; if (sk->sk_state != TCP_LISTEN) goto out_err; /* Find already established connection */ if (reqsk_queue_empty(queue)) { long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK); ... error = inet_csk_wait_for_connect(sk, timeo); ... } EXPORT_SYMBOL(inet_csk_accept);
inet_csk_wait_for_connect()函数:
/* * Wait for an incoming connection, avoid race conditions. This must be called * with the socket locked. */245static int inet_csk_wait_for_connect(struct sock *sk, long timeo) { ... //无限for循环,一旦有请求则跳出 for (;;) { prepare_to_wait_exclusive(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE); ... }
三次握手中携带SYN/ACK的TCP头数据的发送和接收
连接建立成功后,接收数据放入accept队列
跟踪这部分代码的思路:
网卡接收到数据需要通知上层协议来接收并处理数据,那么应该有TCP协议的接收数据的函数被底层网络驱动callback方式进行调用,针对这个思路我们需要回头来看TCP/IP协议栈的初始化过程是不是有将recv的函数指针发布给网络底层代码
TCP/IP协议栈初始化:
inet_init函数
static const struct net_protocol tcp_protocol = { .early_demux = tcp_v4_early_demux, .handler = tcp_v4_rcv, .err_handler = tcp_v4_err, .no_policy = 1, .netns_ok = 1, .icmp_strict_tag_validation = 1, }; ... static int __init inet_init(void) { ... /* * Add all the base protocols. */ if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0) pr_crit("%s: Cannot add ICMP protocol\n", __func__); if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0) pr_crit("%s: Cannot add UDP protocol\n", __func__); if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0) pr_crit("%s: Cannot add TCP protocol\n", __func__); #ifdef CONFIG_IP_MULTICAST1719 if (inet_add_protocol(&igmp_protocol, IPPROTO_IGMP) < 0) pr_crit("%s: Cannot add IGMP protocol\n", __func__); #endif ... }
服务端接收客户端发来的SYN,发送SYN+ACK
tcp_v4_do_rcv函数
/* The socket must have it's spinlock held when we get * here. * * We have a potential double-lock case here, so even when * doing backlog processing we use the BH locking scheme. * This is because we cannot sleep with the original spinlock * held. */ int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb) { ... if (sk->sk_state == TCP_LISTEN) { struct sock *nsk = tcp_v4_hnd_req(sk, skb); if (!nsk) goto discard; if (nsk != sk) { ... return 0; } } else sock_rps_save_rxhash(sk, skb); if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) { rsk = sk; goto reset; } ... } EXPORT_SYMBOL(tcp_v4_do_rcv);
客户端收到服务端的SYN+ACK,发送ACK
tcp_rcv_synsent_state_process函数:
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,const struct tcphdr *th, unsigned int len) { .. tcp_send_ack(sk); ... }
到这里我们已经从linux网络核心的角度从架构上整体理解了三次握手,即携带SYN/ACK标志的数据收发过程