极客时间:网络排查案例课
目录:
网络排查案例课
网络为什么要分层
如果不分层的话,就需要应用程序包办一切
- 程序把应用层的数据,按某种编码转化为二进制数据,然后程序去操控网卡,把二进制数据发送到网络上。这期间,通信的连接方式、传输的可靠性、速度和效率的保证等等,都需要这个程序去实现。然后下次开发另外一个应用的时候,就把上面这些活,再干一遍
而应用程序、操作系统、网络设备等环节各自分工
- 应用程序只负责实现应用层的业务逻辑,操作系统负责连接的建立、处理网络拥塞和丢包乱序、优化网络读写速度等等,然后把数据交给网卡,后者和交换机等设备做好联动,负责二进制数据在物理线路上的传送和接收。
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端不接收握手,会怎么做呢
- 不响应这次连接
- 响应,给予拒绝的回复
第一种情况,只需要丢弃即可,但是Client端无法分辨是什么情况,一直处于SYN_SENT
- 数据包在网络上丢失,Server端没有收到
- Server端收到了,但是没有回复
- 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}
- 两端的抓包应该差不多在同时开始和结束
- 边重现,边抓包
查看抓包文件的流程
- 查看Expert Information
- 查看可疑报文
- 查找故障与网络现象的关系
- 对比两侧抓包
查看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
而计算乱序报文的百分比:
- 计算全部报文数量:
capinfos file.pcap | grep packets
- 计算乱序报文数量:
tshark -n -q -r file.pcap -z "io,stat,0,tcp.analysis.out_of_order"
- 两者相除,或者肉眼也能看出来比例大概在什么数量级了
定位防火墙:网络层精准打击
基于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的包就是防火墙丢弃的,防火墙进行了双向的拦截,整体的链路就是