TCP的自连接是网络编程中的又一个“坑”。
先写一个简单的脚本来展示这个问题:
  1. # cat self_connect.sh
  2. #!/bin/bash
  3. while true; do
  4.     telnet 127.0.0.1 44444
  5. done
脚本很简单,不停地去连接本机的端口号44444。

看一下本机的44444端口状态:
  1. # netstat -anp | grep 444444

没有任何程序在监听该端口,所以连接不可能被建立。

运行程序,输出如下:
  1. ./self_connect.sh
  2. telnet: Unable to connect to remote host: Connection refused
  3. Trying 127.0.0.1...
  4. telnet: Unable to connect to remote host: Connection refused
  5. Trying 127.0.0.1...
  6. telnet: Unable to connect to remote host: Connection refused
  7. Trying 127.0.0.1...
  8. # 中间省略
  9. telnet: Unable to connect to remote host: Connection refused
  10. Trying 127.0.0.1...
  11. telnet: Unable to connect to remote host: Connection refused
  12. Trying 127.0.0.1...
  13. Connected to 127.0.0.1.
我们发现,脚本在多次连接失败后,竟然连接上了。再来看下端口状态:
  1. # netstat -anp | grep 44444
  2. tcp 0 0 127.0.0.1:44444 127.0.0.1:44444 ESTABLISHED 8668/telnet
状态确实是ESTABLISHED,更奇怪的是连接的源和目标端口都是44444。

为了更清晰地了解情况,我们使用tcpdump工具来抓包分析:
  1. # tcpdump -i lo
  2. 前面省略
  3. 19:22:02.416705 IP localhost.44441 > localhost.44444: Flags [S], seq 3362395666, win 43690, options [mss 65495,sackOK,TS val 2589409478 ecr 0,nop,wscale 7], length 0
  4. 19:22:02.416715 IP localhost.44444 > localhost.44441: Flags [R.], seq 0, ack 3362395667, win 0, length 0
  5. 19:22:02.418525 IP localhost.44442 > localhost.44444: Flags [S], seq 2268272069, win 43690, options [mss 65495,sackOK,TS val 2589409479 ecr 0,nop,wscale 7], length 0
  6. 19:22:02.418536 IP localhost.44444 > localhost.44442: Flags [R.], seq 0, ack 2268272070, win 0, length 0
  7. 19:22:02.420240 IP localhost.44443 > localhost.44444: Flags [S], seq 3381102759, win 43690, options [mss 65495,sackOK,TS val 2589409479 ecr 0,nop,wscale 7], length 0
  8. 19:22:02.420251 IP localhost.44444 > localhost.44443: Flags [R.], seq 0, ack 3381102760, win 0, length 0
  9. 19:22:02.421920 IP localhost.44444 > localhost.44444: Flags [S], seq 4242302188, win 43690, options [mss 65495,sackOK,TS val 2589409479 ecr 0,nop,wscale 7], length 0
  10. 19:22:02.421932 IP localhost.44444 > localhost.44444: Flags [S.], seq 4242302188, ack 4242302189, win 43690, options [mss 65495,sackOK,TS val 2589409479 ecr 2589409479,nop,wscale 7], length 0
  11. 19:22:02.421938 IP localhost.44444 > localhost.44444: Flags [.], ack 1, win 342, options [nop,nop,TS val 2589409479 ecr 2589409479], length 0
从tcpdump的输出,我们可以发现几点:
1. 每次连接失败后,下一次进程连接使用的端口号都是前一次加1
2. 连接失败的情况,进程向44444端口发送SYN后,由于44444端口没有被监听,进程收到RST分节后退出。
3. 当进程使用44444端口发起连接时,三次握手正常完成了。

这里可以提出两点疑问:1. 发起连接时系统是如何分配端口号的? 2. 如果源端口号和目标端口号相同,会发生什么?我们分别来解答。

发起连接时系统是如何分配端口号的?


因为我们的程序没有使用bind绑定端口号,内核会自动为我们分配一个端口,可以分配的端口称作ephemeral port,使用如下命令查看ephemeral port范围:
  1. $ cat /proc/sys/net/ipv4/ip_local_port_range
  2. 32768 61000
在本台机子上,内核可以自动分配的端口范围为32768 - 61000。于是,我们的测试脚本执行时就会出现源端口号和目标端口号相同的情况。

如果源端口号和目标端口号相同,会发生什么?


在前面用tcpdump抓包时,我们看到内核是正常完成了三次握手,那么具体是怎么做的呢?

对于这个问题,linux邮件列表也进行过讨论(详见:https://lkml.org/lkml/2007/10/21/228),内核维护人员认为:
simultaneous connect又是什么呢?这是指相互独立的两台主机的两个tcp套接字, 可能会在同一时刻向对方发起连接。
该过程如下:
socket A和socket B同时向对方发送SYN分节,分别接收到SYN又分别发送了SYN和ACK,两者接收到后又分别发送了ACK,进入ESTABLISHED状态。

linux内核对这种情况进行了处理。在文件net/ipv4/tcp_input.c函数tcp_rcv_synsent_state_process()中,有一段代码:
  1. if (th->syn) {
  2.         /* We see SYN without ACK. It is attempt of
  3.          * simultaneous connect with crossed SYNs.
  4.          * Particularly, it can be connect to self.
  5.         */
  6.         tcp_set_state(sk, TCP_SYN_RECV);
  7.         ...
  8. }
这段代码是在三次握手时发送第一个SYN后执行的,if条件指的是发送完SYN后又接收到了SYN,那么我们就认为这种情况simultaneous connect了,并且注释中还写了这很可能是自连接。代码就不看下去了,这里只需要知道内核确实做了处理,并且这个处理导致自己连接自己是可行的。


小结一下,自连接指的是当源ip和目标ip、源端口和目标端口都相同时,出现的自己连上自己的情况。为了避免这种情况,我们需要使目标端口不在ephemeral port范围内。可以修改目标端口,也可以修改/proc/sys/net/ipv4/ip_local_port_range文件。另外,本文浅析了出现自连接的原因,内核人员认为这个问题不是一个bug,这属于simultaneous connect,因此,需要我们自己在实际应用中避免这个情况。

12-23 09:27