216 条网络命令,每条都标了坑:一份现场速查表

216 条网络命令,每条都标了坑:一份现场速查表

这份清单从一本 15 万字的网络排障书里抽出来,去掉重复、按工具族分类、每条给一句「什么时候用它」和一个「最容易被误读的地方」。

不是 man page——man page 告诉你参数怎么填,这里告诉你手上有输出时该往哪个方向想。

先说三条纪律,否则清单会害了你

一、先写预期,再执行。 每条命令执行前,先在脑子里写一句「如果正常,我会看到什么」。看到的东西与预期不符,这一步就给出了方向;相符,才进下一步。没有预期就敲命令,等于在大海里舀水。

二、计数看增量,不看绝对值。 dropped 27 这六个字符本身没有意义——要看它 30 秒后还是不是 27。绝对值只会让你盯着一个历史遗留数字发呆。

三、带数字的判据必须带前提。 ping -M do -s 1392 只在 wg0 MTU = 1420 时成立;换一套 MTU 就得重算。任何不写前提的数字,等于什么都没说——这是这份清单里反复出现的规矩。


一、地址与邻居(8 条)

ip -4 addr show [dev eth0]

看内核实际生效的地址与掩码,不是配置文件里写了什么。

  
坑:出现 169.254.x.x 说明 DHCP 失败,很多人把它当成「网络通了」。
  
坑:只看有没有地址,不看前缀长度——/24 和 /32 决定它能不能直连网关。
  

ip -6 addr show scope global

scope global 过滤掉 fe80:: 链路本地,只看能路由的地址。

  
坑:有 global 地址 ≠ 有 v6 出口。必须再查 ip -6 route get。
  
坑:RA 可能在你没配过的情况下自动给地址,valid_lft 会随前缀变化。
  

ip neigh show [dev eth0]

ARP(v4)/ NDP(v6)邻居表。

  
关键:STALE 是正常老化态,不是故障;FAILED 才是解析失败。
  
判据:同网段不通,先看这里,不要先怀疑路由。
  

完整状态机:

  
(从未通信)→ INCOMPLETE ──有回应──→ REACHABLE
  
                    │                    │ 约 30 秒内不再验证
  
                    │ 无回应             ▼
  
                    ▼                  STALE  ← 过期,但先不验证
  
                  FAILED                 │ 下次发包时
  
                                         ▼
  
                                       DELAY(先发数据,同时准备验证)
  
                                         │ 5 秒
  
                                         ▼
  
                                       PROBE(真的发 ARP)
  

为什么要有 DELAY 这 5 秒? 内核在说:「我先信你,但你得在 5 秒内证明一下。」TCP 用 ACK 完成了证明,UDP 没有这个能力,只能让内核亲自发 ARP 去问。

实测对照(veth 双 netns,强制置 STALE 后各发 8 秒流量):

| 协议 | 起始 | 过程中 | 抓到的 ARP 包 |

|---|---|---|---|

| TCP 长连接 | STALE | DELAY → REACHABLE | 0 个 |

| UDP 持续发送 | STALE | DELAY → PROBE | 2 个 |

所以看到一个 UDP 业务每隔几十秒发一次 ARP,这不是故障,是无连接语义在链路层的物理代价。

ip -s neigh

邻居解析的统计。配合容量参数看:

  
sysctl net.ipv4.neigh.default.gc_thresh3    # 硬上限,默认 1024
  

撞上限之后内核直接丢弃新的 ARP 学习请求,直到 GC 清到 gc_thresh2 之下。现场证据在 dmesg 里:

  
neighbour: arp_cache: neighbor table overflow!
  

看到这一行,「Pod 间偶发 No route to host、重试又好了」就有解释了:表没满时是 FAILED(问了没人应),表满了连问都问不出去。

arping -c 3 -A -I eth0 192.168.1.100

发免费 ARP,用于 VIP 漂移后刷新上游交换机的 MAC 表。

  
背景:VIP 漂了,但交换机 MAC 表还记着旧机器,最长要等 300 秒老化。
  
判据:切换成功但业务断五分钟 → 就是这段窗口。
  

ip -d link show type bridge / bridge link show

看 veth 拓扑、对端是否插进 bridge、state 是否 forwarding。

  
坑:veth 成对。netns 删了,宿主侧 veth 会变孤儿。
  

二、路由与选路(14 条)

ip route get <目的IP> —— 整份清单里最重要的命令

直接打印内核的路由决策结论:为这个目的,选了哪个 dev、哪个 src。

  
ip route get 10.0.0.5
  
ip route get 10.0.0.5 from 10.0.0.1      # 测源地址路由
  
ip route get 10.0.0.5 mark 0x64          # 测策略路由
  

回归正题:这是「内核会怎么选」,不是「配置文件想让它怎么选」。

  
坑:用 ip route show 代替它。路由表里有十条,但内核只选一条,选哪条才是问题。
  
判据:流量不走隧道、metric 莫名其妙、被 /32 抢占——这些都先看这一条。
  

ip route show [table all]

列出路由条目,用来解释上一条为什么得出那个结论。

  
关键:只查 main 表会漏掉 ip rule 引到别的表的流量。
  
坑:metric 只在同前缀长度时比较。前缀比直连短,metric 根本没参与比较。
  

ip rule show

策略路由规则,按优先级数字从小到大匹配,先命中先走。

  
判据:「路由表里有却不走」——一半以上是包被 ip rule 引到另一张表了。
  

ip rule add from 203.0.113.9/32 lookup 100

  
ip route add default via 203.0.113.1 dev eth1 table 100
  

源地址匹配的流量进 100 号表。这是多出口选路的标准做法。

ip route add / replace / del

  
ip route add 10.0.0.0/24 dev wg0 metric 50
  
ip route replace default via 10.0.0.1 dev eth0 metric 100   # 原子替换
  
ip route del 10.0.0.0/24 dev wg0
  
  
坑:网关必须在直连网段内,否则内核直接拒绝(Network is unreachable)。
  
坑:排障时临时加的路由,改完必须清——忘了清就是下一个故障。
  
关键:replace 是原子的,但 ARP 不是。切换瞬间新网关 MAC 还没学到,会丢几个包。
  
      缓解办法:切换前先 ping 一下新网关,把 ARP 提前建立。
  

ip -6 route get <v6 目的>

  
坑:ip -6 addr 有地址就以为 v6 可用。v4/v6 的路由是两个独立世界。
  

三、socket 与连接(13 条)

ss -tulnp

所有监听端口、绑定地址、归属进程。暴露面审计第一步。

  
坑:宿主看不到监听 ≠ 服务没起,可能在另一个 netns。用 nsenter 进去看。
  
坑:管理服务绑 0.0.0.0 被当成「方便」,实际是暴露面。
  

ss -tan state close-wait|time-wait|syn-recv

  
ss -tan state close-wait | wc -l      # 应用没 close() 的连接数
  
ss -tan state syn-recv | wc -l        # 半连接队列积压
  
ss -tan state time-wait | wc -l
  

这四行能分开「网络问题」和「程序问题」:

  
CLOSE-WAIT 堆积   → 应用代码没调 close(),调内核参数完全无效
  
SYN-RECV 堆积     → 半连接队列满了,或者 SYN 被丢在回程
  
TIME-WAIT 多      → 主动关闭方才有,正常;调 tcp_fin_timeout 没用
  

ss -lnt

LISTEN 状态下 Recv-Q / Send-Q 含义被改写,这是最常被误读的一条:

  
LISTEN 时:
  
  Recv-Q = 已完成三次握手、但应用还没 accept() 的连接数
  
  Send-Q = 队列上限(不是已发送字节)
  
  
判据:Recv-Q 接近 Send-Q → 服务进程活着但 accept 不过来,
  
      表现为「服务活着但建连超时」。这类故障 ping 和 curl -I 都测不出来。
  

ss -ti [dport = :443]

单条连接的 rtt / retrans / cwnd / snd_wnd。

  
ss -ti 'dport = :443' | grep -o 'retrans:[0-9]*'
  
  
关键:实际可发送量 = min(cwnd, snd_wnd) - bytes_in_flight。
  
     发送卡住时先看被谁顶死:
  
       snd_wnd 顶死 → 对端消费慢(应用读得慢,或接收缓冲小)
  
       cwnd 锁住   → 自己拥塞控制在限速(丢包/高 RTT)
  
坑:只盯 cwnd 会漏掉「接收方读得慢」;只盯 retrans 会漏掉「窗口不足」。
  

ss -s

  
坑:输出里没有 "ports" 字样。ss -s | grep ports 永远是空的。
  
    端口用量要看 ip_local_port_range + established 计数。
  

四、隧道(WireGuard)

wg show [wg0]

控制面和数据面的唯一入口。

  
wg show wg0
  
wg show wg0 latest-handshakes    # 脚本友好
  
wg show wg0 transfer             # 双向字节数
  
wg show wg0 allowed-ips
  
wg show all dump                 # 制表符分隔,喂监控
  
  
关键:重握手约每 120 秒一次。30 秒内时间戳不变是正常的。
  
阈值:< 180 秒(120 秒 + 50% 余量)。
  
坑:有握手 ≠ 包进了隧道(要看 AllowedIPs)。
  
坑:transfer 是累加值,判吞吐要用两次采样的增量。
  

wg set / wg syncconf

  
wg set wg0 peer <公钥> allowed-ips 10.0.0.0/24
  
wg set wg0 peer <公钥> allowed-ips 10.0.0.0/24,192.168.31.0/24
  
wg set wg0 peer <公钥> persistent-keepalive 25
  
wg syncconf wg0 <(wg-quick strip wg0)
  

留下一个容易理解错的点:AllowedIPs 是覆盖语义,不是追加;而且它同时承担两件事——发送时选路、接收时准入。

  
坑:只改一侧 → 一半通(能发不能收)。
  
坑:/32 和 /24 是范围,不是精度。
  
坑:syncconf 和 setconf 都会删除文件里没有的 peer,但前者不重置未变 peer。
  
坑:少了 wg-quick strip → wg 报不认识的键(Address/MTU/DNS 这些它不认)。
  

persistent-keepalive 25 到底是什么

25 秒对抗的是路径上 NAT 设备的 UDP 映射老化(常见 30–300 秒),

不是本机 conntrack 的 30 秒 UDP 超时。

这两件事经常被混为一谈。因果一错,调优方向就全错。


五、netfilter(47 条)

iptables -vnL [链]

  
iptables -vnL
  
iptables -vnL FORWARD --line-numbers | head -8
  
  
关键:计数涨只说明匹配,不说明 hook 选对了。
  
坑:DNAT 之后目的地址变了,包走 FORWARD 而不是 INPUT。
  
    写在 INPUT 链的规则永远碰不到它——这是最高频的一条死路。
  

iptables -t nat -vnL [PREROUTING|POSTROUTING]

  
关键:nat 表一条连接只查一次,所以计数只涨 1 是正常的。
  
判据:计数涨了 = 改写成功?不一定。要看 conntrack 里有没有成对的会话。
  

iptables -S

规则的可复现文本形式。

  
坑:只看 -L 不看 -S,容易漏掉链的默认策略(-P INPUT DROP 这类)。
  

建立一条能用的基线

  
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
  
iptables -A INPUT -m conntrack --ctstate INVALID -j DROP
  
iptables -P INPUT DROP
  

第一条几乎总是 ctstate。 没有它,回程包会被拦,你会看到「请求出去了、响应回不来」。

IPv6 的默认全通陷阱

  
ip6tables -S | head -20
  
  
坑:ip6tables 的默认策略可能是 -P INPUT ACCEPT(全放行)。
  
    v4 严、v6 裸奔,是极常见的配置失衡。
  

v6 最小防护基线:

  
ip6tables -A INPUT -p ipv6-icmp --icmpv6-type 1 -j ACCEPT    # 目的不可达
  
ip6tables -A INPUT -p ipv6-icmp --icmpv6-type 2 -j ACCEPT    # 包太大
  
ip6tables -A INPUT -p ipv6-icmp --icmpv6-type 3 -j ACCEPT    # 超时
  
ip6tables -A INPUT -p ipv6-icmp --icmpv6-type 133:136 -j ACCEPT  # RS/RA/NS/NA
  
ip6tables -A INPUT -p ipv6-icmp -j DROP
  

ICMP:只禁 Echo,别封 Type 3 与 11

  
iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 2/s -j ACCEPT
  
iptables -A INPUT -p icmp --icmp-type fragmentation-needed -j ACCEPT
  
iptables -A INPUT -p icmp --icmp-type time-exceeded -j ACCEPT
  
  
反例:iptables -A INPUT -p icmp -j DROP
  
    全禁 ICMP = 制造 PMTUD 黑洞。大包被丢,小包正常,症状极其难查。
  

nft list ruleset

  
关键:inet 族一张表同时管 v4/v6。
  
坑:看到 table inet 就以为都齐了,没确认规则落在哪个 priority。
  
坑:iptables-nft 兼容表可能同时存在,两套规则并存。
  

nmap -Pn -p <端口> <公网IP>

从外部验证暴露面。

  
判据:本机 ss 有监听 + 云侧放行 + 主机规则放行,三者缺一不可。
  

六、conntrack(3 条)

conntrack -L

  
conntrack -L | grep 8080
  
  
关键:同一会话应该有成对的两行(正向 + 反向)。
  
坑:查不到就断定「没建连」——可能走了 XDP 旁路,或者被 NOTRACK 跳过。
  

conntrack -S

  
conntrack -S
  

看 insert_failed / drop / early_drop。这是表满的直接证据,即便系统日志里什么都没有。

cat /proc/sys/net/netfilter/nf_conntrack_count / max

  
cat /proc/sys/net/netfilter/nf_conntrack_count
  
cat /proc/sys/net/netfilter/nf_conntrack_max
  
  
判据:比值 > 0.9 且失败与并发峰值同步 → 表满。
  
关键:每条约 300 字节常驻内核内存,且不可交换。
  
      100 万条 ≈ 300 MB。1 GB 内存的网关上,调大上限可能先触发 OOM。
  
顺序:永远先降产生速率,再评估调大。
  

七、抓包(9 条)

三档深度,成本递增

| 档 | 命令 | 拿到什么 |

|---|---|---|

| 一 | ip -s link / nstat -az / conntrack -S | 计数器,包在不在 |

| 二 | tcpdump -ni eth0 -c 20 '<filter>' | 头部,谁发给谁 |

| 三 | tcpdump -s0 -w /tmp/cap.pcap | 全长载荷 |

先数计数,再抓包。 抓包成本最高,而且会引入新的怀疑对象。

tcpdump -ni eth0 -c 20 'tcp port 8099'

  
关键参数:
  
  -n  不做反向 DNS(否则你会等解析超时)
  
  -i  指定接口;隧道场景 wg0 是明文、eth0 是 UDP 密文
  
  -c  限数,防止刷屏
  
坑:默认只抓包头前若干字节。判「大包是否被丢」时,会以为包不存在。
  

判读重点:[S] 之后有没有 [S.]、重传间隔、包长是否贴着 MTU。

tcpdump -ni eth0 udp port 51820

确认 WireGuard 外层密文包。

  
关键:外层看不到内层 IP——它被加密了。
  
     在 eth0 找不到内层地址,不等于没走隧道。
  
长度:148 / 92 字节是握手包。
  

tcpdump -s0 -w /tmp/cap.pcap

第三档,抓全长。

  
坑:加密流量看不到载荷。
  
坑:Wireshark 标 [TCP Previous segment not captured] 时,
  
    先确认是不是抓包侧自己丢了。
  
坑:含敏感数据要脱敏。
  

八、路径与 MTU(21 条)

ping -M do -s <size> <dst> —— 找路径 MTU

-M do 禁止分片(DF 位),-s 是载荷大小。

  
# 前提:wg0 MTU = 1420,且 IP 头为常规 20 字节(未开 IP Options)
  
ping -M do -s 1392 10.0.0.5    # 1392 + 8(ICMP) + 20(IP) = 1420  通
  
ping -M do -s 1393 10.0.0.5    # 1421 > 1420                     不通
  
  
铁律:不写链路 MTU 直接抄 1392,换环境就错。
  
      1422/1423 那组数字只在路径 MTU = 1450 时成立。
  

二分法找边界:

  
for s in 1200 1300 1350 1380 1392 1400; do
  
  ping -c1 -W1 -M do -s $s 8.8.8.8 >/dev/null 2>&1 \
  
    && echo "$s OK" || { echo "$s FAIL"; break; }
  
done
  

ping -c 4 <dst>

  
坑:默认载荷 56 字节,测不出任何 MTU 问题。
  
坑:ping 通 ≠ 业务通;ping 不通 ≠ 服务不可用。
  

traceroute -T -p 443 -n vs traceroute -I -n

TCP 探针穿透力最强,ICMP 探针对比着看。

  
判据:两种探针在某一跳开始分叉 → 那一跳就是策略设备(防火墙/负载均衡)。
  
坑:中间的星号不是「不通」,是「这一跳没把 ICMP 送回给我」。
  

mtr -rwz -c 10 <dst>

持续探测 + 丢包率统计。

  
关键:要写采样次数。短跑一次噪声极大。
  
判据:某一跳 100% 丢包但后续跳恢复正常 → 忽略那一跳。
  

九、DNS(13 条)

dig 家族

  
dig example.com A                    # 基本查询
  
dig +short NS example.com            # 权威服务器
  
dig @ns1.example.com example.com A   # 直接问权威
  
dig @127.0.0.53 example.com A        # 问本地 stub
  
  
坑:只看一次结果,不比对解析器与 TTL。
  
    三个公共解析器结果不一致 = 缓存未过期或劫持。
  

dig +tcp vs dig +notcp —— 区分两种失败

  
dig +tcp example.com TXT       # 走 TCP
  
dig +notcp example.com DNSKEY  # 强制 UDP
  
dig +bufsize=512 +ignore example.com TXT
  
  
判据:+tcp 能返回、+notcp 超时 → 大 UDP 包在路径上被丢了。
  
      A 记录正常不代表 DNS 全好,大记录类型(TXT/DNSKEY)走的是另一条路。
  

getent ahosts example.com

走 glibc 的解析路径,和 dig 不是一条路。

  
为什么重要:dig 直接发 DNS 查询,getent 走 NSS + nscd + glibc。
  
            「dig 能解析但应用解析不了」时,差别就在这里。
  

5 秒 DNS 延迟的物质基础

  
strace -f -e trace=network -s 60 python3 -c "socket.getaddrinfo('x.example.com',80)"
  

输出里会有这么一行:

  
sendmmsg(3, [{qtype=A}, {qtype=AAAA}], 2, MSG_NOSIGNAL) = 2
  

一次系统调用,同一个 socket、同一个源端口,两个包背靠背发出。

报文里 \0\1 是 A(type 1),\0\34 是 AAAA(type 28)。两个包五元组完全相同 → 对 conntrack 来说是一条会话,两条报文几乎同时到达,争抢同一个表项创建槽位。

改配置后的对照:

| 配置 | 系统调用序列 | 两包间隔 |

|---|---|---|

| 默认 | sendmmsg(fd3, [A][AAAA], 2) | 微秒级 |

| single-request-reopen | sendto(A) → recvfrom → connect → sendto(AAAA) | 70 ms |

后者把并发改成了串行,竞态消失。

两条修复路径:

  • 改客户端(single-request-reopen):从 glibc 侧消除并发

  • 改路径(NodeLocal DNSCache):让查询不出节点,不经过 conntrack

两条都不需要动 DNS 服务器——问题从来不在那一侧。

resolvectl flush-caches

  
关键:缓存有六层(浏览器/系统/本地 stub/运营商/公共递归/权威)。
  
      你只能清第 3 层。清了还不生效,说明等的是 TTL。
  

十、TLS 与 HTTP(22 条)

openssl s_client

  
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null \
  
  2>&1 | grep "Verify return code"
  
  
关键:-servername 传 SNI,不传会拿到默认证书。
  
判据:Verify return code: 0,且证书链至少 2 张(叶子 + 中间)。
  
背景:浏览器有补链缓存,curl/Java 没有。
  
      所以「浏览器正常、命令行报错」最常见的根因就是链不全。
  

openssl verify

  
openssl verify -CAfile ca.pem -untrusted intermediate.crt server.crt
  
  
坑:只验叶子不验链。
  

openssl x509

  
openssl x509 -in client.crt -noout -text | grep -A2 "Extended Key Usage"
  
  
判据:mTLS 的客户端证书 EKU 必须是 clientAuth。
  
坑:「证书尚未生效」常见于容器时钟漂移,只有部分机器报错。
  

curl -w —— 一条命令拆开三层耗时

  
curl -sS -o /dev/null -w \
  
  "dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n" \
  
  https://example.com
  

哪个数大,问题就在那一层。

  
坑:只看 total,不知道卡在 DNS 还是 TLS。
  

curl -4 / -6 / --http2 -v

  
curl -6 -s -o /dev/null -w "%{time_total}\n" https://example.com
  
curl -sv --http2 https://example.com 2>&1 | grep ALPN
  
  
判据:v4 与 v6 耗时差应 < 1 秒。
  
坑:ALPN 协商成功 ≠ 请求真的走了 h2。要看响应头是不是 HTTP/2。
  

curl -I 看缓存与 Range

  
curl -sI https://example.com | grep -iE "cache-control|age|etag"
  
curl -sI -H "Range: bytes=0-99" https://example.com | head -3
  
  
关键:no-cache 是「可存但用前验证」,no-store 才是「不存」。
  
判据:Range 请求应返回 206。返回 200 说明 Range 被代理剥掉了。
  

十一、队列与网卡(14 条)

ip -s link show <iface>

  
ip -s link show eth0
  

输出里的 dropped 分两个方向,含义完全不同:

  
rx_dropped 涨  → 本机来不及处理(软中断/缓冲区)
  
tx_dropped 涨  → 本机发不出去(无路由/无 peer/队列满)
  
rx_errors + rx_crc_errors 涨 → 线缆/光模块/双工
  
tx_carrier_errors 涨         → 链路闪断
  

完整统计在 /sys/class/net/<dev>/statistics/,那里有 21 个具体错误计数器,比 ip 合并的 errors/dropped 精确得多:

  
for f in /sys/class/net/eth0/statistics/*; do
  
  printf '%-28s %s\n' "$(basename $f)" "$(cat $f)"
  
done
  

21 个全为 0,说明从驱动到链路层没有一处出错。

ethtool

  
ethtool eth0 | grep -E "Speed|Duplex|Link detected"
  
ethtool -S eth0 | grep -iE "err|drop|crc|fcs"
  
ethtool -a eth0
  
  
判据:Speed 符合预期、Duplex: Full。
  
坑:一端自协商、一端写死,是最容易出双工不匹配的配法。
  
     链路起来了、小流量正常,很多人就到这儿停了。
  
坑:坏帧在进内核之前就被网卡丢掉了,tcpdump 根本看不到。
  

容器里没有 ethtool 怎么办

  
ip -d link show eth0
  

输出里有:

  
gso_max_size 65536  gso_max_segs 65535
  
tso_max_size 524280 tso_max_segs 65535
  

这比 ethtool -k 的 on/off 更有用——它给出聚合的上限数值。

  
判据:平均 TX 包大小超过 MTU,说明聚合在生效(不是错误)。
  
      1686 B 的均值意味着「聚合过,但很不充分」。
  
      上限是 512 KB,说明流量都是并发小请求,攒不起大包。
  

tc -s qdisc show dev <iface>

  
tc -s qdisc show dev eth0
  
  
关键:多队列网卡会列出多个实例(mq / fq_codel)。
  
判据:dropped 不增长。
  
坑:丢包发生在主机队列时,链路层计数器看不出来。
  

tc qdisc add ... netem —— 造故障

  
tc qdisc add dev eth0 root netem delay 100ms
  
tc qdisc change dev eth0 root netem delay 100ms 20ms      # 100ms ± 20ms 抖动
  
tc qdisc change dev eth0 root netem loss 1%
  
tc qdisc change dev eth0 root netem delay 10ms reorder 25%
  
tc qdisc change dev eth0 root netem delay 50ms rate 10mbit
  
tc qdisc del dev eth0 root                                # 清理!
  
  
关键:只在出口方向生效。加在不同接口,测的是不同链路。
  
铁律:演练完必须清理。忘了 tc qdisc del,就是留下一个持续故障。
  

十二、内核计数与参数(21 条)

nstat -az

协议栈层的全部计数。

  
nstat -az | grep -i drop
  
nstat -az | grep -i conntrack
  
  
纪律:看增量,不看绝对值。
  
坑:只查网卡计数,漏掉协议栈层的丢弃。
  

cat /proc/net/softnet_stat

第二列是软中断队列丢包。

  
判据:它涨而网卡 rx_dropped 是 0 → CPU 处理不过来。
  
      这是「网卡没丢包但业务丢包」的答案。
  

sysctl:真正需要看的几个

  
sysctl net.ipv4.ip_forward                    # 是否开了转发
  
sysctl net.ipv4.conf.all.rp_filter            # 反向路径过滤
  
sysctl net.ipv4.tcp_syncookies                # SYN 保护
  
sysctl net.ipv4.tcp_abort_on_overflow         # 队列溢出时回 RST 还是丢
  
sysctl net.ipv4.tcp_congestion_control        # 当前拥塞算法
  

rp_filter 的坑值得单说:

  
关键:最终生效取 all 与接口两者的最大值(最严的那个)。
  
坑:「规则对但不回包」就关掉它——那是拆防线。
  
    正确顺序是先修路由对称(让去程和回程走同一路径)。
  

调优的几个常改项

  
sysctl -w net.core.somaxconn=4096
  
sysctl -w net.ipv4.tcp_congestion_control=bbr      # 需内核支持
  
sysctl -w net.netfilter.nf_conntrack_max=524288
  
sysctl -w net.ipv4.neigh.default.gc_thresh3=8192
  
  
坑:tcp_congestion_control 只对新建连接生效。改了不重建连接,会看到「没用」的假象。
  
坑:调大上限只是把撞墙时间推后。真正的解法是缩小广播域 / 降产生速率。
  

cat /proc/sys/net/ipv4/ip_local_port_range

  
默认约 32768–60999,约 28000 个端口。
  
理论上限 ≈ 端口数 / 60(TIME_WAIT 时长)。
  
报「无法分配地址」时,先查这里 + ss -s。
  

十三、命名空间(4 条)

nsenter -t <pid> -n ss -tulnp

  
nsenter -t <pid> -n ss -tulnp | grep 8080
  
  
坑:宿主 ss 看不到容器内监听 ≠ 服务没起。
  
    直接判「服务没起」,会浪费大量时间——这是容器排障的第一大坑。
  

ip netns list / ip -d link show type veth

  
关键:veth 成对。删掉一半会留孤儿。
  

十四、故障注入与守护(5 条)

flock -n 9

  
exec 9>/tmp/guard.lock
  
flock -n 9 || exit 0
  

单实例锁,比 PID 文件可靠。

  
关键:锁随进程退出由内核释放,所以锁文件永不删。
  
反例:退出时 rm 锁文件 → 制造竞态(A 删了,B 建了,C 也拿到锁)。
  

strace -e trace=connect

  
strace -e trace=connect curl -s -m2 http://198.51.100.77:80
  

本机和远端看到的完全不一样,这一条能证明:

  
远端视角:100% 静默丢包,SYN 重传,几十秒后 ETIMEDOUT
  
本机视角:connect(...) = -1 EINVAL,0 毫秒返回
  

原因:blackhole 路由在本机发起的路径上直接把错误返回给系统调用,包根本没生成。

  
判据:curl 报 after 0 ms 连不上 → 查本机策略路由,不要抓包。
  
      网卡上一个包都没有,tcpdump 只会给你一段空白,
  
      然后你会开始怀疑抓包过滤写错了、接口选错了——那才是真正浪费时间的开始。
  
      先跑 ip route show type blackhole,十秒钟就有答案。
  

十五、二层配置与 mTLS(12 条)

排查之外,书里还有一类命令是「造环境」用的。它们不出现在故障现场,但你要复现故障、或者搭实验场时离不开。

VLAN 子接口

  
ip link add link eth0 name eth0.10 type vlan id 10
  
ip addr add 192.168.10.5/24 dev eth0.10
  
ip link set eth0.10 up
  
  
坑:只加子接口不 up,地址配了也不通。
  
坑:父接口 eth0 必须 up,子接口才可能 up(依赖关系)。
  
判据:ip -d link show eth0.10 能看到 vlan protocol 802.1Q id 10。
  

macvlan / ipvlan

  
ip link add mv0 link eth0 type macvlan mode bridge
  
ip link add iv0 link wlan0 type ipvlan mode l2
  
  
关键区别:
  
  macvlan 给每个虚拟接口分配独立 MAC,宿主与容器同网段
  
  ipvlan  共享父接口 MAC,只分 IP
  

  
坑:macvlan 在无线网卡上基本不可用。原因不是驱动写得差,是协议规范:
  
    IEEE 802.11 帧通常是三地址格式,一条关联链路只能承载一个客户端 MAC,
  
    客户端网卡无法在单条链路上透传多个源 MAC,AP 协议层面直接不认。
  
    这就是为什么这里 ipvlan 挂 wlan0、macvlan 挂 eth0。
  
坑:macvlan 子接口与父接口之间不能直接通信(内核设计如此)。
  

bond

  
ip link add bond0 type bond mode 802.3ad
  
ip link set eth0 master bond0
  
ip link set eth1 master bond0
  
ip link set bond0 up
  
  
坑:一端配了 bond、对端没配 LACP → 反而更糟(部分链路被哈希到黑洞)。
  
判据:cat /proc/net/bonding/bond0 看 Partner Mac 是否为非零。
  
      为零 = 对端根本没跑 LACP。
  

netfilter 基线(nftables 版)

  
nft add table inet myfilter
  
nft add chain inet myfilter input '{ type filter hook input priority 0 ; policy drop ; }'
  
nft add rule inet myfilter input ct state established,related accept
  
nft add rule inet myfilter input tcp dport 22 accept
  
  
关键:priority 0 是 filter 钩子的标准位置。
  
      conntrack 那条必须排在最前面,否则回程包全被 policy drop 掉。
  

mTLS 客户端证书

  
curl --cert client.crt --key client.key https://api.example.com/
  
curl --cert client-chain.crt --key client.key https://api.example.com/
  
  
判据:第一行报错、第二行成功 → 服务端只认完整链,需要带上中间证书。
  
背景:很多服务端配了 fullchain,但客户端证书链同样可能不全。
  

WebSocket 握手

  
curl -s -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" \
  
     -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: <16字节base64>" \
  
     https://example.com/ws
  
  
判据:期望 101 Switching Protocols。
  
返回 200 说明代理没转发 Upgrade 头(常见于 Nginx 少了 proxy_set_header Upgrade)。
  

十六、反例清单:认出来,别照抄

| 命令/参数 | 为什么是反例 |

|---|---|

| net.ipv4.tcp_tw_recycle=1 | NAT 下会造成部分客户端连不上,Linux 4.12 已移除 |

| ss -s \| grep ports | 输出里没有这个字样,永远 grep 不到 |

| nc -u <host> <port> | 默认不做等待,「没输出」不携带任何信息 |

| 全局 rp_filter=0 | 拆防线。应先修路由对称 |

| iptables -A INPUT -p icmp -j DROP | 制造 PMTUD 黑洞,大包被丢小包正常,极难查 |

| nf_conntrack_max 直接翻倍 | 每条约 300B 常驻不可交换,小内存机器先 OOM |

| -s 1392 不给前提 | 换 MTU 就错 |

| tcp_tw_reuse=1 解决服务端 TIME_WAIT | 只对出向连接生效,入向无效。对纯 Web 服务端无效;但对反向代理(Nginx 连上游时自己是客户端)有效 |

| tcp_max_tw_buckets 调得过小 | TIME_WAIT 被提前回收,旧包到达会回 RST |


最后:这份清单真正的用法

不是「照着敲」,是「照着排除」。

正确的顺序永远是:

  
1. 先定层:这个现象属于 L2 / L3 / L4 / L7 的哪一层?
  
2. 再取证:那一层有哪几条计数/状态可以看?
  
3. 写预期:如果正常,我会看到什么?
  
4. 执行并比对:不符 → 方向;相符 → 下一层。
  

命令是第 4 步的事。跳过前 3 步直接找命令,你会有 216 个选项和零个方向。


清单来自《从协议到自愈》,216 条命令、31 个工具族。书里的每条命令都带「前提 / 判据 / 出处」,这里按博客可读性重排过。