极客时间:网络排查案例课

时间:March 21, 2022 分类:

目录:

网络排查案例课

网络为什么要分层

如果不分层的话,就需要应用程序包办一切

  • 程序把应用层的数据,按某种编码转化为二进制数据,然后程序去操控网卡,把二进制数据发送到网络上。这期间,通信的连接方式、传输的可靠性、速度和效率的保证等等,都需要这个程序去实现。然后下次开发另外一个应用的时候,就把上面这些活,再干一遍

而应用程序、操作系统、网络设备等环节各自分工

  • 应用程序只负责实现应用层的业务逻辑,操作系统负责连接的建立、处理网络拥塞和丢包乱序、优化网络读写速度等等,然后把数据交给网卡,后者和交换机等设备做好联动,负责二进制数据在物理线路上的传送和接收。

TCP流,为英文的TCP Stream,有"连续的事件"这样一个含义,所以它是有前后、有顺序的,这也正对应了TCP的特性

和Stream相对的一个词是Datagram,它是指没有前后关系的数据单元,比如UDP和IP都属于Datagram

在Linux网络编程里面,TCP对应的socket类型是SOCK_STREAM,而UDP对应的,就是SOCK_DGRAM

在具体的网络报文层面,一个TCP流,对应的就是一个五元组

  • 传输协议类型
  • 源IP
  • 源端口
  • 目的IP
  • 目的端口

其他的一些概念

  • 报文在每个层都可以用
  • 数据包在每个层都可以用
  • 帧frame是二层数据链路层的概念,代表二层报文,包括帧头,载荷和帧尾
  • 分组是IP层报文
  • 段特指TCP segment,也就是TCP报文,当报文超过传输层数据单元的限制,就会划分为多个segment

查看是否存在丢包的情况

watch --diff netstat -s

对链路的多次探测

$ mtr www.baidu.com -r -c 10
Start: 2022-01-07T04:05:02+0000
HOST: victorebpf                  Loss%   Snt   Last   Avg  Best  Wrst StDev
  1.|-- _gateway                   0.0%    10    0.3   0.4   0.2   1.2   0.3
  2.|-- 192.168.1.1                0.0%    10    1.6   1.8   1.4   3.2   0.5
  3.|-- 100.65.0.1                 0.0%    10    3.8   7.0   3.8  10.3   2.0
  4.|-- 61.152.54.125              0.0%    10    4.0   4.3   3.6   5.1   0.5
  5.|-- 61.152.25.110             30.0%    10    5.0   6.8   4.4  18.9   5.4
  6.|-- 202.97.101.30             20.0%    10    7.8   6.6   5.4   7.8   0.8
  7.|-- 58.213.95.110             80.0%    10   10.0   9.8   9.6  10.0   0.3
  8.|-- ???                       100.0    10    0.0   0.0   0.0   0.0   0.0
  9.|-- 58.213.96.74               0.0%    10   10.5  12.7   9.9  24.7   4.9
 10.|-- ???                       100.0    10    0.0   0.0   0.0   0.0   0.0
 11.|-- ???                       100.0    10    0.0   0.0   0.0   0.0   0.0
 12.|-- ???                       100.0    10    0.0   0.0   0.0   0.0   0.0
 13.|-- 180.101.49.12              0.0%    10    9.4   9.1   8.3   9.7   0.5

排查链路层,基于网卡驱动会到内核中注册ethtool回调函数

# ethtool -S enp0s3
NIC statistics:
     rx_packets: 45897
     tx_packets: 9457
     rx_bytes: 59125524
     tx_bytes: 834625
     rx_broadcast: 0
     tx_broadcast: 17
     rx_multicast: 0
     tx_multicast: 59
     rx_errors: 0
     tx_errors: 0
     tx_dropped: 0

traceroute获取

原理就是发送包的TTL由1递增,进行探测返回的IP地址

默认使用UDP,但是可以指定ICMP,获取的数据可能可以互相补全

抓包工具Tcpdump和Wireshark

tcpdump

tcpdump基于bpf

文件格式有多种

  • pcap是libpcap的格式,也是tcpdump和wireshark等工具默认支持的文件格式
  • cap包含libpcap的格式和libpcap标准之外的数据格式
  • pcapng为为了解决libpcap不支持的多网卡

tcpdump常用参数

  • -w文件名,可以把报文保存到文件
  • -c数量,可以抓取固定数量的报文,可以避免抓取过多报文
  • -s长度,可以只抓取每个报文的一定长度
  • -n不做地址转换(比如IP 地址转换为主机名,port 80 转换为 http)
  • -v/-vv/-vvv,可以打印更加详细的报文信息
  • -e,可以打印二层信息,特别是MAC地址
  • -p,关闭混杂模式。所谓混杂模式,也就是嗅探(Sniffering),就是把目的地址不是本机地址的网络报文也抓取下来
  • -X打印详细报文

示例过滤TLS握手的报文

tcpdump -w file.pcap 'dst port 443 && tcp[20]==22 && tcp[25]==1'
  • dst port 443 抓取从客户端发过来的访问HTTPS的报文
  • tcp[20]==22 这是提取了TCP的第21个字节,由于TCP头部占20字节,TLS又是TCP的载荷,那么TLS的第1个字节就是TCP的第21个字节,也就是 TCP[20],这个位置的值如果是22(十进制),那么就表明这个是TLS 握手报文
  • tcp[25]==1 这是提取了TCP的第26个字节,如果它等于1,那么就表明这个是Client Hello类型的TLS握手报文

记录的数据是十六进制的16,转换为十进制就是22了

读取tcpdump文件

tcpdump -r file.pcap 'tcp[tcpflags] & (tcp-rst) != 0'

读取tcpdump文件过滤并转存

tcpdump -r file.pcap 'tcp[tcpflags] & (tcp-rst) != 0' -w rst.pcap

避免无效的数据

一般来说,帧头是14字节,IP头是20字节,TCP头是20~40字节。如果你明确地知道这次抓包的重点是传输层,那么理论上,对于每一个报文,你只要抓取到传输层头部即可,也就是前14+20+40字节(即前74字节)

tcpdump -s 74 -w file.pcap

对于IP数据包

tcpdump -i eth0 -s 34
或者
tcpdump -i any -s 36

长度不一致是因为tcpdump在做-i any时,把以太网头部模拟为这个linux cooked capture的特定格式了,它占用了16个字节,而普通以太网头部是14字节

抓取TCP SYN包的过滤表达式

# 通过偏移量方法抓取SYN包
tcpdump -i any 'tcp[13]&2 !=0'
# 通过标志位方法抓取SYN包
tcpdump -i any 'tcp[tcpflags]&tcp-syn !=0'
# 当我们只想过滤仅有SYN标志的包时,第14个字节的二进制是00000010,十进制是2
tcpdump -i eth1 'tcp[13] = 2' 
# 匹配SYN+ACK包时(二进制是00010010或是十进制18)
tcpdump -i eth1 'tcp[13] = 18'
# 匹配SYN或是SYN+ACK的数据时
tcpdump -i eth1 'tcp[13] & 2 = 2'

详细的TCP数据包

tcptrace也可以分析数据包

$ tcptrace -b test.pcap
1 arg remaining, starting with 'test.pcap'
Ostermann's tcptrace -- version 6.6.7 -- Thu Nov  4, 2004

145 packets seen, 145 TCP packets traced
elapsed wallclock time: 0:00:00.028527, 5082 pkts/sec analyzed
trace file elapsed time: 0:00:04.534695
TCP connection info:
  1: victorebpf:51952 - 180.101.49.12:443 (a2b)   15>   15<  (complete)  (reset)
  2: victorebpf:56794 - 180.101.49.58:443 (c2d)   56>   59<  (complete)  (reset)

Wireshark

判断是client端进行抓包还是server端进行抓包,一般client发出的IP包的TTL都是默认值,64,128和255中的某一个,经过若干网络跳数server端一般会比64,128和255小

乱序一定会有问题吗,一般乱序报文达10%以上就是传输质量的问题了

TCP的连接都是用TCP协议沟通的吗

三次握手

TCP连接是标准的TCP三次握手完成的

  • Client端发送SYN
  • Server端收到SYN后,回复SYN+ACK
  • Client端收到SYN+ACK后,回复ACK

但是Server端不接收握手,会怎么做呢

  1. 不响应这次连接
  2. 响应,给予拒绝的回复

第一种情况,只需要丢弃即可,但是Client端无法分辨是什么情况,一直处于SYN_SENT

  1. 数据包在网络上丢失,Server端没有收到
  2. Server端收到了,但是没有回复
  3. Server端收到了,但是也没有回复

可以直接测试

# server端
iptables -I INPUT -p tcp --dport 80 -j DROP
# client抓包
tcpdump -i any -w telnet-80.pcap port 80
# client测试
telnet ServerIP 80

telnet会挂起,抓包可以看到重试间隔1,1,2,4,8,16s,一共发送了7次,基于的指数退避

由内核参数控制

$ sudo sysctl net.ipv4.tcp_syn_retries
net.ipv4.tcp_syn_retries = 6

如果改为

iptables -I INPUT -p tcp --dport 80 -j REJECT

telnet会直接退出,但是抓包是没有抓到TCP RST

这时候抓包去掉tcp协议,就会收到一个icmp协议的消息

Destination unreachable (Port unreachable)

这里是因为iptables设置的时候默认添加了--reject-with icmp-port-unreachable,如果设置为–reject-with tcp-reset就能获取到RST

Window Scale

TCP在80年代的时候,用两个字节的长度代表65535的窗口,随着网络带宽的增大,窗口不够用,就在TCP的Options扩展

原来的Window大小不变,增加Window Scale,表示原始Window的左移位数,最高可以左移14位

  • Kind:TCP Option编号,3代表这是Window Scale类型
  • Length:3 字节,含Kind、Length、Shift count
  • Shift count:6,也就是窗口将要被右移的位数,2的6次方就是64

另外需要注意的

  • SYN包里的Window是不会被Scale放大的,只有握手后的报文才会
  • Window Scale只出现在TCP握手里面

UDP握手

TCP是有握手的概念的,而UDP的握手是因为nc命令会返回port [udp/*] succeeded!

是因为UDP不是面向连接的,没有一个确定的UDP协议层面的答复,需要UDP程序自行实现,而nc的成功是因为对端没有返回ICMP port unreachable,所以nc的逻辑

  • 对于UDP来说,除非明确拒绝,否则可视为连通
  • 对于TCP来说,除非明确接受,否则视为不连通

同时握手

nginx的connection reset by peer含义

网络问题的排查需要将应用层的信息,翻译为传输层和网络层的信息

应用层的信息包括

  • 应用层日志,包括成功日志、报错日志等等
  • 应用层性能数据,比如RPS(每秒请求数),transaction time(处理时间)等
  • 应用层载荷,比如HTTP请求和响应的header、body等

传输层的信息包括

  • TCP 序列号(Sequence Number)
  • 确认号(Acknowledge Number)
  • MSS
  • 接收窗口(Receive Window)
  • 拥塞窗口(Congestion Window)
  • 时延(Latency)
  • 重复确认(DupAck)
  • 选择性确认(Selective Ack)
  • 重传(Retransmission)
  • 丢包(Packet loss)

网络层的信息包括

  • TTL
  • MTU
  • 跳数(hops)
  • 路由表

connection reset by peer

nginx的错误日志

2015/12/01 15:49:48 [info] 20521#0: *55077498 recv() failed (104: Connection reset by peer) while sending to client, client: 10.255.252.31, server: manager.example.com, request: "POST /WebPageAlipay/weixin/notify_url.htm HTTP/1.1", upstream: "http:/10.4.36.207:8080/WebPageAlipay/weixin/notify_url.htm", host: "manager.example.com"
2015/12/01 15:49:54 [info] 20523#0: *55077722 recv() failed (104: Connection reset by peer) while sending to client, client: 10.255.252.31, server: manager.example.com, request: "POST /WebPageAlipay/app/notify_url.htm HTTP/1.1", upstream: "http:/10.4.36.207:8080/WebPageAlipay/app/notify_url.htm", host: "manager.example.com"
2015/12/01 15:49:54 [info] 20523#0: *55077710 recv() failed (104: Connection reset by peer) while sending to client, client: 10.255.252.31, server: manager.example.com, request: "POST /WebPageAlipay/app/notify_url.htm HTTP/1.1", upstream: "http:/10.4.36.207:8080/WebPageAlipay/app/notify_url.htm", host: "manager.example.com"
2015/12/01 15:49:58 [info] 20522#0: *55077946 recv() failed (104: Connection reset by peer) while sending to client, client: 10.255.252.31, server: manager.example.com, request: "POST /WebPageAlipay/app/notify_url.htm HTTP/1.1", upstream: "http:/10.4.36.207:8080/WebPageAlipay/app/notify_url.htm", host: "manager.example.com"
2015/12/01 15:49:58 [info] 20522#0: *55077965 recv() failed (104: Connection reset by peer) while sending to client, client: 10.255.252.31, server: manager.example.com, request: "POST /WebPageAlipay/app/notify_url.htm HTTP/1.1", upstream: "http:/10.4.36.207:8080/WebPageAlipay/app/notify_url.htm", host: "manager.example.com"
  • recv() failed:这里的recv()是一个系统调用,就是用来接收数据的。可以直接man recv,看到这个系统调用的详细信息,也包括它的各种异常状态码
  • 104:这个数字也是跟系统调用有关的,它就是recv()调用出现异常时的一个状态码,这是操作系统给出的。在Linux系统里,104对应的是ECONNRESET,也正是一个TCP连接被RST报文异常关闭的情况

在client抓包,使用wireshark分析

# 过滤出源IP或者目的IP为my_ip的报文
ip.addr eq my_ip
# 过滤出源IP为my_ip的报文
ip.src eq my_ip
# 过滤出目的IP为my_ip的报文
ip.dst eq my_ip
# RST报文
tcp.flags.reset eq 1
# 合并过滤条件
ip.addr eq 10.255.252.31 and tcp.flags.reset eq 1

过滤到的包可以看到flags中reset

可以看到reset的报文,占比为4%

右击报文,选择Follow->TCP Stream可以查看整个TCP流

可以看到RST处于握手阶段,不是正常第三次握手需要的ACK,而是RST+ACK

Client端发送连接的系统调用

  • socket()
  • connect()

Server端接收连接的系统调用

  • socket()
  • bind()
  • listen()
  • accept()

Server端的用户空间要获取TCP连接,需要获取accept()的返回,RST+ACK报文并不能生成一次有效连接,这种情况Nginx是不能感知这次失败的握手的

Client端调用的connect(),失败会返回ECONNRESET的返回码,可以记录错误

这种情况不是连接建立后的Reset

排除刚出现的报文

ip.addr eq 10.255.252.31 and tcp.flags.reset eq 1 and !(tcp.seq eq 1 and tcp.ack eq 1)

过滤到的报文

Follow TCP Stream可以有挥手的报文,可以指定tcp.stream eq xxx

排除刚出现的报文(seq=1)

ip.addr eq 10.255.252.31 and tcp.flags.reset eq 1 and !(tcp.seq eq 1 or tcp.ack eq 1)

依然剩下很多报文

可以基于数据或者时间再次过滤

# 日志
2015/12/01 15:49:48 [info] 20521#0: *55077498 recv() failed (104: Connection reset by peer) while sending to client, client: 10.255.252.31, server: manager.example.com, request: "POST /WebPageAlipay/weixin/notify_url.htm HTTP/1.1", upstream: "http:/10.4.36.207:8080/WebPageAlipay/weixin/notify_url.htm", host: "manager.example.com"
# 条件
frame.time >="dec 01, 2015 15:49:48" and frame.time <="dec 01, 2015 15:49:49" and ip.addr eq 10.255.252.31 and tcp.flags.reset eq 1 and !(tcp.seq eq 1 or tcp.ack eq 1)

查询到的报文

报错的报文带着weixin,查找TCP Stream中包含weixin的

这就是想要的报文了,因为

  • 时间吻合
  • RST行为吻合
  • URL路径吻合

分析一下就是

握手和POST的请求和响应都正常,但是Client端对HTTP的响应发送ACK之后,没有通过正常的发送FIN断开连接,而是发送了RST和ACK,导致了Server端的Nginx的recv()调用收到了ECONNRESET的报错

具体Client是否获取到数据,因为有对HTTP的响应的ACK,就应该是发送到Client端的Receive Buffer,但是断开连接应该调用的close(),走到tcp_close(),会判断buffer是否有未读的数据,如果有未读则调用tcp_send_active_reset(),发出RST,直接变为CLOSE状态,不进入TIME_WAIT状态

所以要查为什么没有拿走buffer中的数据

FIN完成挥手

正常的四次挥手流程

TCP的挥手可以在任意一方进行发送

就会各有一次FIN和一次ACK

但是有抓到的数据包不是这样

可以看到只有一个FIN的数据包,这是因为TCP的报文可以搭另一个报文的顺风车,类似

看Wireshark以为是Server端发起的FIN,是因为Wireshark在显示应用层信息的时候,TCP层面的控制信息就显示不全了,可以点开看

所以实际情况是

同时挥手

  • 双方同时发起关闭后,也同时进入了FIN_WAIT_1状态
  • 然后也因为收到了对方的FIN,也都进入了CLOSING状态
  • 当双方都收到对方的ACK后,最终都进入了TIME_WAIT状态

但是也意味两边都要等2MSL,才能复用这个五元组TCP连接

挥手不意味着双方都停止发送数据

一方发送FIN,意味着这个连接要开始关闭了,但是只是FIN的发送方不发送数据了,还可以接收数据并响应ACK,进入半关闭状态

对端仍然可以发送数据,会对FIN响应ACK,当发送完数据,就可以也发送FIN进行连接的关闭

定位防火墙:传输层的对比分析

防火墙需要结合传输层和应用层进行分析

示例应用A通过LB访问应用B,经常耗时过长,甚至事务无法在限定时间内完成,并且都出现在HTTPS

这时候不能光看A,也要看B,进行双向抓包

双向抓包需要注意的是

  • 各端的抓包过滤条件一般以对端IP作为条件,比如tcpdump dst host {对端IP}
  • 两端的抓包应该差不多在同时开始和结束
  • 边重现,边抓包

查看抓包文件的流程

  1. 查看Expert Information
  2. 查看可疑报文
  3. 查找故障与网络现象的关系
  4. 对比两侧抓包

查看Expert Information

查看方式:

1:Analyze->Expert Information 2:左下角的黄色圆点

可以看到整体的网络传输状况

  • Warning:条目的底色是黄色,意味着可能有问题,应重点关注
  • Note:条目的底色是浅蓝色,是在允许范围内的小问题,也要关注,例如TCP本身就是容许一定程度的重传的,那么这些重传报文,就属于允许范围内
  • Chat:条目的底色是正常蓝色,属于TCP/UDP的正常行为,可以了解通信里TCP握手和挥手分别有多少次等

示例

  • Warning:有7个乱序(Out-of-Order)的TCP报文,6个未抓到的报文(如果是在抓包开始阶段,这种未抓到报文的情况也属正常)
  • Note:有1个怀疑是快速重传,5个是重传(一般是超时重传),6个重复确认
  • Chat:有TCP挥手阶段的20个FIN包,握手阶段的10个SYN包和10个SYN+ACK包

乱序的出现,可能是中间设备发生了问题

关注问题报文

可以点开Warning前边的小箭头,展开乱序报文,尽量选择后边的报文,因为TCP Stream相对更完整,然后Follow即可

Seq、NextSeq、TCP Seglen都是自定义添加的,目的就是便于分析

这里红色报文需要重点关注

  • 189:服务端(HTTPS)回复给客户端的报文,TCP previous segment not captured意思是,它之前的报文没有在应该出现的位置上被抓到(并不排除这些报文在之后被抓到)
  • 190:客户端回复给服务端的重复确认报文(DupAck),可能(DupAck 报文数量多的话)会引起重传
  • 191:服务端(HTTPS)给客户端的报文,是TCP Out-of-Order,即乱序报文
  • 193:服务端(HTTPS)给客户端的 TCP Retransmission,即重传报文
  • 195:也是服务端(HTTPS)给客户端的重传报文

可以看到192和193报文之间有1.020215s的间隔

结合应用层分析

为什么有一个1s的重试

对比两侧的抓包文件,使用TCP序号

TCP需要在Client的TCP Stream中选择SYN包的序列号4022234701

注意是Raw Sequence Number,而不是Wireshark处理过的Sequence Number

需要设置Wireshark->Preference->Protocols->TCP->Relative sequence numbers勾选

然后就可以通过过滤器过滤了

tcp.seq_raw eq 4022234701

在两端都找到对应的包进行Follow即可,左侧是Server抓包,右侧是Client抓包

当时提示的

  • Out-of-Order:包1、2、3
  • TCP previous segment not captured:包4

在Server端分析

在Server端发送的4个报文,真正乱序的只有4,就是没有足够乱,没能触发3个重复确认进而快速重传,所以Server端进行超时重传,1s的超时是硬件的LB时间,Linux默认为200ms

内网环境常见的

  • 丢包率在万分之一上下
  • 乱序率大概在百分之一以下到千分之一左右都属正常

是因为两侧防火墙通过隧道进行解包和封包,IPIP协议会在IP头部再添加IP头部,消耗系统资源和代码层面bug就可能导致乱序

为什么HTTP的没有问题呢?

是因为TCP载荷只有200~300字节,远小于MSS的1460字节,而隧道会增大报文的长度,例如MTU为1500字节的IP报文,做了IPIP隧道就会到1520字节,所以主机的MTU都需要改小来适配隧道

如果网络没有启用Jumbo Frame,就会被拆分两个报文,而到了对端,就需要合并报文,出问题的概率就会增大

在Linux如果使用IPIP隧道会自动将接口MTU设置为1480,而防火墙的MTU逻辑不同

所以在大包的情况,就会引入两次额外开销

  • IPIP本身的隧道头的封包和拆包
  • IP层因为超过MTU而引发的报文分片和合片

出现问题可以是防火墙,路由器或者交换机等等

对于这种过中间层的一定要尽可能两侧抓包

可以使用的对两侧抓包进行关联的在每一层都有,例如TCP的头部

# TCP确认号
tcp.ack_raw == 754313633

而计算乱序报文的百分比:

  1. 计算全部报文数量:capinfos file.pcap | grep packets
  2. 计算乱序报文数量:tshark -n -q -r file.pcap -z "io,stat,0,tcp.analysis.out_of_order"
  3. 两者相除,或者肉眼也能看出来比例大概在什么数量级了

定位防火墙:网络层精准打击

基于TTL

过滤可以看到对端直接reset导致无法访问

ip.addr eq 253.61.239.103 and tcp.flags.reset eq 1

对于有中间层的情况,无法判断是中间层,还是后端Server进行了reset

这时候可以通过TTL来解决

不过TTL不是默认的展示列,可以选中报文中Internet Protocol Version->Time to live,右击选择Apply as column

就能看到TTL列

可以看到在三次握手的时候SYN+ACK包的时候,TTL还是59,但是RST的时候TTL为64,证明这个RST包不是握手的Server端发出

排查路由器果然有一条异常策略

为什么握手成功了呢,因为异常策略是针对HTTP

LDAPS服务报connection reset by peer

在两侧抓包

Client端

Server端

可以看到Client端发送了Client Hello,而Server端没有收到

可以检查一下TTL

在Server端的SYN包的TTL为112

在Server端的RST包的TTL为55

在Client端的SYN+ACK包的TTL为110

在Client端的RST包的TTL为54

所以Client Hello的包就是防火墙丢弃的,防火墙进行了双向的拦截,整体的链路就是