TCP/IP协议族中,网络层协议和传输层协议有明显的分工:前者提供点到点通信,后者提供端到端通信。TCP协议是传输层应用最广泛的协议,通过三次握手和四次挥手,建立面向连接的、可靠的、全双工的字节流传输服务,实现运行于不同主机的两个应用程序间的通信。

应用层数据采用TCP协议传输,会被封装添加TCP首部,然后交给网络层。TCP首部格式如下(图片来自网络)。
Image Title

  • 源端口号:2个字节,发送端端口号。

  • 目的端口号:2个字节,接收端端口号。

  • 序列号:4个字节,发送数据的第一个字节的编号。TCP传输的字节流的每一个字节都会按顺序编号,由于长度限制,最大为2^32-1。到达最大值后会产生回绕,从0开始。回绕会导致出现两个同样序列号的不同字节数据,后面的时间戳选项可以解决这个问题。TCP通过序列号保证了传输数据的顺序。

  • 确认号:4个字节,期望收到的下一个字节数据的编号。

  • 首部长度:4位,TCP首部长度,4个字节为单位。首部包含可选项,是变长数据,取值范围为5-15,能表示20-60字节。

  • 标志:6位,标志位,多个可同时被设置为1。

    • URG:表示发送的数据是否包含紧急数据。只有当URG=1时,后面的紧急指针字段才有效。

    • ACK:表示前面的确认号字段是否有效。只有当ACK=1时,前面的确认号字段才有效。TCP规定,连接建立后,ACK必须为1。

    • PSH:告诉对方收到该报文段后是否应该立即把数据推送给上层。如果为1,则表示对方应当立即把数据提交给上层,而不是缓存起来。

    • RST:只有当RST=1时才有用。如果收到一个RST=1的报文,说明连接出现了严重错误(如主机崩溃),必须释放然后再重新建立连接,或者说明上次发送的数据有问题,主机拒绝响应。

    • SYN:在建立连接时使用,用来同步序列号。当SYN=1,ACK=0时,表示这是请求建立连接的报文段;当SYN=1,ACK=1时,表示对方同意建立连接。三次握手中的前两次SYN置为1。

    • FIN:标记数据是否发送完毕。如果FIN=1,表示可以释放连接。

  • 窗口大小:2个字节,用于流量控制。出现在每一个TCP报文段中,配合32位的确认号,用于向对端通知本地接收滑动窗口大小。

  • 检验和:2个字节,覆盖首部和数据。

  • 紧急指针:2个字节,标记紧急数据在数据字段中的位置。

TCP首部选项是变长的可选信息,长度必须为4字节的整数倍,不足则填充,最大40字节。常见的选项有7种,如下所示(图片与均来自网络)。
Image Title

kind是选项类型,length是选项长度,均是1个字节,后面是选项信息,部分内容参考其他文章。

  • kind=0:选项表结束。

  • kind=1:空操作(nop),一般用于将TCP选项的总长度填充为4字节的整数倍。

  • kind=2:最大报文段长度(MSS)。TCP连接初始化时,通信双方使用该选项来协商MSS。上篇文章中提到,IP数据报长度若大于MTU,则会发生IP分片。TCP将MSS设置为MTU-40(MTU-IP首部长度-TCP首部长度),大于MSS的数据包将被分段,以避免IP分片。该选项只能出现在有SYN标志的报文段中,否则将被忽略。

  • kind=3:窗口扩大因子。TCP连接初始化时,通信双方使用该选项来协商接收滑动窗口的扩大因子。在TCP的首部中,窗口大小是用2个字节表示的,故最大为为2^16字节,但为了提高TCP通信的吞吐量,实际上TCP模块允许的接收滑动窗口大小远大于此。窗口扩大因子用于实现该需求。假设TCP头部中的接收滑动窗口大小是N,窗口扩大因子是M,那么TCP报文段的实际接收滑动窗口大小是N<<M。注意,M的取值范围是0~14,即最大TCP序列号限定为2^16*2^14=2^30,小于2^31,用于防止序列号溢出。关于窗口扩大因子选项的细节,可参考标准文档RFC 1323。该选项只能出现在有SYN标志的报文段中,否则将被忽略。

  • kind=4:选择性确认(Selective Acknowledgment, SACK)。TCP连接初始化时,通信双方使用该选项表示是否支持SACK技术。TCP通信时,如果某个TCP报文段丢失,TCP会重传最后被确认的TCP报文段后续的所有报文段。SACK将使TCP模块只重传丢失的TCP报文段,而不是所有。

  • kind=5:SACK实际工作的选项。该选项的参数告诉发送方本端已经收到并缓存的不连续的数据块,从而让发送端可以据此检查并重发丢失的数据块。每个块边沿(edge of block)参数包含一个4字节的序列号。其中块左边沿表示不连续块的第一个数据的序列号,而块右边沿则表示不连续块的最后一个数据的序列号的下一个序列号。这样一对参数(块左边沿和块右边沿)之间的数据是没有收到的。因为一个块信息占用8字节,所以TCP头部选项中实际上最多可以包含4个这样的不连续数据块(考虑选项类型和长度占用的2字节)。

  • kind=8:时间戳选项。该选项提供了较为准确的计算通信双方之间的RTT(Round Trip Time)的方法,从而为TCP流量控制提供重要信息。

再次以tcpdump捕获到的TCP三次握手数据包为例。
Image Title

首先分析第一个包。IP数据报长度为52字节,TCP报文段长度为32字节。只看TCP报文段部分。

  • d0dc:源端口为53468。

  • 270e:目的端口号为9998。

  • 967b 0775:序列号。

  • 0000 0000:确认号。

  • 8002:8表示TCP首部长度为32(8*4)字节,与根据IP首部计算出的一致,包括20字节标准字段和12字节的选项域。002表示SYN标志位置1,其他标志位为0,说明这是三次握手中的第一次握手数据包,发起连接请求。

  • 2000: 窗口大小为8192字节。

  • 30f7:检验和。覆盖整个报文段:TCP首部和TCP数据。

  • 0000:紧急指针,由于URG标志位为0,无效。

前面是20字节的标准字段,后面是12字节的选项域。

  • 0204 05b4:第一个选项。02表示是MSS选项,04表明选项长度为4个字节,05b4是MSS值即1460。

  • 01:1个空操作(填充字段)。

  • 030308:第二个选项。03表示启用窗口扩大因子,03表明选项长度为3个字节,08表示窗口扩大因子为8,上面看到窗口大小为8192字节,则扩大后的窗口大小为8192<<8即2M字节。

  • 0101:2个空操作(填充字段)。

  • 0402:04表示请用SACK,02表明选项长度2个字节。

可以用同样方法分析后面两个包,三个包正是三次握手的请求响应包。

值得说明的是,连接建立时,两端都会产生一个TCP初始化序列号ISN,其不能设置为一个固定值,因为这样容易被攻击者猜出后续序列号,从而遭到攻击。

RFC1948中提出了一个较好的初始化序列号ISN随机生成算法。

ISN = M + F(localhost, localport, remotehost, remoteport).
M是一个计时器,这个计时器每隔4毫秒加1。
F是一个Hash算法,根据源IP、目的IP、源端口、目的端口生成一个随机数值。

结合本例,第一个包序列号967b 0775和第二个包的序列号0d59 11c4即是两端各自的ISN,第二个包的确认号是967b 0776,正好是第一个包序列号967b 0775加1,是对第一个包数据的确认。第三个包同理。

连接建立后,两端的数据包序列号分别从各自的ISN + 1开始,这是因为SYN请求本身要占用一个序列号,而ACK不需要。