《关于TCP SYN包的超时与重传》——那些你应该知道的知识(四)

近日,在分析某项业务故障时,抓取到,TCP客户端发送SYN包,对端没有收到,然而客户端也没有进行SYN包重传的现象。具体情况如下图:

可以看到,经过过滤,本次抓包抓取到的tcp连接情况,只有客户端主动发起了TCP连接,发送了建立连接的syn包,之后再无关于该tcp连接的任何数据包传递发生。由此可以推测,该syn包没有被服务器端收到,或者服务器端收到syn包后没有响应。于是,根据tcp源端口,在服务器侧抓取到的数据包中进行过滤。

可见,服务器侧没有收到该SYN包。所以服务器侧,关于该TCP连接没有任何响应是正常的。

然而,我们知道tcp连接,在客户端发送了SYN包后,若没有收到服务器端返回的SYN-ACK,客户端会进行SYN包的重传,然而通过TCP客户端侧的抓包看,在发送了一次SYN包后,没有收到响应后,却没有进行SYN包的重传,导致该TCP连接,在只尝试了一次后,就没有再尝试建立连接。

经过进一步分析TCP-SYN包重传的机制,客户端发送syn包,在没有收到SYN-ACK的情况下,等待RTO时间超时,之后会重新发送SYN包,再有等待了2个RTO时间没有收到响应后,会再一次进行SYN报的重传,直到连接建立或达到重传次数限制。RTO时间和重传次数在linux上均有定义,同时应用程序也可以修改重传次数。

首先来讲重传次数

该参数在vi /etc/sysctl.con中修改

net.ipv4.tcp_syn_retries=1

net.ipv4.tcp_synack_retries=1

可以看到,可以修改操作系统作为客户端或服务器端,发送syn包或SYN-ACK包的数量。

这个参数同样可以在/proc/sys/net/ipv4/tcp_syn_retries进行设置。经查看,本次客户端设置/proc/sys/net/ipv4/tcp_syn_retries为5。同样,也可以通过在该机器上telnet 不存在的IP地址和端口的方式,验证操作系统的行为。

除此之外,应用程序也可以通过也可以通过TCP_SYNCNT的socket选项单独设置某一个TCP连接SYN重传次数。对这个不是很懂,不详细说了。

经查看操作系统配置并咨询开发人员,关于SYN重传的参数设置5,按道理应该是SYN包会重传5次,可见不是TCP重传参数的设置问题导致SYN包没有重传。

下面我们来讲SYN包超时的问题:

在研究重传次数的问题时,我们发现,网络上存在着该参数设置的值与实际重传发生的次数不匹配的一些案例,这些案例是由于linux内核的一个bug导致的,连接如下:

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4d22f7d372f5769c6c0149e427ed6353e2dcfe61

Suse网站上也有相关描述:https://www.suse.com/support/kb/doc/?id=7010211

简单来说,文章中描述了,在bug未修复前,超时时间是由TCP_RTO_MIN这个参数指定,然后带入计算的,该参数在内核代码/include/net/tcp.h中定义

#define TCP_RTO_MIN ((unsigned(HZ/5))


这个bug修复后,超时时间由TCP_TIMEOUT_INIT定义



#define TCP_TIMEOUT_INIT((unsigned(1*HZ)

/*RFC6298 2.1 initial RTO value */

由于上述差异导致计算上的错误,导致重传的次数没有按照设置的TCP_SYN_RETRIES发生。

这个值在RFC6298中,定义为1秒。在RFC 1122中,该值为3秒。也就是说若3秒没有收到SYN包的回复,会进行SYN包的重发。若仍没有响应,TCP连接中RTO的时间,采用指数退避的方式增长,每一次的RTO是上一个RTO值得两倍,也就是说SYN包会在6秒后进行重发,若仍没有响应,会再等待12秒以后,在此重传,直到达到前文提到地SYN_Retry次数的限制。

为什么没有重传发生?

在知道了以上知识以后,是什么导致了我们在TCP连接客户端一侧的抓包没有抓取到TCP SYN重传的发生呢,经过与应用系统管理员的沟通,该应用程序等待返回超时的时间设置为2秒。而我们的操作系统版本为Linux Suse 11.3,查看/include/net/tcp.h文件中的定义,设置的TCP_TIMEOUT_INIT为3秒钟。在客户端应用程序调用TCP Socket发出建立连接的SYN包以后,在等待了2秒仍然没有返回时,应用程序便将这个TCP连接主动关闭了,导致了没有重传的发生。

总结

至此,我们了解了关于Linux操作系统中,影响TCP连接中SYN包重传次数的参数设置;SYN包的RTO时间是如何得出的以及这个时间如何增长的;最后我们分析了为什么在文章开头一开始讲到的案例中,没有重传的发生。

我们应在之后的工作中进一步了解操作系统之上运行的应用系统的关于时间的要求,以进一步优化操作系统,提高应用系统运行的连续性和可靠性。

最后附上tcp.h源码的连接,感兴趣的读者可以通过查看源码查看其中各项参数的设置

https://github.com/torvalds/linux/blob/master/include/net/tcp.h

  • 5
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
可以使用golang中的net库和raw socket来构造一个TCP SYN并添加TCP选项字段。以下是一个示例代码: ```go package main import ( "net" "syscall" ) func main() { // 构造TCP SYN srcIP := net.ParseIP("192.168.1.100") dstIP := net.ParseIP("192.168.1.1") srcPort := 12345 dstPort := 80 seqNum := uint32(123456789) tcpHeaderLen := 20 tcpFlags := syscall.TCP_SYN // 构造TCP选项字段 tcpOptions := []byte{0x02, 0x04, 0x05, 0xb4, 0x04, 0x02, 0x08, 0x0a, 0x00, 0x0f, 0x48, 0x61, 0x63, 0x6b, 0x65, 0x72, 0x20, 0x57, 0x65, 0x62, 0x21} // 构造TCP头部 tcpHeader := make([]byte, tcpHeaderLen) tcpHeader[0] = byte(srcPort >> 8) tcpHeader[1] = byte(srcPort) tcpHeader[2] = byte(dstPort >> 8) tcpHeader[3] = byte(dstPort) tcpHeader[4] = byte(seqNum >> 24) tcpHeader[5] = byte(seqNum >> 16) tcpHeader[6] = byte(seqNum >> 8) tcpHeader[7] = byte(seqNum) tcpHeader[8] = byte(0) tcpHeader[9] = byte(0) tcpHeader[10] = byte(tcpHeaderLen >> 8) tcpHeader[11] = byte(tcpHeaderLen) tcpHeader[13] = byte(tcpFlags) tcpHeader[14] = byte(0x10) tcpHeader[15] = byte(0xff) // 添加TCP选项字段 copy(tcpHeader[tcpHeaderLen:], tcpOptions) // 构造IP头部 ipHeaderLen := 20 ipVersion := 4 ipHeader := make([]byte, ipHeaderLen) ipHeader[0] = byte(ipVersion<<4 | ipHeaderLen>>2) ipHeader[1] = byte(0) ipHeader[2] = byte(0) ipHeader[3] = byte(0) ipHeader[4] = byte(0) ipHeader[5] = byte(0) ipHeader[6] = byte(0) ipHeader[7] = byte(0) ipHeader[8] = byte(0) ipHeader[9] = byte(0) ipHeader[10] = byte(0xff) ipHeader[11] = byte(0xff) copy(ipHeader[12:16], srcIP.To4()) copy(ipHeader[16:20], dstIP.To4()) // 计算IP头部校验和 checksum := checksum(ipHeader) // 添加IP头部校验和 ipHeader[10] = byte(checksum >> 8) ipHeader[11] = byte(checksum) // 合并IP和TCP头部 packet := make([]byte, len(ipHeader)+len(tcpHeader)) copy(packet, ipHeader) copy(packet[len(ipHeader):], tcpHeader) // 发送TCP SYN conn, err := net.Dial("ip4:tcp", dstIP.String()) if err != nil { panic(err) } defer conn.Close() _, err = conn.Write(packet) if err != nil { panic(err) } } // 计算校验和 func checksum(data []byte) uint16 { var sum uint32 for i := 0; i < len(data)-1; i += 2 { sum += uint32(data[i])<<8 | uint32(data[i+1]) } if len(data)%2 != 0 { sum += uint32(data[len(data)-1]) << 8 } for (sum >> 16) > 0 { sum = (sum & 0xffff) + (sum >> 16) } return ^uint16(sum) } ``` 这段代码构造了一个源IP为192.168.1.100,目标IP为192.168.1.1,源端口为12345,目标端口为80的TCP SYN,并添加了TCP选项字段。在发送之前,需要使用`net.Dial("ip4:tcp", dstIP.String())`连接到目标IP地址,然后将构造好的TCP SYN写入连接中即可。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值