网络知识
Troubleshooting · Decision Tree

排障决策树

"网不通 / 网慢"不要猜:从下往上逐层取证,每一层用命令说话,证据足了再往下走。配套命令速查、Wireshark 过滤器与抓包纪律——所有内容手机可查。

6 层 决策链 macOS / Linux 双列对照 30+ 条过滤器 6 条 抓包纪律

分层排障决策树

bottom-up · evidence first

总原则:从下往上查,每层先证明"这层没问题"再上楼。每一层给出判断命令、输出怎么读、以及"通 / 不通"两个分支的下一步。所有计数器看增量不看累计值(连看两次取差);调大任何队列 / 缓冲之前,先确认是哪一只计数器在涨。

第 1 层

物理链路 / 网卡

链路 up 吗?网卡层就开始丢包了吗?
# Linux ip -s link show dev eth0 ethtool eth0 # link detected / speed / duplex ethtool -S eth0 # NIC 私有计数器: rx_dropped / rx_fifo_errors… ethtool -g eth0 # ring buffer 大小 # macOS ifconfig en0 # status: active / media(无错误计数) netstat -ibn -I en0 # 错误/丢弃计数看 Link 行,取增量 networksetup -getMedia en0 # 仅适用有线网卡

输出怎么读

  • NO-CARRIER / link detected: no → 物理层问题,到此为止不用再往上查
  • 速率 / 双工协商成 100Mb half-duplex 而期望千兆 → 协商异常,表现为"慢"而不是"断"
  • ip -s link 的 RX errors / dropped 在增长 → 接收路径异常(它只覆盖接收侧);再用 ip -s -s link 细分与驱动计数(rx_missed_errors 等,见 ethtool -S)定位
  • ethtool -S 里 rx_fifo / rx_ring 相关计数器增长 → ring buffer 来不及取
✓ 链路正常

进入第 2 层查邻居解析。

✗ 有问题

先修物理 / 驱动层:换线换口、查协商速率、看 dmesg。若是 ring 溢出 → 按下面证据链定位到具体一层再调参。

◆ Linux 丢包证据链(包究竟丢在哪一层)

收包路径上每一层都有队列和计数器,哪只计数器在涨,包就丢在哪一层:

NIC FIFO/ring:ethtool -S eth0(各驱动字段名不同,找 *_dropped / *_fifo_*) → NAPI 软中断:cat /proc/net/softnet_stat 第 2 列(十六进制,backlog 溢出丢弃数)增长 = per-CPU backlog 满 → 协议栈:nstat -az | grep -E '^(Ip|IpExt|Tcp|TcpExt|Udp|UdpLite)' 或对旧系统 netstat -s,按 IP/TCP/UDP 分组看丢弃原因(注意 InReceives 在 Ip 组,叫 IpInReceives,不在 IpExt 里) → socket 缓冲:ss -m 看 skmem 是否顶到 rmem 上限。

关键认知(分方向):入向——tcpdump 的 tap 点在 netfilter 和协议栈之前,抓到 ≠ 协议栈收了 ≠ 应用收到,抓到之后仍可能被 iptables / rp_filter / socket 缓冲丢弃;出向相反——包先过 OUTPUT/POSTROUTING 才到抓包点,被防火墙丢掉的出向包通常根本抓不到。两端证据矛盾时先想这两点。

第 2 层

邻居解析(ARP / NDP)

拿到网关的 MAC 了吗?
# Linux ip neigh show arping -I eth0 -c 3 192.168.1.1 ip -6 neigh show # IPv6 用 NDP,不用 ARP # macOS arp -a ndp -a # IPv6 邻居表

输出怎么读

  • 网关条目 INCOMPLETE / FAILED → ARP 请求没人答:二层不通或目标不在线
  • 网关 MAC 变了,或两个 IP 对应同一 MAC → 怀疑 ARP 欺骗
  • 大量 FAILED 条目 → 扫描行为或邻居表压力
  • 记住:访问外网时解析的是网关的 MAC,不是远端服务器的
✓ 有稳定 MAC

进入第 3 层查 IP 与路由。

✗ 解析失败

回到第 1 层查链路;或查交换机 VLAN 划分、目标是否同网段。整网广播暴涨、MAC 表翻滚 → 怀疑二层环路。深入案例见 cases.html#case-9 →

第 3 层

IP / 路由

包从哪个口、走哪条路由出去?分段 ping 到哪断了?
# Linux ip addr ip route get 93.184.216.34 # 关键:实际选路结果 ping -c 4 192.168.1.1 # 分段: 网关 → 公网IP → 域名 ping -c 4 223.5.5.5 mtr -n 93.184.216.34 # 逐跳持续探测 # 大包不通小包通 → 怀疑 PMTUD 黑洞: ping -M do -s 1472 93.184.216.34 # DF 置位逐步减 -s # macOS route -n get 93.184.216.34 ping -c 4 -D -s 1472 93.184.216.34 # -D = DF

输出怎么读

  • ip route get 显示的 via / dev / src 才是实际选路,多网卡 / 多路由表时和想象常不一致
  • ping 网关通、ping 公网不通 → 出网关之后的问题(上游 / NAT / 防火墙)
  • mtr 某跳开始丢包且之后各跳都丢才是真问题;只有中间一跳丢、后面正常 = 该路由器限 ICMP,不算故障
  • DF 大包超时、小包正常 → MTU 黑洞:路径上某处 MTU 更小且报错报文被拦——IPv4 是 ICMP Type 3 Code 4(Fragmentation Needed),IPv6 是 ICMPv6 Type 2(Packet Too Big)
✓ 路由通

IP 可达。如果症状是"能 ping IP、打不开网站"→ 直接上第 4 层查 DNS。

✗ 断在某处

用 mtr 锁定最后通的一跳;MTU 黑洞则逐点抓包看 ICMP Too Big(IPv4 Type 3 Code 4 / ICMPv6 Type 2)是否生成、是否被拦。深入案例见 cases.html#case-4 →

第 4 层

DNS

名字能解析成对的 IP 吗?应用和命令行走的是同一条 resolver 路径吗?
# 通用(两系统 dig 一致;注意 dig 只直询 resolv.conf 里的服务器) dig example.com # 直询 resolv.conf 第一个 DNS dig @223.5.5.5 example.com # 指定服务器,对比出差异 dig +trace example.com # 从根开始走完整迭代路径 dig +noedns example.com # EDNS 兼容性单变量验证 dig +tcp example.com # 排除 UDP 被拦/截断 # Linux: 用 strace 对照应用实际 resolver 路径 cat /etc/resolv.conf strace -f -e trace=openat,connect -p PID 2>&1 | grep -E 'resolv|hosts|:53' # macOS: dig 不走 mDNSResponder/scoped DNS;系统原生解析要用 scutil --dns # 看系统 resolver 全貌(含 scoped) dscacheutil -q host -a name example.com # 走系统解析路径查询 dns-sd -G v4 example.com # 经 mDNSResponder 解析(对照用)

输出怎么读

  • 不同 DNS 服务器给出不同答案 → 内部域 / 拆分解析问题,不是"DNS 坏了"
  • nslookup / dig 正常但应用失败:应用走的路径(NSS、hosts、nscd 缓存、search 域、rotate、语言运行时自己的 resolver)和命令行不同——Linux 先 strace 看应用读什么、连谁;macOS 用 dscacheutil / dns-sd 走系统路径对照(dig 在 macOS 上不过 mDNSResponder)
  • 加 +noedns 就好 → 对端实现不兼容 EDNS OPT
  • 固定 5 秒超时 → 典型重传等待:A 与 AAAA 两查询其一丢包 / conntrack 竞态
✓ 解析正确

上第 5 层查传输。

✗ 解析异常

比对 resolver 路径,抓 53 端口确认查询发给了谁、有没有响应;EDNS 用 +noedns 单变量验证。深入案例见 cases.html#case-5 →

第 5 层

传输(TCP/UDP)

握手建立了吗?队列满了吗?单条连接的窗口 / 重传长什么样?
# Linux: 监听与队列 ss -lnt # Recv-Q = 当前全连接队列占用; # Recv-Q 持续等于 Send-Q(backlog 上限) → 队列满 nstat -az | grep -E 'ListenOverflows|ListenDrops' # 看增量! # 单连接细节(慢 / 卡都看这) ss -ti dst 93.184.216.34 # rtt / rto / cwnd / ssthresh / retrans ss -tanp state time-wait | wc -l ss -tanp state close-wait # 指向本地应用没 close # macOS 对照 netstat -an -p tcp | grep LISTEN netstat -an | grep -c TIME_WAIT

输出怎么读

  • 客户端"连得上"但应用无感知、批量超时 → 怀疑全连接(accept)队列溢出:ListenOverflows 增量是强指向证据;ListenDrops 更宽泛(含收到 SYN 即丢、内存分配失败等),可单独增长——两者结合内核版本一起看,不要套固定机制
  • ss -ti:吞吐近似上限 ≈ min(发送可用窗口, 对端通告窗口, cwnd×MSS) ÷ RTT——cwnd 的单位是段不是字节。cwnd 低 / retrans 涨 / rtt 抖动只是线索,要结合基线判断:丢包减窗、缓冲设小、窗口缩放未协商各有不同的处置
  • CLOSE_WAIT 堆积 = 对端关了、本地应用迟迟不 close(回应用代码查);TIME_WAIT 多先看主动关闭方是谁,不着急调内核参数
  • 到同一目标的连接数逼近 临时端口范围 × 1 才谈得上"端口耗尽"——四元组是到每个目的独立计数的
✓ 传输健康

握手正常、无异常重传 → 上第 6 层查应用。

✗ 异常

队列溢出 → 应用 accept 太慢(线程池 / 事件循环问题),调大 backlog 只是缓冲;丢包限速 → 回第 1 层证据链找丢包点。队列溢出 cases.html#case-1 → RST 溯源 #case-6 →

◆ 内核证据(Linux)

nstat -az 按协议分组(过滤用 grep -E '^(Ip|IpExt|Tcp|TcpExt|Udp|UdpLite)',注意 IpInReceives 在 Ip 组而非 IpExt):TcpExt 下 ListenOverflows(强指向 accept 队列溢出)与 ListenDrops(更宽泛:收 SYN 即丢、内存失败等,可单独增长,需结合内核版本判断)、TCPBacklogDrop、PAWS 相关拒绝计数、TCPReqQFullDrop(半连接队列)。Ip/IpExt 组各 Drop 系计数对照收包量看收进来多少、丢掉多少。对旧系统用 netstat -s 看同类计数。一律连看两次取增量,累计值会骗你。

NAT / 有状态防火墙环境:conntrack 表满时新连接被静默丢弃,表现为随机超时——conntrack -C(当前条数)对照 sysctl net.netfilter.nf_conntrack_max,或看 dmesg 的 nf_conntrack: table full, dropping packet。

第 6 层

应用(HTTP / TLS)

连上了但慢或报错?把时间切开来量。
# 分段计时: DNS / TCP / TLS / 首字节 / 总耗时各花多少 curl -v -o /dev/null -s https://example.com/ \ -w 'dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' # 只验 TLS 握手与证书 openssl s_client -connect example.com:443 -servername example.com # 指定 HTTP 版本对照(h2 vs h1.1 行为差异) curl --http2 -sI https://example.com/ -o /dev/null -w '%{http_version}\n'

输出怎么读

  • dns 大 → 回第 4 层;tcp−dns 大 → RTT / 握手重传,回第 3/5 层
  • tls−tcp 大 → 证书链太大、OCSP、或中间设备干预 TLS
  • ttfb−tls 大 → 优先怀疑服务端 / 上游处理慢(也含请求上传、代理队列、重传与回程时延)——用 ss / tcpdump / 服务端 tracing 交叉验证后再下"应用慢"的结论
  • s_client 看证书链、协商出的版本与套件;证书错误在这一步就现形
✓ 全程快

网络全链路清白,问题在应用逻辑 / 后端依赖——用应用侧 tracing 继续。

✗ 某段慢

哪段时间异常就回到对应层取证;至此每一层都有命令结论,可以拿着证据链沟通了。

先定性,再进树:动手之前先把症状归入五类之一——① 解析失败(直接奔第 4 层)② 连接拒绝 RST(第 5 层 + RST 溯源)③ 连接超时(第 1→3 层,或中间设备静默丢包)④ 握手后断开(第 5/6 层,看 FIN/RST 谁发的、时机)⑤ 已连接但慢(第 5 层 ss -ti + 第 6 层分段计时)。定性错了,再勤快也是白跑。

命令速查对照表

macOS · Linux

macOS 是 BSD 系工具(ifconfig / netstat / route -n get),Linux 是 iproute2(ip / ss)。Windows 的 tracert 用 ICMP Echo,Unix 的 traceroute 默认发 UDP 高端口,发现中间跳靠的都是 ICMP 时间超过。

目的LinuxmacOS
接口 / 地址ip addr · ip -s link-s 带收发与错误计数ifconfig en0status / media / 错误计数在末尾
链路状态 / 速率ethtool eth0networksetup -getMedia en0
路由表ip route · ip route get 目标IProute get 才是实际选路netstat -rn · route -n get 目标IP
邻居表ip neigh · ip -6 neigharp -a · ndp -a
连接 / 监听ss -tanp · ss -lnt-p 带进程; -i 单连接详情; -m socket 内存netstat -an -p tcp · lsof -iTCP -sTCP:LISTEN
逐跳路径mtr -n 目标 · traceroute -n 目标sudo mtr -n 目标(brew,需 root/raw-socket 权限) · traceroute -n 目标
协议栈计数器nstat -az · netstat -s看增量; 按 IP/TCP/UDP 分组netstat -s字段名与 Linux 不同
DNS 查询dig 域名 · dig @服务器 域名 · dig +trace · dig +short · dig +noedns · dig -x IP两系统 dig 行为一致; macOS 查系统 resolver 用 scutil --dns

tcpdump 常用过滤式

抓包语法(BPF)两系统一致,但 -i any 仅 Linux 有;macOS 没有 any 接口,先 tcpdump -D 列出接口再选(en0 / utunN / lo0)。另注意 Apple 自带 /usr/sbin/tcpdump 与 Homebrew 版在 pktap 支持与权限行为上有差异。先 -w 写文件,分析时再 -r 慢慢过滤。

想抓什么过滤式
到某主机的全部流量tcpdump -i any -w out.pcap host 93.184.216.34-i any 仅 Linux;macOS 用 tcpdump -D 选接口
主机对到 443 的流量tcpdump -i any -w out.pcap 'host A and host B and tcp port 443'要锁单条连接需补全四元组(双方 IP+双方端口)
SYN / FIN 标志包tcpdump -i any -r out.pcap 'tcp[tcpflags] & (tcp-syn|tcp-fin) != 0'不含纯 ACK(第三次握手)与 RST;RST 见下一行
只看 RSTtcpdump -i any -r out.pcap 'tcp[tcpflags] & tcp-rst != 0'
DNS 查询与响应tcpdump -i any -n 'udp port 53 or tcp port 53'
ICMP(含 PMTUD 证据)tcpdump -i any -n icmp or icmp6
VLAN 标签帧tcpdump -i any -n vlan
隧道口(WireGuard / IPsec NAT-T)tcpdump -i <utunN> -n · tcpdump -i any -n 'udp port 51820 or udp port 4500'utun 编号按机器变化,先 tcpdump -D 查
排除干扰(不看 SSH 自己)tcpdump -i any -n 'not tcp port 22'
长抓轮转(保故障前窗口)tcpdump -i any -s 0 -C 200 -W 10 -w /tmp/ring.pcap10 × 200MB 循环覆盖,出事即停;-i any 仅 Linux

Wireshark 过滤器速查

display filter by scenario
capture filter ≠ display filter:capture filter(BPF 语法,如 tcp port 443)在抓包时生效、丢掉的包不可恢复;display filter 只在显示层过滤,原始数据都在。纪律:抓的时候宁宽勿窄(甚至不带 capture filter),分析时再用 display filter 缩小。下面是 display filter。

握手异常handshake

tcp.flags.syn == 1 tcp.flags.syn == 1 && tcp.flags.ack == 1 # 只看 SYN/ACK tcp.analysis.initial_rtt # 握手时延(SYN→SYN/ACK) tcp.analysis.ack_rtt # 段→对应 ACK 的时延

SYN 有去无回 = 对端静默丢包(防火墙 / 队列溢出);SYN 立刻吃 RST = 端口未监听。量握手慢不慢看 initial_rtt,别信不存在的伪字段。

重传与乱序retransmission

tcp.analysis.retransmission tcp.analysis.fast_retransmission tcp.analysis.out_of_order tcp.analysis.duplicate_ack tcp.analysis.spurious_retransmission

先看是超时重传(RTO,卡几百 ms~秒级)还是快重传(dup ACK 触发,快)。spurious = 其实没丢,ACK 只是慢到了。

RST 定位who reset

tcp.flags.reset == 1 tcp.flags.reset == 1 && tcp.flags.ack == 1 # RST+ACK:常见于对 SYN 的拒绝

判断"对 SYN 的拒绝"要结合同一 stream 里紧挨的前一个 SYN;比对序号时注意 Wireshark 默认显示 relative sequence number。定位"谁 reset":双端同时抓,比 TTL / IP ID / seq 落窗——与同方向基线对不上是可疑信号而非结论(源 MAC 逐跳本来就会变,不能当证据);最终归因要结合 seq/ack 落窗、双端抓包与设备日志。深入案例见 cases.html#case-6。

窗口与零窗口window

tcp.analysis.zero_window tcp.analysis.window_full tcp.analysis.zero_window_probe

zero_window = 接收方缓冲满(应用读得慢);window_full = 发送方填满了接收端此前通告的接收窗口——指向接收侧窗口 / 应用读取慢,不是网络或 cwnd 问题。方向完全不同,别混淆。

DNSdns

dns dns.flags.response == 0 # 只看查询 dns.flags.rcode != 0 # 错误响应 dns.time > 1 # 响应超过 1 秒

查询发出无响应 → 看是不是 A/AAAA 双发只丢其一(固定 5s 超时模型);rcode=2(SERVFAIL) 常见于上游故障。

TLStls

tls tls.handshake.type == 1 # ClientHello tls.handshake.extensions_server_name # SNI tls.alert_message # 告警=握手失败点

可见性分版本:TLS 1.2 的 Certificate 通常明文可见,证书问题在 ServerHello 之后现形;TLS 1.3 里 Certificate 与 alert 都在 ServerHello 之后加密,没有 SSLKEYLOGFILE 就看不到内容,只能凭时序和连接结果推断。

QUIC / HTTP3quic

quic udp.port == 443 && !quic # 仅表示 dissector 未识别,不等于"非 QUIC" http3

排障先确认应用实际跑了 h2 还是 h3(curl -w '%{http_version}');UDP 被 QoS / 拦截时浏览器会回退 TCP。"!quic" 命中可能只是版本太新解析器不认识,要结合 Draft/version 字段判断。

隧道 / VPNtunnel

esp || ah || gre # IP 承载的隧道 isakmp # IKEv2 (UDP 500/4500) wg # WireGuard openvpn

分接口看:物理接口抓到的是 ESP/NAT-T 外层——只能核 SPI 与序列号连续性,内层不可见属正常;utun 口抓到的是加密前 / 解密后的明文内层 IP 包。详见 协议图谱 → 隧道与 VPN。

抓包纪律

capture discipline

抓包是唯一能看到"线上真相"的手段,但抓得不对等于没抓。六条纪律:

1 · 双端同时抓

客户端与服务端(有 LB / 防火墙就再加上中间点)同时开抓。只有双端才能回答"包是没发、没收到、还是收到没回"——单端抓包永远分不清对端与链路的责任。

2 · 全长快照

始终 -s 0(snaplen 全帧)。截断的包没法做 TCP 重组和内容分析,等于白抓。

3 · 先落盘,后分析

抓的时候只 -w file.pcap 写文件,不做实时过滤显示——-w 模式的 I/O 路径最短、丢包率最低。分析时 -r 读回来慢慢过滤。

tcpdump -i any -s 0 -w ring.pcap -C 200 -W 10 # -i any 仅 Linux;macOS 先 tcpdump -D 选接口

4 · 时钟对齐

双端机器的时间差会直接毁掉时序判断。抓之前 date 对一次时、记录偏差;正式环境靠 NTP 保证两端偏差在毫秒级以内。

5 · 过滤宁宽勿窄

capture filter 丢掉的包不可恢复。接口拿不准时:Linux 用 -i any;macOS 没有 any,先 tcpdump -D 列表再逐个抓(容器环境尤其容易漏抓 veth / lo)。过滤条件只写确定的部分,其余留给 display filter 阶段。漏抓 lo 曾让真实案例误判方向。

6 · 先缩小范围,再看细节

大文件不要逐包翻:先用四元组 / tcp.stream eq N 锁定问题连接,再看 flags、seq/ack、window、options。tshark 批量导出字段做分布统计,比 GUI 翻百万个包有效。

tshark -r ring.pcap -Y 'tcp.stream eq 42' -T fields -e frame.time_relative -e tcp.flags
证据的表述纪律:每条结论注明视角与版本——"在客户端 eth0 上 12:03:44.102 发出 SYN,服务端 12:03:44.118 未出现(两端时钟差已校准 ±2ms,内核 5.15)"。工具输出脱离视角和内核版本,就是另一种猜测。