深入理解TCP协议及其源代码

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报文格式图:

深入理解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来触发,整个流程如下图所示:

深入理解TCP协议及其源代码
图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

下面会出现深入理解TCP协议及其源代码

 选择yes。

如果没有出现,则需要我们手动设置:

#dpkg-reconfigure wireshark-common
“Should non-superusers be able to capture packages?//选择Yes (默认是no)

当前用户获取权限:

sudo vim /etc/group

深入理解TCP协议及其源代码

 保存退出即可。

运行直接输入:wireshark

下面开始加入断点,在server.c文件中bind,listen,accpet之前加入break_bind(),break_listen(),break_accpet()

深入理解TCP协议及其源代码

 在client.c中connect()之前加入break_connect().分别编译两个文件gcc -o client client.c,gcc -o server server.c,生成可执行程序client,server。

打开两个终端分别输入:

gdb server gdb client

深入理解TCP协议及其源代码

 开始打断点 :

在上面右图中打上 b break_bind() b break_listen b break_accept

在上面左图中打上 b break_connect(),

然后开始运行wireshark。

开始调试gdb server中   

start

c

c

深入理解TCP协议及其源代码

此时accept陷入阻塞。查看wireshark没有任何数据

深入理解TCP协议及其源代码

开始调试gdb client中,运行connect()也没有抓到任何数据,

start 

c

之后有数据了。

 深入理解TCP协议及其源代码

 深入理解TCP协议及其源代码

 三次握手的具体过程发生在accept和connect之间。

分析代码:

深入理解TCP协议及其源代码

结构体变量struct proto tcp_prot指定了TCP协议栈的访问接口函数:

 

 深入理解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函数:

深入理解TCP协议及其源代码

/* 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函数

深入理解TCP协议及其源代码

/*
 * 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函数
深入理解TCP协议及其源代码

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
", __func__);
 if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
 pr_crit("%s: Cannot add UDP protocol
", __func__);
if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
 pr_crit("%s: Cannot add TCP protocol
", __func__);
#ifdef CONFIG_IP_MULTICAST1719 if (inet_add_protocol(&igmp_protocol, IPPROTO_IGMP) < 0)
 pr_crit("%s: Cannot add IGMP protocol
", __func__);
#endif
...
}



 服务端接收客户端发来的SYN,发送SYN+ACK
tcp_v4_do_rcv函数

深入理解TCP协议及其源代码

/* 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标志的数据收发过程