關於TCP協議
TCP/IP協議分層模型
可以看到,TCP協議位於運輸層,TCP將用戶數據打包構成報文段,它發送數據時啟動一個定時器,另一端收到數據進行確認,對失序的數據重新排序,丟棄重復的數據。TCP提供一種面向連接的可靠的字節流服務,面向連接意味着兩個使用TCP的應用(B/S)在彼此交換數據之前,必須先建立一個TCP連接,類似於打電話過程,先撥號振鈴,等待對方說喂,然后應答。在一個TCP連接中,只有兩方彼此通信。
TCP可靠性來自於:
(1)應用數據被分成TCP最合適的發送數據塊
(2)當TCP發送一個段之后,啟動一個定時器,等待目的點確認收到報文,如果不能及時收到一個確認,將重發這個報文。
(3)當TCP收到連接端發來的數據,就會推遲幾分之一秒發送一個確認。
(4)TCP將保持它首部和數據的檢驗和,這是一個端對端的檢驗和,目的在於檢測數據在傳輸過程中是否發生變化。(有錯誤,就不確認,發送端就會重發)
(5)TCP是以IP報文來傳送,IP數據是無序的,TCP收到所有數據后進行排序,再交給應用層
(6)IP數據報會重復,所以TCP會去重
(7)TCP能提供流量控制,TCP連接的每一個地方都有固定的緩沖空間。TCP的接收端只允許另一端發送緩存區能接納的數據。
(8)TCP對字節流不做任何解釋,對字節流的解釋由TCP連接的雙方應用層解釋。
TCP建立連接的過程(三次握手)
TCP是一個面向連接的協議,無論哪一方向另一方發送數據之前,都必須先在雙方之間建立一條連接,所謂三次握手(Three-Way Handshake)即建立TCP連接,就是指建立一個TCP連接時,需要客戶端和服務端總共發送3個包以確認連接的建立。在socket編程中,這一過程由客戶端執行connect來觸發,整個流程如下圖所示:
(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之間可以開始傳輸數據了。
簡單來說,就是:
1、建立連接時,客戶端發送SYN包(SYN=i)到服務器,並進入到SYN-SEND狀態,等待服務器確認;
2、服務器收到SYN包,必須確認客戶的SYN(ack=i+1),同時自己也發送一個SYN包(SYN=k),即SYN+ACK包,此時服務器進入SYN-RECV狀態;
3、客戶端收到服務器的SYN+ACK包,向服務器發送確認報ACK(ack=k+1),此包發送完畢,客戶端和服務器進入ESTABLISHED狀態,完成三次握手,客戶端與服務器開始傳送數據。
關於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
談了TCP協議連接建立的三次握手后,接下來再復習下linux socket協議棧的相關內容:
關於linux socket
linux網絡路徑
從上圖中可以清晰地看到socket在網絡分層中的位置,由於socket接口應該划在應用層,而我們日常的網絡編程也都基本在應用層上,所以下面將從發送端和接收端兩個角度分別對應用層進行分析:
發送端
應用層
(1) Socket
應用層的各種網絡應用程序基本上都是通過 Linux Socket 編程接口來和內核空間的網絡協議棧通信的。Linux Socket 是從 BSD Socket 發展而來的,它是 Linux 操作系統的重要組成部分之一,它是網絡應用程序的基礎。從層次上來說,它位於應用層,是操作系統為應用程序員提供的 API,通過它,應用程序可以訪問傳輸層協議。
- socket 位於傳輸層協議之上,屏蔽了不同網絡協議之間的差異
- socket 是網絡編程的入口,它提供了大量的系統調用,構成了網絡程序的主體
- 在Linux系統中,socket 屬於文件系統的一部分,網絡通信可以被看作是對文件的讀取,使得我們對網絡的控制和對文件的控制一樣方便。
TCP Socket 處理過程
(2) 應用層處理流程
- 網絡應用調用Socket API socket (int family, int type, int protocol) 創建一個 socket,該調用最終會調用 Linux system call socket() ,並最終調用 Linux Kernel 的 sock_create() 方法。該方法返回被創建好了的那個 socket 的 file descriptor。對於每一個 userspace 網絡應用創建的 socket,在內核中都有一個對應的 struct socket和 struct sock。其中,struct sock 有三個隊列(queue),分別是 rx , tx 和 err,在 sock 結構被初始化的時候,這些緩沖隊列也被初始化完成;在收據收發過程中,每個 queue 中保存要發送或者接受的每個 packet 對應的 Linux 網絡棧 sk_buffer 數據結構的實例 skb。
- 對於 TCP socket 來說,應用調用 connect()API ,使得客戶端和服務器端通過該 socket 建立一個虛擬連接。在此過程中,TCP 協議棧通過三次握手會建立 TCP 連接。默認地,該 API 會等到 TCP 握手完成連接建立后才返回。在建立連接的過程中的一個重要步驟是,確定雙方使用的 Maxium Segemet Size (MSS)。
- 應用調用 Linux Socket 的 send 或者 write API 來發出一個 message 給接收端
- sock_sendmsg 被調用,它使用 socket descriptor 獲取 sock struct,創建 message header 和 socket control message
- _sock_sendmsg 被調用,根據 socket 的協議類型,調用相應協議的發送函數。
對於 TCP ,調用 tcp_sendmsg 函數。
接收端
應用層
- 每當用戶應用調用 read 或者 recvfrom 時,該調用會被映射為/net/socket.c 中的 sys_recv 系統調用,並被轉化為 sys_recvfrom 調用,然后調用 sock_recgmsg 函數。
- 對於 INET 類型的 socket,/net/ipv4/af inet.c 中的 inet_recvmsg 方法會被調用,它會調用相關協議的數據接收方法。
- 對 TCP 來說,調用 tcp_recvmsg。該函數從 socket buffer 中拷貝數據到 user buffer。
TCP相關源碼深度分析及GDB跟蹤調試
虛擬機:ubuntu 16.04
內核版本:linux 5.0.1
編譯方式:x86-64
模擬器:qemu
基於系統:部署好TCP通信程序的Menu OS系統
相關目錄路徑:/net/ipv4、/net/socket.c
TCP相關系統接口定義
再次跑起來之前已經部署好TCP通信程序的Menu OS系統,追蹤與TCP連接相關的socket、connect、listen、accept函數的系統調用:
首先以調試模式運行Menu OS系統:
cd kernel qemu-system-x86_64 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -append nokaslr -s
新打開一個命令行運行GDB進行Menu OS的調試:
cd kernel file linux-5.0.1/vmlinux target remote:1234
設置相應的斷點:
b __sys_socket b __sys_connect b __sys_listen b __sys_accept4 info breakpoints
發現相應的socket系統調用函數都在 net/socket.c目錄下,打開該目錄,分析源代碼,其中socket接口函數都定義在SYSCALL_DEFINE接口里,找到主要的相關SYSCALL_DEFINE定義如下:
1 SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) 2 { 3 return __sys_socket(family, type, protocol); 4 } 5 6 SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen) 7 { 8 return __sys_bind(fd, umyaddr, addrlen); 9 } 10 11 SYSCALL_DEFINE2(listen, int, fd, int, backlog) 12 { 13 return __sys_listen(fd, backlog); 14 } 15 16 17 SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr, 18 int __user *, upeer_addrlen) 19 { 20 return __sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0); 21 } 22 23 SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr, 24 int, addrlen) 25 { 26 return __sys_connect(fd, uservaddr, addrlen); 27 } 28 29 SYSCALL_DEFINE3(getsockname, int, fd, struct sockaddr __user *, usockaddr, 30 int __user *, usockaddr_len) 31 { 32 return __sys_getsockname(fd, usockaddr, usockaddr_len); 33 } 34 35 SYSCALL_DEFINE3(getpeername, int, fd, struct sockaddr __user *, usockaddr, 36 int __user *, usockaddr_len) 37 { 38 return __sys_getpeername(fd, usockaddr, usockaddr_len); 39 } 40 41 42 SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len, 43 unsigned int, flags) 44 { 45 return __sys_sendto(fd, buff, len, flags, NULL, 0); 46 } 47 48 SYSCALL_DEFINE4(recv, int, fd, void __user *, ubuf, size_t, size, 49 unsigned int, flags) 50 { 51 return __sys_recvfrom(fd, ubuf, size, flags, NULL, NULL); 52 }
逐步分析TCP三次握手的系統級實現
TCP連接前的初始化過程
1)調用__sys_socket, __sys_socket源碼如下:
int __sys_socket(int family, int type, int protocol) { int retval; struct socket *sock; int flags; /* Check the SOCK_* constants for consistency. */ BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC); BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK); BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK); BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK); flags = type & ~SOCK_TYPE_MASK; if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK)) return -EINVAL; type &= SOCK_TYPE_MASK; if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK)) flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK; retval = sock_create(family, type, protocol, &sock); if (retval < 0) return retval; return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK)); }
可以看到,__sys_socket調用了sock_create和sock_map_fd函數;
2)調用sock_create():創建socket結構,針對每種不同的family的socket結構的初始化,就需要調用不同的create函數來完成。對應於inet類型的地址來說,在網絡協議初始化時調用sock_register()函數中完成注冊的定義如下:
struct net_proto_family inet_family_ops={ PF_INET; inet_create };
所以inet協議最后會調用inet_create函數。
3)調用inet_create: 初始化sock的狀態設置為SS_UNCONNECTED,申請一個新的sock結構,並且初始化socket的成員ops初始化為inet_stream_ops,而sock的成員prot初始化為tcp_prot。然后調用sock_init_data,將該socket結構的變量sock和sock類型的變量關聯起來。
inet_create函數源碼如下:
1 static int inet_create(struct net *net, struct socket *sock, int protocol,int kern) 2 { 3 ... 4 /* Look for the requested type/protocol pair. */ 5 lookup_protocol: 6 err = -ESOCKTNOSUPPORT; 7 rcu_read_lock(); 8 9 // TCP套接字、UDP套接字、原始套接字的inet_protosw實 例都在inetsw_array數組中定義, 10 //這些實例會調inet_register_protosw()注冊到inetsw中 11 //根據protocol查找要創建的套接字對應的四層傳輸協議。 12 list_for_each_entry_rcu(answer, &inetsw[sock->type], list) { 13 ... 14 } 15 16 //如果沒有找到,則調用request_module()來嘗試加載協議所屬的模塊,正常情況下不會發生。 17 if (unlikely(err)) { 18 if (try_loading_module < 2) { 19 rcu_read_unlock(); 20 ... 21 }
4)調用sock_map_fd()獲取一個未被使用的文件描述符,並且申請並初始化對應的file{}結構。
TCP連接的三次握手過程深度解析
接下來通過閱讀TCP源代碼的方式,一步步地追蹤解析系統級TCP三次握手的詳細過程。首先從__sys_connect源碼開始,逐步向深處探究:
1 int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen) 2 { 3 struct socket *sock; 4 struct sockaddr_storage address; 5 int err, fput_needed; 6 //得到socket對象 7 sock = sockfd_lookup_light(fd, &err, &fput_needed); 8 if (!sock) 9 goto out; 10 //將地址對象從用戶空間拷貝到內核空間 11 err = move_addr_to_kernel(uservaddr, addrlen, &address); 12 if (err < 0) 13 goto out_put; 14 //內核相關 15 err = 16 security_socket_connect(sock, (struct sockaddr *)&address, addrlen); 17 if (err) 18 goto out_put; 19 //對於流式套接字,sock->ops為 inet_stream_ops -->inet_stream_connect 20 21 //對於數據報套接字,sock->ops為 inet_dgram_ops --> inet_dgram_connect 22 err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen, 23 sock->file->f_flags); 24 out_put: 25 fput_light(sock->file, fput_needed); 26 out: 27 return err; 28 }
在該函數中做了三件事:
1. 根據文件描述符找到指定的socket對象;
2. 將地址信息從用戶空間拷貝到內核空間;
3. 調用指定類型套接字的connect函數。
對應流式套接字的connect函數是inet_stream_connect,接着我們分析該函數:
在GDB中設置斷點找到源文件:
1 int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr, 2 int addr_len, int flags) 3 { 4 int err; 5 6 lock_sock(sock->sk); 7 err = __inet_stream_connect(sock, uaddr, addr_len, flags); 8 release_sock(sock->sk); 9 return err; 10 } 11 12 /* 13 * Connect to a remote host. There is regrettably still a little 14 * TCP 'magic' in here. 15 */ 16 17 //1. 檢查socket地址長度和使用的協議族。 18 //2. 檢查socket的狀態,必須是SS_UNCONNECTED或SS_CONNECTING。 19 //3. 調用tcp_v4_connect()來發送SYN包。 20 //4. 等待后續握手的完成: 21 int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr, 22 int addr_len, int flags) 23 ... 24 后面太多便不再展示,可直接看源碼
該函數主要做了幾件事:
1. 檢查socket地址長度和使用的協議族;
2. 檢查socket的狀態,必須是SS_UNCONNECTED或SS_CONNECTING;
3. 調用實現協議的connect函數,對於流式套接字,實現協議是tcp,調用的是tcp_v4_connect();
4.對於阻塞調用,等待后續握手的完成;對於非阻塞調用,則直接返回 -EINPROGRESS。
我們先關注tcp_v4_connect,同樣先在gdb中設置斷點,找到源文件所在位置:
1 /* This will initiate an outgoing connection. */ 2 3 //對於TCP 協議來說,其連接實際上就是發送一個 SYN 報文,在服務器的應答到來時,回答它一個 ack 報文,也就是完成三次握手中的第一和第三次 4 int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len) 5 { 6 struct sockaddr_in *usin = (struct sockaddr_in *)uaddr; 7 struct inet_sock *inet = inet_sk(sk); 8 struct tcp_sock *tp = tcp_sk(sk); 9 __be16 orig_sport, orig_dport; 10 __be32 daddr, nexthop; 11 struct flowi4 *fl4; 12 struct rtable *rt; 13 int err; 14 struct ip_options_rcu *inet_opt; 15 16 if (addr_len < sizeof(struct sockaddr_in)) 17 return -EINVAL; 18 19 if (usin->sin_family != AF_INET) 20 return -EAFNOSUPPORT; 21 22 nexthop = daddr = usin->sin_addr.s_addr; 23 inet_opt = rcu_dereference_protected(inet->inet_opt, 24 lockdep_sock_is_held(sk)); 25 26 //將下一跳地址和目的地址的臨時變量都暫時設為用戶提交的地址。 27 if (inet_opt && inet_opt->opt.srr) { 28 if (!daddr) 29 return -EINVAL; 30 nexthop = inet_opt->opt.faddr; 31 } 32 33 //源端口 34 orig_sport = inet->inet_sport; 35 36 //目的端口 37 orig_dport = usin->sin_port; 38 39 fl4 = &inet->cork.fl.u.ip4; 40 41 //如果使用了來源地址路由,選擇一個合適的下一跳地址。 42 rt = ip_route_connect(fl4, nexthop, inet->inet_saddr, 43 RT_CONN_FLAGS(sk), sk->sk_bound_dev_if, 44 IPPROTO_TCP, 45 orig_sport, orig_dport, sk); 46 if (IS_ERR(rt)) { 47 err = PTR_ERR(rt); 48 if (err == -ENETUNREACH) 49 IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES); 50 return err; 51 } 52 ... 53 后面較長,不再展示。
在該函數主要完成:
1. 路由查找,得到下一跳地址,並更新socket對象的下一跳地址;
2. 將socket對象的狀態設置為TCP_SYN_SENT;
3. 如果沒設置序號初值,則選定一個隨機初值;
4. 調用函數tcp_connect完成報文構建和發送。
我接着看下tcp_connect:
1 /* Build a SYN and send it off. */ 2 //由tcp_v4_connect()->tcp_connect()->tcp_transmit_skb()發送,並置為TCP_SYN_SENT. 3 int tcp_connect(struct sock *sk) 4 { 5 struct tcp_sock *tp = tcp_sk(sk); 6 struct sk_buff *buff; 7 int err; 8 9 //初始化傳輸控制塊中與連接相關的成員 10 tcp_connect_init(sk); 11 12 if (unlikely(tp->repair)) { 13 tcp_finish_connect(sk, NULL); 14 return 0; 15 } 16 //分配skbuff --> 為SYN段分配報文並進行初始化 17 buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true); 18 if (unlikely(!buff)) 19 return -ENOBUFS; 20 21 //構建syn報文 22 23 //在函數tcp_v4_connect中write_seq已經被初始化隨機值 24 tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN); 25 26 tp->retrans_stamp = tcp_time_stamp; 27 28 //將報文添加到發送隊列上 29 tcp_connect_queue_skb(sk, buff); 30 31 //顯式擁塞通告 ---> 32 //路由器在出現擁塞時通知TCP。當TCP段傳遞時,路由器使用IP首部中的2位來記錄擁塞,當TCP段到達后, 33 //接收方知道報文段是否在某個位置經歷過擁塞。然而,需要了解擁塞發生情況的是發送方,而非接收方。因 34 //此,接收方使用下一個ACK通知發送方有擁塞發生,然后,發送方做出響應,縮小自己的擁塞窗口。 35 tcp_ecn_send_syn(sk, buff); 36 37 /* Send off SYN; include data in Fast Open. */ 38 err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) : 39 40 //構造tcp頭和ip頭並發送 41 tcp_transmit_skb(sk, buff, 1, sk->sk_allocation); 42 if (err == -ECONNREFUSED) 43 return err; 44 45 /* We change tp->snd_nxt after the tcp_transmit_skb() call 46 * in order to make this packet get counted in tcpOutSegs. 47 */ 48 tp->snd_nxt = tp->write_seq; 49 tp->pushed_seq = tp->write_seq; 50 TCP_INC_STATS(sock_net(sk), TCP_MIB_ACTIVEOPENS); 51 52 /* Timer for repeating the SYN until an answer. */ 53 54 //啟動重傳定時器 55 inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, 56 inet_csk(sk)->icsk_rto, TCP_RTO_MAX); 57 return 0; 58 }
該函數完成:
1. 初始化套接字跟連接相關的字段;
2. 申請sk_buff空間;
3 . 將sk_buff初始化為syn報文,實質是操作tcp_skb_cb,在初始化TCP頭的時候會用到;
4 . 調用tcp_connect_queue_skb()函數將報文sk_buff添加到發送隊列sk->sk_write_queue;
5 . 調用tcp_transmit_skb()函數構造tcp頭,然后交給網絡層;
6. 初始化重傳定時器。
接着我們進入tcp_connect_queue_skb:
1 /* This routine actually transmits TCP packets queued in by 2 * tcp_do_sendmsg(). This is used by both the initial 3 * transmission and possible later retransmissions. 4 * All SKB's seen here are completely headerless. It is our 5 * job to build the TCP header, and pass the packet down to 6 * IP so it can do the same plus pass the packet off to the 7 * device. 8 * 9 * We are working here with either a clone of the original 10 * SKB, or a fresh unique copy made by the retransmit engine. 11 */ 12 static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it, 13 gfp_t gfp_mask) 14 { 15 const struct inet_connection_sock *icsk = inet_csk(sk); 16 struct inet_sock *inet; 17 struct tcp_sock *tp; 18 struct tcp_skb_cb *tcb; 19 struct tcp_out_options opts; 20 unsigned int tcp_options_size, tcp_header_size; 21 struct tcp_md5sig_key *md5; 22 struct tcphdr *th; 23 int err; 24 25 BUG_ON(!skb || !tcp_skb_pcount(skb)); 26 tp = tcp_sk(sk); 27 28 //根據傳遞進來的clone_it參數來確定是否需要克隆待發送的報文 29 if (clone_it) { 30 skb_mstamp_get(&skb->skb_mstamp); 31 TCP_SKB_CB(skb)->tx.in_flight = TCP_SKB_CB(skb)->end_seq 32 - tp->snd_una; 33 tcp_rate_skb_sent(sk, skb); 34 35 //如果一個SKB會被不同的用戶獨立操作,而這些用戶可能只是修改SKB描述符中的某些字段值,如h、nh,則內核沒有必要為每個用戶復制一份完整 36 //的SKB描述及其相應的數據緩存區,而會為了提高性能,只作克隆操作。克隆過程只復制SKB描述符,同時增加數據緩存區的引用計數,以免共享數 37 //據被提前釋放。完成這些功能的是skb_clone()。一個使用包克隆的場景是,一個接收包程序要把該包傳遞給多個接收者,例如包處理函數或者一 38 //個或多個網絡模塊。原始的及克隆的SKB描述符的cloned值都會被設置為1,克隆SKB描述符的users值置為1,這樣在第一次釋放時就會釋放掉。同時 39 //將數據緩存區引用計數dataref遞增1,因為又多了一個克隆SKB描述符指向它 40 41 if (unlikely(skb_cloned(skb))) 42 //如果skb已經被clone,則只能復制該skb的數據到新分配的skb中 43 skb = pskb_copy(skb, gfp_mask); 44 else 45 skb = skb_clone(skb, gfp_mask); 46 if (unlikely(!skb)) 47 return -ENOBUFS; 48 } 49 ... 50 后面較長,不再展示。
可以看到主要是移動sk_buff的data指針,然后填充TCP頭,接着的事就是交給網絡層,將報文發出。這樣三次握手中的第一次握手在客戶端的層面完成,報文到達服務端,由服務端處理完畢后,第一次握手完成,客戶端socket狀態變為TCP_SYN_SENT。下面我們看下服務端的處理。
數據到達網卡的時候,對於TCP協議,將大致要經過這個一個調用鏈:
網卡驅動 ---> netif_receive_skb() ---> ip_rcv() ---> ip_local_deliver_finish() ---> tcp_v4_rcv()
我們直接看tcp_v4_rcv():
1 /* 2 * From tcp_input.c 3 */ 4 5 //網卡驅動-->netif_receive_skb()--->ip_rcv()--->ip_local_deliver_finish()---> tcp_v4_rcv() 6 int tcp_v4_rcv(struct sk_buff *skb) 7 { 8 struct net *net = dev_net(skb->dev); 9 const struct iphdr *iph; 10 const struct tcphdr *th; 11 bool refcounted; 12 struct sock *sk; 13 int ret; 14 15 //如果不是發往本地的數據包,則直接丟棄 16 if (skb->pkt_type != PACKET_HOST) 17 goto discard_it; 18 19 /* Count it even if it's bad */ 20 __TCP_INC_STATS(net, TCP_MIB_INSEGS); 21 22 23 ////包長是否大於TCP頭的長度 24 if (!pskb_may_pull(skb, sizeof(struct tcphdr))) 25 goto discard_it; 26 27 //tcp頭 --> 不是很懂為何老是獲取tcp頭 28 th = (const struct tcphdr *)skb->data; 29 ... 30 后面較長,不再展示。
該函數主要工作就是根據tcp頭部信息查到處理報文的socket對象,然后檢查socket狀態做不同處理,我們這里是監聽狀態TCP_LISTEN,直接調用函數tcp_v4_do_rcv():
1 /* The socket must have it's spinlock held when we get 2 * here, unless it is a TCP_LISTEN socket. 3 * 4 * We have a potential double-lock case here, so even when 5 * doing backlog processing we use the BH locking scheme. 6 * This is because we cannot sleep with the original spinlock 7 * held. 8 */ 9 10 //網卡驅動-->netif_receive_skb()--->ip_rcv()--->ip_local_deliver_finish()---> tcp_v4_rcv() 11 12 13 //tcp_v4_do_rcv() -> tcp_rcv_state_process() -> tcp_v4_conn_request() -> tcp_v4_send_synack() 14 int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb) 15 { 16 struct sock *rsk; 17 18 //如果是連接已建立狀態 19 if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */ 20 struct dst_entry *dst = sk->sk_rx_dst; 21 22 sock_rps_save_rxhash(sk, skb); 23 sk_mark_napi_id(sk, skb); 24 if (dst) { 25 if (inet_sk(sk)->rx_dst_ifindex != skb->skb_iif || 26 !dst->ops->check(dst, 0)) { 27 dst_release(dst); 28 sk->sk_rx_dst = NULL; 29 } 30 } 31 tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len); 32 return 0; 33 } 34 ... 35 后面較長,不再展示。
在這里並沒有很多代碼,做的東西也不是很多,對於監聽狀態的套接字,主要是一個SYN FLOOD防范相關的東西,不是我們研究的重點;接着就是調用tcp_rcv_state_process():
1 /* 2 * This function implements the receiving procedure of RFC 793 for 3 * all states except ESTABLISHED and TIME_WAIT. 4 * It's called from both tcp_v4_rcv and tcp_v6_rcv and should be 5 * address independent. 6 */ 7 8 9 //除了ESTABLISHED和TIME_WAIT狀態外,其他狀態下的TCP段處理都由本函數實現 10 11 // tcp_v4_do_rcv() -> tcp_rcv_state_process() -> tcp_v4_conn_request() -> tcp_v4_send_synack(). 12 int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb) 13 { 14 struct tcp_sock *tp = tcp_sk(sk); 15 struct inet_connection_sock *icsk = inet_csk(sk); 16 const struct tcphdr *th = tcp_hdr(skb); 17 struct request_sock *req; 18 int queued = 0; 19 bool acceptable; 20 21 switch (sk->sk_state) { 22 23 //SYN_RECV狀態的處理 24 case TCP_CLOSE: 25 goto discard; 26 27 //服務端第一次握手處理 28 case TCP_LISTEN: 29 if (th->ack) 30 return 1; 31 32 if (th->rst) 33 goto discard; 34 35 if (th->syn) { 36 if (th->fin) 37 goto discard; 38 // tcp_v4_do_rcv() -> tcp_rcv_state_process() -> tcp_v4_conn_request() -> tcp_v4_send_synack(). 39 if (icsk->icsk_af_ops->conn_request(sk, skb) < 0) 40 return 1; 41 42 consume_skb(skb); 43 return 0; 44 } 45 goto discard; 46 47 ...
這是TCP建立連接的核心所在,幾乎所有狀態的套接字,在收到數據報時都在這里完成處理。對於服務端來說,收到第一次握手報文時的狀態為TCP_LISTEN,處理代碼為:
1 //服務端第一次握手處理 2 case TCP_LISTEN: 3 if (th->ack) 4 return 1; 5 6 if (th->rst) 7 goto discard; 8 9 if (th->syn) { 10 if (th->fin) 11 goto discard; 12 // tcp_v4_do_rcv() -> tcp_rcv_state_process() -> tcp_v4_conn_request() -> tcp_v4_send_synack(). 13 if (icsk->icsk_af_ops->conn_request(sk, skb) < 0) 14 return 1; 15 16 consume_skb(skb); 17 return 0; 18 } 19 goto discard;
接下將由tcp_v4_conn_request函數處理,而tcp_v4_conn_request實際上調用tcp_conn_request:
1 // tcp_v4_do_rcv() -> tcp_rcv_state_process() -> tcp_v4_conn_request() -> tcp_v4_send_synack(). 2 int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb) 3 { 4 /* Never answer to SYNs send to broadcast or multicast */ 5 if (skb_rtable(skb)->rt_flags & (RTCF_BROADCAST | RTCF_MULTICAST)) 6 goto drop; 7 8 //tcp_request_sock_ops 定義在 tcp_ipv4.c 1256行 9 10 //inet_init --> proto_register --> req_prot_init -->初始化cache名 11 return tcp_conn_request(&tcp_request_sock_ops, 12 &tcp_request_sock_ipv4_ops, sk, skb); 13 14 drop: 15 tcp_listendrop(sk); 16 return 0; 17 } 18 int tcp_conn_request(struct request_sock_ops *rsk_ops, 19 const struct tcp_request_sock_ops *af_ops, 20 struct sock *sk, struct sk_buff *skb) 21 ...
在該函數中做了不少的事情,但是我們這里重點了解兩點:
1. 分配一個request_sock對象來代表這次連接請求(狀態為TCP_NEW_SYN_RECV),如果沒有設置防范syn flood相關的選項,則將該request_sock添加到established狀態的tcp_sock散列表(如果設置了防范選項,則request_sock對象都沒有,只有建立完成時才會分配);
2. 調用tcp_v4_send_synack回復客戶端ack,開啟第二次握手。
我們看下該函數:
1 //向客戶端發送SYN+ACK報文 2 static int tcp_v4_send_synack(const struct sock *sk, struct dst_entry *dst, 3 struct flowi *fl, 4 struct request_sock *req, 5 struct tcp_fastopen_cookie *foc, 6 enum tcp_synack_type synack_type) 7 { 8 const struct inet_request_sock *ireq = inet_rsk(req); 9 struct flowi4 fl4; 10 int err = -1; 11 struct sk_buff *skb; 12 13 /* First, grab a route. */ 14 15 //查找到客戶端的路由 16 if (!dst && (dst = inet_csk_route_req(sk, &fl4, req)) == NULL) 17 return -1; 18 19 //根據路由、傳輸控制塊、連接請求塊中的構建SYN+ACK段 20 skb = tcp_make_synack(sk, dst, req, foc, synack_type); 21 22 //生成SYN+ACK段成功 23 if (skb) { 24 25 //生成校驗碼 26 __tcp_v4_send_check(skb, ireq->ir_loc_addr, ireq->ir_rmt_addr); 27 28 29 //生成IP數據報並發送出去 30 err = ip_build_and_send_pkt(skb, sk, ireq->ir_loc_addr, 31 ireq->ir_rmt_addr, 32 ireq->opt); 33 err = net_xmit_eval(err); 34 } 35 36 return err; 37 }
代碼較少,查找客戶端路由,構造syn包,然后調用ip_build_and_send_pkt,依靠網絡層將數據報發出去。至此,第一次握手完成,第二次握手服務端層面完成。
數據報到達客戶端網卡,同樣經過:網卡驅動-->netif_receive_skb()--->ip_rcv()--->ip_local_deliver_finish()---> tcp_v4_rcv() --> tcp_v4_do_rcv() 。
客戶端socket的狀態為TCP_SYN_SENT,所以直接進入tcp_rcv_state_process,處理該狀態的代碼為:
1 //客戶端第二次握手處理 2 case TCP_SYN_SENT: 3 tp->rx_opt.saw_tstamp = 0; 4 5 //處理SYN_SENT狀態下接收到的TCP段 6 queued = tcp_rcv_synsent_state_process(sk, skb, th); 7 if (queued >= 0) 8 return queued; 9 10 /* Do step6 onward by hand. */ 11 12 //處理完第二次握手后,還需要處理帶外數據 13 tcp_urg(sk, skb, th); 14 __kfree_skb(skb); 15 16 //檢測是否有數據需要發送 17 tcp_data_snd_check(sk); 18 return 0; 19 }
接着看tcp_rcv_synsent_state_process:
1 //在SYN_SENT狀態下處理接收到的段,但是不處理帶外數據 2 static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb, 3 const struct tcphdr *th) 4 { 5 struct inet_connection_sock *icsk = inet_csk(sk); 6 struct tcp_sock *tp = tcp_sk(sk); 7 struct tcp_fastopen_cookie foc = { .len = -1 }; 8 int saved_clamp = tp->rx_opt.mss_clamp; 9 10 //解析TCP選項並保存到傳輸控制塊中 11 tcp_parse_options(skb, &tp->rx_opt, 0, &foc); 12 if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr) 13 tp->rx_opt.rcv_tsecr -= tp->tsoffset; 14 15 ...
處理三種可能的包:
1. 帶ack標志的,這是我們預期的;
2. 帶rst標志的,直接丟掉傳輸控制塊;
3. 帶syn標志,但是沒有ack標志,兩者同時發起連接。
我們重點研究第一種情況。首先調用tcp_finish_connect設置sock狀態為TCP_ESTABLISHED:
1 void tcp_finish_connect(struct sock *sk, struct sk_buff *skb) 2 { 3 struct tcp_sock *tp = tcp_sk(sk); 4 struct inet_connection_sock *icsk = inet_csk(sk); 5 6 //設置sock狀態為TCP_ESTABLISHED 7 tcp_set_state(sk, TCP_ESTABLISHED); 8 9 if (skb) { 10 icsk->icsk_af_ops->sk_rx_dst_set(sk, skb); 11 security_inet_conn_established(sk, skb); 12 } 13 ...
接着延時發送或立即發送確認ack,我們先不去了解延時確認的東西,我們直接看直接發送確認ack,調用tcp_send_ack:
1 //主動連接時,向服務器端發送ACK完成連接,並更新窗口 2 void tcp_send_ack(struct sock *sk) 3 { 4 struct sk_buff *buff; 5 6 /* If we have been reset, we may not send again. */ 7 if (sk->sk_state == TCP_CLOSE) 8 return; 9 10 tcp_ca_event(sk, CA_EVENT_NON_DELAYED_ACK); 11 ...
比較簡單,無非是構造報文,然后交給網絡層發送。至此第二次握手完成,客戶端sock狀態變為TCP_ESTABLISHED,第三次握手開始。我們之前說到服務端的sock的狀態為TCP_NEW_SYN_RECV,報文到達網卡:
網卡驅動-->netif_receive_skb()--->ip_rcv()--->ip_local_deliver_finish()---> tcp_v4_rcv(),報文將被以下代碼處理:
1 //網卡驅動-->netif_receive_skb()--->ip_rcv()--->ip_local_deliver_finish()---> tcp_v4_rcv() 2 int tcp_v4_rcv(struct sk_buff *skb) 3 { 4 ............. 5 6 7 //收到握手最后一個ack后,會找到TCP_NEW_SYN_RECV狀態的req,然后創建一個新的sock進入TCP_SYN_RECV狀態,最終進入TCP_ESTABLISHED狀態. 並放入accept隊列通知select/epoll 8 if (sk->sk_state == TCP_NEW_SYN_RECV) { 9 struct request_sock *req = inet_reqsk(sk); 10 struct sock *nsk; 11 12 sk = req->rsk_listener; 13 if (unlikely(tcp_v4_inbound_md5_hash(sk, skb))) { 14 sk_drops_add(sk, skb); 15 reqsk_put(req); 16 goto discard_it; 17 } 18 ...
看下如何創建新sock,進入tcp_check_req():
1 struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb, 2 struct request_sock *req, 3 bool fastopen) 4 { 5 struct tcp_options_received tmp_opt; 6 struct sock *child; 7 const struct tcphdr *th = tcp_hdr(skb); 8 __be32 flg = tcp_flag_word(th) & (TCP_FLAG_RST|TCP_FLAG_SYN|TCP_FLAG_ACK); 9 bool paws_reject = false; 10 bool own_req; 11 ...
又回到了函數tcp_rcv_state_process,TCP_SYN_RECV狀態的套接字將由一下代碼處理:
1 //服務端第三次握手處理 2 case TCP_SYN_RECV: 3 if (!acceptable) 4 return 1; 5 6 if (!tp->srtt_us) 7 tcp_synack_rtt_meas(sk, req); 8 9 /* Once we leave TCP_SYN_RECV, we no longer need req 10 * so release it. 11 */ 12 if (req) { 13 inet_csk(sk)->icsk_retransmits = 0; 14 reqsk_fastopen_remove(sk, req, false); 15 } else { 16 /* Make sure socket is routed, for correct metrics. */ 17 18 //建立路由,初始化擁塞控制模塊 19 icsk->icsk_af_ops->rebuild_header(sk); 20 tcp_init_congestion_control(sk); 21 22 tcp_mtup_init(sk); 23 tp->copied_seq = tp->rcv_nxt; 24 tcp_init_buffer_space(sk); 25 } 26 smp_mb(); 27 //正常的第三次握手,設置連接狀態為TCP_ESTABLISHED 28 tcp_set_state(sk, TCP_ESTABLISHED); 29 sk->sk_state_change(sk); 30 31 /* Note, that this wakeup is only for marginal crossed SYN case. 32 * Passively open sockets are not waked up, because 33 * sk->sk_sleep == NULL and sk->sk_socket == NULL. 34 */ 35 36 //狀態已經正常,喚醒那些等待的線程 37 if (sk->sk_socket) 38 sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT); 39 40 tp->snd_una = TCP_SKB_CB(skb)->ack_seq; 41 tp->snd_wnd = ntohs(th->window) << tp->rx_opt.snd_wscale; 42 tcp_init_wl(tp, TCP_SKB_CB(skb)->seq); 43 44 if (tp->rx_opt.tstamp_ok) 45 tp->advmss -= TCPOLEN_TSTAMP_ALIGNED; 46 47 if (req) { 48 /* Re-arm the timer because data may have been sent out. 49 * This is similar to the regular data transmission case 50 * when new data has just been ack'ed. 51 * 52 * (TFO) - we could try to be more aggressive and 53 * retransmitting any data sooner based on when they 54 * are sent out. 55 */ 56 tcp_rearm_rto(sk); 57 } else 58 tcp_init_metrics(sk); 59 60 if (!inet_csk(sk)->icsk_ca_ops->cong_control) 61 tcp_update_pacing_rate(sk); 62 63 /* Prevent spurious tcp_cwnd_restart() on first data packet */ 64 65 //更新最近一次發送數據包的時間 66 tp->lsndtime = tcp_time_stamp; 67 68 tcp_initialize_rcv_mss(sk); 69 70 //計算有關TCP首部預測的標志 71 tcp_fast_path_on(tp); 72 break;
可以看到到代碼對sock的窗口,mss等進行設置,以及最后將sock的狀態設置為TCP_ESTABLISHED,至此三次握手完成。等待用戶調用accept調用,取出套接字使用。
分析過程中的斷點設置情況如下圖:
現在給出大體的TCP三次握手協議棧從上至下提供的接口,具體的詳細過程見上面的分析:
TCP三次握手協議棧從上至下提供的接口
如下圖所示:
總結
本文內容主要分為三個部分,首先講述了TCP協議的相關知識,接着談到了與TCP通信聯系密切的linux socket接口函數及應用層處理TCP連接的詳細流程,最后通過gdb調試加閱讀源碼的方式,一步步揭開socket接口函數背后的TCP三次握手的神秘面紗,詳細地從linux 系統函數調用的角度理解了TCP連接建立三次握手的過程,相信我們對TCP協議的理解又有了一個新的高度!