跟我一起学TCP/IP

Web、Mail、Ftp、DNS、Proxy、VPN、Samba、LDAP 等基础网络服务
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#31

帖子 723937936@qq.com » 2023-03-27 10:24

第十八章:TCP Connection Establishment and Termination

连接建立:

1. 客户端发送SYN segment,目的是:通告自己的ISN和MSS
2. 服务端发送SYN+ACK segment,目的是:除了通告自己的ISN和MSS外,顺带确认已收到客户端发送的SYN segment
3. 客户端发送ACK segment,目的是:确认收到服务端发送的SYN+ACK segment

上述过程也被称为:three-way handshake

几点说明:
1. 第1步的SYN segment是由connect函数触发的
2. 第2步的SYN+ACK segment是TCP模块自动生成的
3. 客户端收到SYN+ACK segment后,connect函数才返回
4. 服务端收到ACK segment后,accept函数才返回

先发送SYN segment的一端(一般是客户端)称为执行一个active open
后发送SYN+ACK segment的一端(一般是服务端)称为执行一个passive open
另外还可能发生同时打开(simultaneous open)的情况,也就是两端都执行active open

连接终止:

为了便于描述,下文称两端为A端和B端

1. A端发送FIN segment,通知B端我已经没有数据要发送了
2. B端发送ACK segment,确认已收到A端发送的FIN segment
3. B端发送FIN segment,通知A端我已经没有数据要发送了
4. A端发送ACK segment,确认已收到B端发送的FIN segment

几点说明:
1. FIN segment是由close或shutdown函数触发的
2. ACK segment是TCP模块自动生成的
3. 收到FIN的一端的应用的read/recv/recvmsg等操作会返回EOF


先发送FIN segment的一端称为执行一个active close
后发送FIN segment的一端称为执行一个passive close
另外还可能发生同时关闭(simultaneous close)的情况,也就是两端都执行active close

两端都需要发送一个FIN segment,是因为TCP是全双工协议,两端都要执行关闭连接的操作,每一端的关闭操作称为half-close(关闭连接的发送方向)

观察连接建立与终止

代码: 全选

$ sudo ./mytcpdump port 8888
 0.000 ( 0.000) IP 192.168.0.3.50396 > 192.168.0.6.8888: flags [S], seq 634664549, win 65535, options [mss 1460], length 0
 0.000 ( 0.000) IP 192.168.0.6.8888 > 192.168.0.3.50396: flags [SA], seq 3060868865, ack 634664550, win 65160, options [mss 1460], length 0
 0.001 ( 0.001) IP 192.168.0.3.50396 > 192.168.0.6.8888: flags [A], seq 634664550, ack 3060868866, win 2058, options [], length 0

 5.990 ( 5.989) IP 192.168.0.3.50396 > 192.168.0.6.8888: flags [FA], seq 634664550, ack 3060868866, win 2058, options [], length 0
 5.990 ( 0.000) IP 192.168.0.6.8888 > 192.168.0.3.50396: flags [FA], seq 3060868866, ack 634664551, win 510, options [], length 0
 5.990 ( 0.000) IP 192.168.0.3.50396 > 192.168.0.6.8888: flags [A], seq 634664551, ack 3060868867, win 2058, options [], length 0
在上一个帖子已经描述了上述的输出,这里再强调两点:
1. SYN segment和FIN segment 都消耗一个sequence number
2. SYN segment除了通告对端自己的ISN外,还通告对端自己期望接收的MSS(表示TCP segment里data的最大字节数)


TCP模块发送的segment大小主要由两个值决定:
1.对端通告的MSS
2.自己发现的pMTU

TCP模块发送的segment大小由这两个值中的较小值决定

注意:
MSS指的对segment里data的大小限制
pMTU指的是对整个IP Datagram的大小限制


观察连接建立超时重传

在arp cache里添加一个不存在的主机的硬件地址(避免IP模块调用ARP请求)

代码: 全选

$ sudo arp -s 192.168.0.8 90:9c:4a:c0:be:d1
$ arp -n
Address                  HWtype  HWaddress           Flags Mask            Iface
192.168.0.3              ether   90:9c:4a:c0:be:d0   C                     enp0s3
192.168.0.8              ether   90:9c:4a:c0:be:d1   CM                    enp0s3
192.168.0.1              ether   2c:61:04:ba:ff:fa   C                     enp0s3
然后使用nc命令建立连接:

代码: 全选

$ time nc 192.168.0.8 8888

real	2m9.874s
user	0m0.002s
sys	0m0.000s
mytcpdump的输出:

代码: 全选

$ sudo ./mytcpdump -i enp0s3 port 8888
 0.000 ( 0.000) IP 192.168.0.6.39996 > 192.168.0.8.8888: flags [S], seq 1324841680, win 64240, options [mss 1460], length 0
 1.005 ( 1.005) IP 192.168.0.6.39996 > 192.168.0.8.8888: flags [S], seq 1324841680, win 64240, options [mss 1460], length 0
 3.025 ( 2.020) IP 192.168.0.6.39996 > 192.168.0.8.8888: flags [S], seq 1324841680, win 64240, options [mss 1460], length 0
 7.309 ( 4.284) IP 192.168.0.6.39996 > 192.168.0.8.8888: flags [S], seq 1324841680, win 64240, options [mss 1460], length 0
15.438 ( 8.129) IP 192.168.0.6.39996 > 192.168.0.8.8888: flags [S], seq 1324841680, win 64240, options [mss 1460], length 0
31.569 (16.131) IP 192.168.0.6.39996 > 192.168.0.8.8888: flags [S], seq 1324841680, win 64240, options [mss 1460], length 0
64.428 (32.859) IP 192.168.0.6.39996 > 192.168.0.8.8888: flags [S], seq 1324841680, win 64240, options [mss 1460], length 0
从上述的输出看:
1. time命令报告的时间大概130s(最后一个SYN segment需要再过大约32.859*2=65.718秒后超时,总时间大约为64.428+65.718=130.146)
2. tcp模块一共重传了6次,每次重传的时间间隔(RTO)都是上次的2倍(括号里的时间是距离前一个packet的时间)

建立连接过程中的syn的重传次数可以通过/proc/sys/net/ipv4/tcp_syn_retries文件配置

代码: 全选

$ cat /proc/sys/net/ipv4/tcp_syn_retries
6
TCP Half-Close

我理解half-close有两个含义:
1. TCP的half-close:指的是一端关闭发送方向,即触发FIN segment,从这个角度来说,close和shutdown(how=1)都执行TCP的half-close
2. FD的half-close:指的是关闭文件描述符的write方向,这个只能通过shudown函数(how=1),同样触发TCP的half-close,即:触发FIN segment

另外:
shutdown还可以指定how=0,关闭文件描述符的读方向,但是并不会触发TCP的half-close,即:不会触发FIN segment

我们通常说的half-close,一般指的是通过shudown函数(how=1)来触发TCP的half-close
上次由 723937936@qq.com 在 2023-03-27 20:18,总共编辑 1 次。
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#32

帖子 723937936@qq.com » 2023-03-27 19:03

TCP State Transition Diagram
Screen Shot 2023-03-27 at 6.57.17 PM.png
如上图所示:
对于客户端,通常的状态过渡由粗实线表示
对于服务端,通常的状态过渡由粗虚线表示

下面我们用netstat命令观察各种连接状态

客户端的状态过渡:

SYN_SENT状态

我们知道客户端调用connect函数会触发SYN segment的发送,然后客户端进入SYN_SENT状态
为了让客户端停留在这个状态,我们用iptables过滤掉相关的输入segments

代码: 全选

$ sudo iptables -t filter -A INPUT -p tcp -m tcp --sport 8888 -j DROP    # 过滤源端口为8888的输入segments
使用nc命令建立tcp连接

代码: 全选

$ nc 192.168.0.1 8888
使用netstat命令查看连接

代码: 全选

$ netstat -4tan
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      1 192.168.0.6:55472       192.168.0.1:8888        SYN_SENT
...        # 删除一些无关的行
netstat报告该连接(192.168.0.6:55472 192.168.0.1:8888)处于SYN_SENT状态

ESTABLISHED状态

先清除掉前面添加的规则

代码: 全选

$ sudo iptables -F
使用nc命令建立tcp连接

代码: 全选

$ nc 192.168.0.1 80           # 路由器一般都有web管理功能,所以可以连接80端口
使用netstat命令查看连接

代码: 全选

$ netstat -4tan
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      0 192.168.0.6:49852       192.168.0.1:80          ESTABLISHED
...        # 删除一些无关的行
FIN_WAIT1状态

先使用nc命令建立tcp连接

代码: 全选

$ nc 192.168.0.1 80
^C            # 等下一步做完后,再按Ctrl-C杀掉nc进程
然后用iptables过滤掉源端口为80的输入segments

代码: 全选

$ sudo iptables -t filter -A INPUT -p tcp -m tcp --sport 80 -j DROP
使用netstat命令查看连接

代码: 全选

$ netstat -4tan
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      1 192.168.0.6:45836       192.168.0.1:80          FIN_WAIT1
...        # 删除一些无关的行
FIN_WAIT2状态

先清除掉前面添加的过滤规则,然后添加规则过滤掉输入的FIN segments

代码: 全选

$ sudo iptables -F
$ sudo iptables -t filter -A INPUT -p tcp -m tcp --sport 80 --tcp-flags FIN FIN -j DROP
先使用nc命令建立tcp连接

代码: 全选

$ nc 192.168.0.1 80
^C            # 立刻杀掉nc进程
然后使用netstat命令查看连接

代码: 全选

$ netstat -4tan
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      0 192.168.0.6:50222       192.168.0.1:80          FIN_WAIT2
...        # 删除一些无关的行
TIME_WAIT状态

清除掉前面添加的过滤规则,然后使用nc命令建立tcp连接,然后直接按Ctrl-C杀掉nc进程,然后使用netstat命令查看连接

代码: 全选

$ sudo iptables -F
$ nc 192.168.0.1 80
^C
$ netstat -4tan
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      0 192.168.0.6:39490       192.168.0.1:80          TIME_WAIT
...        # 删除一些无关的行
服务端的状态过渡:

LISTEN状态

使用nc命令启动一个tcp server,然后使用netstat命令查看连接

代码: 全选

$ nc -l 8888 &
$ netstat -4tan
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      0 0.0.0.0:8888            0.0.0.0:*               LISTEN
...        # 删除一些无关的行
SYN_RECV状态
在上一步的基础上,使用iptables添加规则,过滤掉输入的ACK segment

代码: 全选

$ sudo iptables -t filter -A INPUT -p tcp -m tcp --dport 8888 --tcp-flags ACK ACK -j DROP
然后使用nc发起tcp连接

代码: 全选

$ nc 192.168.0.6 8888                # 这条命令是在192.168.0.3主机上执行的

然后使用netstat命令查看连接

代码: 全选

$ netstat -4tan
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      0 0.0.0.0:8888            0.0.0.0:*               LISTEN
tcp        0      0 192.168.0.6:8888        192.168.0.3:53862       SYN_RECV
...        # 删除一些无关的行
原来处在LISTEN状态的socket还在,多了一个处于SYN_RECV状态的socket

ESTABLISHED状态

清除掉前面添加的过滤规则

代码: 全选

$ sudo iptables -F
使用nc命令启动一个tcp server,然后使用nc命令发起tcp连接,然后使用netstat命令查看连接

代码: 全选

$ nc -l 8888 &
[1] 32680
$ nc 192.168.0.6 8888 &
[2] 32681
$ netstat -4tan
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      0 0.0.0.0:8888            0.0.0.0:*               LISTEN
tcp        0      0 192.168.0.6:8888        192.168.0.6:55962       ESTABLISHED
...        # 删除一些无关的行
原来处在LISTEN状态的socket还在,多了一个处于ESTABLISHED状态的socket

CLOSE_WAIT状态

从上面的TCP State Transition Diagram图看:

从ESTABLISHED状态过渡到CLOSE_WAIT状态是因为TCP收到FIN segment触发的,当连接进入CLOSE_WAIT状态时,会向应用进程报告EOF
由于nc命令收到EOF后,会立刻调用close关闭socket,从而立刻触发发送FIN segment,然后进入LAST_ACK状态
我们没有办法通过packet过滤来让连接停留在CLOSE_WAIT状态,因为从CLOSE_WAIT状态过渡到LAST_ACK状态是close操作触发的,并不是因为收到某个segment触发的。因此为了观察到连接停留在CLOSE_WAIT状态,我开发一个简单的simpletcpserver程序,该程序不调用close操作,从而使连接停留在CLOSE_WAIT状态

执行simpletcpserver启动一个tcp server

代码: 全选

$ ./simpletcpserver 8888 &
[1] 1810
$ netstat -4tan                       # 这条命令在下一步执行完后再执行
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      0 0.0.0.0:8888            0.0.0.0:*               LISTEN
tcp        1      0 192.168.0.6:8888        192.168.0.3:54062       CLOSE_WAIT
...        # 删除一些无关的行
然后使用nc发起tcp连接

代码: 全选

$ nc 192.168.0.6 8888                # 这条命令是在192.168.0.3主机上执行的
^C                                               # 立刻按Ctrl-C杀掉nc进程
LAST_ACK状态

在192.168.0.6主机上启动一个tcp server

代码: 全选

$ nc -l 8888
在192.168.0.3主机上发起连接

代码: 全选

$ nc 192.168.0.6 8888
在192.168.0.6主机上使用iptables过滤外出的FIN segment(阻止服务端发送FIN segment)

代码: 全选

$ sudo iptables -t filter -A OUTPUT -p tcp -m tcp --sport 8888 --tcp-flags FIN FIN -j DROP
然后使用netstat命令查看连接

代码: 全选

$ netstat -4tan
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
...        # 删除一些无关的行
tcp        0      1 192.168.0.6:8888        192.168.0.3:54097       LAST_ACK
...        # 删除一些无关的行
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#33

帖子 723937936@qq.com » 2023-03-28 14:05

TIME_WAIT状态

TIME_WAIT状态又称为2MSL状态
MSL(maximum segment lifetime):是一个segment可以在网络中传输的最长时间(受IP数据报的TTL字段控制)

执行active close的一端,在发送最后一个ACK segment后,连接会进入TIME_WAIT状态,连接在这个状态停留2MSL的时长

两个问题:
1. 为什么主动关闭的一端要进入TIME_WAIT状态?
答:当主动关闭的一端发送最后的ACK segment后,需要确认对端是否收到了这个ACK segment,所以要等待

2. 为什么要等待2MSL的时长?
这是基于一个事实:如果对端没有收到最后的这个ACK segment的话,对端会重传FIN segment
要等待的最大时长(2MSL)=传输最后的这个ACK segment的最大时长+传输对端重传的那个FIN segment的最大时长

另外:
1. 如果在2MSL时间内没有收到FIN segment的话(对端没有重传FIN segment),就表示对端收到了ACK segment
2. 如果在2MSL时间内收到了对端重传的FIN segment,会再次发送ACK segment,然后重置定时器为2MSL

linux的2MSL的值是60s,这是在内核头文件include/net/tcp.h里定义的TCP_TIMEWAIT_LEN宏常量

连接处于TIME_WAIT状态的另一个效果

我们前面说过连接的唯一标识是:四元组(local ip address, local port, remote ip address, remote port),也称为socket pair
既然是连接的唯一标识,所以两个连接不能使用相同的四元组,这是连接的唯一标识的定义!

当一个连接处于TIME_WAIT状态时,这个连接还没有释放,所以新的连接不能使用与处于TIME_WAIT状态的连接相同的四元组。这是由连接的定义决定的,其实与连接是否处于TIME_WAIT状态无关。

与TIME_WAIT状态有关的是,当连接处于TIME_WAIT状态时,有一个额外的更严格的限制:新的连接(socket)也不能重用(bind)local port,应用程序可以通过SO_REUSEADDR套接字选项解除这个额外的限制

一般情况下是客户端发起active close,然后客户端的连接会进入TIME_WAIT状态,因为客户端很少会显式调用bind指定local port,而是让内核选择一个ephemeral port,所以当客户端连接处于TIME_WAIT状态时,不影响随后的客户端重启(再次启动使用了一个不同的ephemeral port)

然而服务端也可能发起active close,然后服务端的连接会进入TIME_WAIT状态,因为服务端一般都调用bind指定local port,当服务端的连接处于TIME_WAIT状态时,重启服务端时bind函数会报EADDRINUSE(Address already in use)错误(可以通过SO_REUSEADDR套接字选项解除这个限制)

服务端可能是有意设计为主动关闭连接(比如web server),也可能因为服务端进程crash,然后被systemd自动重新拉起

测试linux的2MSL大小

先用sock程序启动一个tcp server

代码: 全选

$ sock -s 6666
hello
^C               # 按^C杀掉sock进程
在另一个终端发起连接

代码: 全选

$ nc localhost 6666
hello              # 输入hello
连接建立后,杀掉sock进程,然后立即执行下面这个脚本

代码: 全选

$ for _ in `seq 100`; do date ; sock -s 6666 ; sleep 1 ; done
2023年 03月 28日 星期二 11:10:26 CST
can't bind local address: Address already in use
...     # 省略中间一些行
2023年 03月 28日 星期二 11:11:25 CST
can't bind local address: Address already in use
2023年 03月 28日 星期二 11:11:26 CST
从上面的输出看出,过