0.13 网络配置与排障
ip addr 的每一段怎么读?为什么 ping 通、端口却连不上?ss 的 Recv-Q/Send-Q 在 LISTEN 和 ESTAB 两种状态下分别是什么意思?DNS 该用 dig 还是 getent?防火墙到底该看 ufw 还是 nft?SSH 连不上该从哪一层查?
按链路→地址→路由→端口→应用五层排网络故障,每层给出命令和证据;用 netplan try 安全改地址;用 ss -ti 看重传、用 tcpdump 看 SYN/RST;用密钥登录、跳板与端口转发排一次 SSH 故障;判断「ping 通」到底证明了什么。
0.12 的服务起来之后,下一层问题就是「端口通不通」。网络排障的困难不在命令多,而在证据链容易断:ping 通了就以为网络没问题,curl 超时就以为带宽不够,ss 看到 LISTEN 就以为应用正常。每一层都有独立的失败方式,跳过一层就会把问题归错类。
这一章按「链路 → 地址 → 路由 → 端口 → 应用」的顺序组织。每层先给一条能立刻跑的命令,再解释输出里哪个字段是证据。Ubuntu 24.04 用 iproute2(ip、ss)和 systemd-resolved;ifconfig、route、netstat 这些旧工具只在 A.1 出现。
配置部分只讲 Ubuntu 默认的 netplan 和 NetworkManager,不展开 systemd-networkd 的所有选项。远程改网络是高风险操作,本章会给 netplan try 的回滚机制和「先留一条后路」的检查清单。
0.13.1 三层:链路、地址、路由
ip 把网络配置拆成对象:link(网卡/链路)、addr(IP 地址)、route(路由)、neigh(邻居表)。排障按这个顺序看,不要一上来就 ping。
$ ip -br link lo UNKNOWN 00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP> enp0s31f6 UP 3c:52:82:1a:2b:3c <BROADCAST,MULTICAST,UP,LOWER_UP> $ ip -br addr lo UNKNOWN 127.0.0.1/8 ::1/128 enp0s31f6 UP 192.168.1.20/24 fe80::3e52:82ff:fe1a:2b3c/64 $ ip route default via 192.168.1.1 dev enp0s31f6 proto dhcp src 192.168.1.20 metric 100 192.168.1.0/24 dev enp0s31f6 proto kernel scope link src 192.168.1.20 $ ip neigh 192.168.1.1 dev enp0s31f6 lladdr 3c:52:82:aa:bb:cc REACHABLE $ ip route get 8.8.8.8 8.8.8.8 via 192.168.1.1 dev enp0s31f6 src 192.168.1.20 uid 1000 cache
ip -br link 的 UP 是管理状态(ip link set up),LOWER_UP 是物理层有信号(网线插好、对端协商成功)。只有 UP 没有 LOWER_UP,说明网卡启用了但链路没通,先查网线、交换机端口、ethtool 的 Link detected。UNKNOWN 对 lo 是正常的,回环没有物理层。
ip -br addr 的 192.168.1.20/24 是地址加前缀长度,/24 决定直连网段;fe80:: 是链路本地 IPv6,自动生成。地址后面可能带 dynamic(DHCP 租约)、secondary(辅助地址)、deprecated(IPv6 准备弃用)。valid_lft/preferred_lft 用 ip addr show 看,租约快到期时它们会递减。
ip route 的 default via 192.168.1.1 是默认路由,所有非直连流量走它;proto dhcp 说明是 DHCP 装的,metric 100 是优先级,数值小的优先。多条默认路由时,内核选 metric 最小的;metric 相同则可能按内核策略。ip route get 8.8.8.8 是排障最有用的一条:它告诉你「去这个目的地会走哪张网卡、哪个源地址、下一跳是谁」,不用猜。
ip neigh 是 ARP/IPv6 邻居表,REACHABLE 表示最近确认过;STALE 表示条目还在但需要重新确认;FAILED 表示解析失败,通常是同网段地址没人响应。邻居表为空或大量 FAILED,先查链路和 VLAN,不要先查路由。
链路质量还要看错误计数:
ip -s link show enp0s31f6
ethtool enp0s31f6 | grep -E 'Speed|Duplex|Link detected'
ip -s link 的 errors/dropped 持续增长,说明网卡、驱动、线缆或 VLAN 有问题;ethtool 的 Link detected: no 直接确认物理层断开。这两个证据比 ping 丢包更接近根因。
0.13.2 配置:netplan、NetworkManager 与 systemd-networkd
Ubuntu 用 netplan 做配置前端,后端可以是 systemd-networkd(服务器默认)或 NetworkManager(桌面)。配置文件在 /etc/netplan/*.yaml,改完用 netplan apply 生成后端配置。
DHCP 的最小配置:
network:
version: 2
renderer: networkd
ethernets:
enp0s31f6:
dhcp4: true
optional: true
静态地址:
network:
version: 2
renderer: networkd
ethernets:
enp0s31f6:
addresses: [192.168.1.20/24]
routes:
- to: default
via: 192.168.1.1
nameservers:
addresses: [1.1.1.1, 9.9.9.9]
search: [lab.example.com]
optional: true 表示网卡不存在时启动不等待,适合 USB 网卡或可插拔设备。routes 里 to: default 是默认路由的写法,旧语法 gateway4 已废弃。nameservers.addresses 写 DNS 服务器,search 写搜索域;如果用 systemd-resolved,这些值会出现在 resolvectl status 里。
远程改网络最怕把自己踢下线。netplan try 会应用配置并启动一个 120 秒的倒计时,期间不确认就自动回滚:
sudo netplan try
# 确认网络仍通后按 Enter;否则等待超时自动恢复
sudo netplan apply
netplan try 只对当前会话有效,SSH 断开不会自动确认,超时后回滚——这正是它的价值。仍然建议先开一个带外控制台(云厂商的 VNC/串口),并确认 sshd 监听在 0.0.0.0:22 而不是某个即将被删掉的地址上。
NetworkManager 的机器用 nmcli:
nmcli device status
nmcli connection show
nmcli connection modify 'Wired connection 1' ipv4.method manual \
ipv4.addresses 192.168.1.20/24 ipv4.gateway 192.168.1.1 \
ipv4.dns 1.1.1.1
nmcli connection up 'Wired connection 1'
nmcli connection modify 只改配置,up 才生效;改错后用 nmcli connection down/up 或 nmcli networking off/on 恢复。networkctl status 看 systemd-networkd 的状态,networkctl list 看链路。两套后端不要同时管同一张网卡,否则配置会互相覆盖。
0.13.3 端口与连接:ss 的四个数字
ss 直接读内核的 socket 表,比 netstat 快且信息全。排「端口通不通」先看 LISTEN,再看 ESTAB,最后看统计:
$ ss -lntp State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1024,fd=3)) LISTEN 0 511 127.0.0.1:631 0.0.0.0:* users:(("cupsd",pid=812,fd=7)) $ ss -tnp state established Recv-Q Send-Q Local Address:Port Peer Address:Port Process 0 0 192.168.1.20:22 192.168.1.50:54321 users:(("sshd",pid=2048,fd=4)) $ ss -s Total: 142 TCP: 18 (estab 3, closed 9, orphaned 0, timewait 8) $ ss -ti state established '( sport = :22 )' ESTAB 0 0 192.168.1.20:22 192.168.1.50:54321 cubic wscale:7,7 rto:204 rtt:0.045/0.020 ato:40 mss:1460 pmtu:1500 rcvmss:1460 retrans:0/0 reordering:3
-l 只看 LISTEN,-t TCP,-n 不解析名字,-p 显示进程(需要权限看别人的进程)。-u 看 UDP,-x 看 Unix socket,-a 看全部。
Recv-Q/Send-Q 的含义随状态变化,这是最容易误读的地方:
| 状态 | Recv-Q | Send-Q | 什么时候是问题 |
|---|---|---|---|
LISTEN | 当前已完成握手、等应用 accept 的连接数 | 监听队列上限(backlog) | Recv-Q 接近 Send-Q,应用 accept 太慢 |
ESTAB | 已收到、应用还没读的字节数 | 已写入、还没被对端确认的字节数 | 持续非零且增长,应用读/写慢或网络拥塞 |
TIME-WAIT | 通常为 0 | 通常为 0 | 数量过多会占本地端口,但通常不是故障 |
LISTEN 的 Recv-Q 是已完成三次握手但还没被 accept 的队列长度;Send-Q 是 backlog 上限。Recv-Q 长时间贴近 Send-Q,说明应用 accept 循环太慢或卡住,新连接会被内核丢弃,客户端看到超时而不是拒绝。调 somaxconn 和应用的 backlog 只是缓解,根因通常在应用。
ESTAB 的 Send-Q 持续非零,说明数据发出去了但对端没 ACK,可能是网络丢包、对端接收窗口满、或本机拥塞窗口受限。ss -ti 的 retrans 是重传次数/总数,rtt 是平滑 RTT/偏差;retrans 持续增长、rtt 抖动大,先查链路质量,再查对端。
ss -s 给总量:estab 是已建立连接,timewait 是主动关闭后等待 2MSL 的连接,orphaned 是没有归属进程的 socket。TIME-WAIT 本身不是问题,它是 TCP 的正常状态;大量短连接会让它堆积,但通常几万个才需要调 tcp_tw_reuse 之类的参数。
lsof -i :80 也能看谁占了端口,但比 ss 慢;容器里 ss 看到的是容器命名空间的 socket,宿主机上要用 nsenter -t <pid> -n ss -lntp 或 docker exec。
0.13.4 DNS:先确认解析,再怀疑网络
DNS 失败和网络失败的表现很像:curl 都超时。区别在于 curl 的报错文本,以及 getent/dig 的结果。
$ readlink -f /etc/resolv.conf /run/systemd/resolve/stub-resolv.conf $ cat /etc/resolv.conf nameserver 127.0.0.53 options edns0 trust-ad search lab.example.com $ resolvectl status | head -10 Global Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported resolv.conf mode: stub Link 2 (enp0s31f6) Current Scopes: DNS DNS Servers: 192.168.1.1 $ getent hosts example.com 93.184.216.34 example.com $ dig +short example.com 93.184.216.34
/etc/resolv.conf 在现代 Ubuntu 上通常是指向 systemd-resolved stub 的符号链接,nameserver 127.0.0.53 是本地 stub,不是真实 DNS。直接编辑这个文件会被 systemd-resolved 覆盖;要改 DNS,改 netplan 的 nameservers 或 resolvectl dns。
getent hosts 走 NSS(/etc/nsswitch.conf 的 hosts: 行),会先查 /etc/hosts、再查 DNS、可能查 mDNS;dig 直接发 DNS 查询,绕过 NSS 和 /etc/hosts。两者结果不一致时,先看 nsswitch.conf 和 /etc/hosts,不要怀疑 DNS 服务器。
hosts: files mdns4_minimal [NOTFOUND=return] dns
这行的意思是:先查 files(/etc/hosts),再查 mDNS,NOTFOUND=return 表示 mDNS 明确说没有就停止,不再查 DNS。很多「DNS 解析不对」其实是 /etc/hosts 里有一条旧记录,或者 mDNS 抢先返回了结果。
resolvectl 是 systemd-resolved 的控制器:
resolvectl status # 全局和各链路的 DNS、DNSSEC、搜索域
resolvectl query example.com # 用 resolved 的解析路径查,显示协议与缓存
resolvectl dns enp0s31f6 1.1.1.1 # 临时改某张网卡的 DNS
resolvectl flush-caches # 清缓存
resolvectl query 会显示「通过哪个协议、哪个服务器、是否命中缓存」,比 dig 更接近应用实际走的路径。dig +trace example.com 从根开始逐级查,适合确认委派链;dig +tcp example.com 强制 TCP,排查 UDP 53 被拦的情况。
DNS 排障顺序:cat /etc/nsswitch.conf → cat /etc/hosts → getent hosts name → resolvectl query name → dig @<server> name → dig +trace name。每一步都能把「本机配置」「NSS」「resolved」「上游 DNS」「委派」分开。
0.13.5 连通性工具:ping、mtr、nc、curl
四个工具测四层,不能互相替代:
| 工具 | 测什么 | 通过说明 | 不通过说明 |
|---|---|---|---|
ping | ICMP 可达 | 到目标的 IP 层基本通 | 可能只是 ICMP 被拦,不代表 TCP 不通 |
mtr | 路径与逐跳丢包 | 路径稳定、丢包集中在某跳 | 定位是本地、运营商还是对端 |
nc -vz | TCP 端口 | 三次握手成功 | 端口没开、防火墙拦、路由不通 |
curl -v | 应用层(HTTP/TLS/代理) | 拿到响应 | DNS、TCP、TLS、HTTP、代理哪段失败 |
$ ping -c 3 -W 1 192.168.1.1 PING 192.168.1.1 (192.168.1.1) 56(84) bytes of data. 64 bytes from 192.168.1.1: icmp_seq=1 ttl=64 time=0.412 ms 64 bytes from 192.168.1.1: icmp_seq=2 ttl=64 time=0.501 ms 64 bytes from 192.168.1.1: icmp_seq=3 ttl=64 time=0.388 ms --- 192.168.1.1 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2003ms rtt min/avg/max/mdev = 0.388/0.433/0.501/0.047 ms $ nc -vz example.com 443 Connection to example.com 443 port [tcp/https] succeeded! $ 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 dns=0.012 tcp=0.031 tls=0.089 ttfb=0.142 total=0.143 $ mtr -rwc 3 1.1.1.1 Start: Sun Mar 2 10:45:00 2025 HOST: lab Loss% Snt Last Avg Best Wrst StDev 1.|-- 192.168.1.1 0.0% 3 0.4 0.5 0.4 0.6 0.1 2.|-- 10.0.0.1 0.0% 3 3.1 3.2 3.0 3.4 0.2 3.|-- 1.1.1.1 0.0% 3 9.8 9.9 9.7 10.1 0.2
ping 通只证明 ICMP Echo 能到目标并被回。很多云厂商和防火墙默认拦 ICMP,所以「ping 不通但 HTTPS 正常」很常见;反过来「ping 通但端口不通」也很常见。不要用 ping 的成败推断 TCP。
nc -vz 只做 TCP 握手,不传数据。Connection refused 表示包到了主机但端口没监听(或有防火墙发 RST);Connection timed out 表示包没到或回不来(路由、安全组、防火墙 DROP)。这两个错误的区别比「通/不通」有用得多。
curl -w 把各阶段耗时拆开:time_namelookup 是 DNS,time_connect 是 TCP,time_appconnect 是 TLS,time_starttransfer 是首字节,time_total 是总时间。哪一段占比大,就查哪一层。curl -v 还能看到请求头、响应头、TLS 版本、代理环境变量。
代理是常见的隐形层:http_proxy/https_proxy/no_proxy 设了但没生效,或者生效了但代理不通,curl 会超时而 nc 直连正常。用 env | grep -i proxy 和 curl --noproxy '*' 对比。
0.13.6 抓包:tcpdump 最小集
当 ss 和 curl 都给不出足够证据时,抓包看「包到底有没有到、回了什么」。最小集只要五条命令:
sudo tcpdump -nn -i any -c 20 'tcp port 443'
sudo tcpdump -nn -i any -w /tmp/cap.pcap 'host 93.184.216.34 and tcp port 443'
sudo tcpdump -nn -r /tmp/cap.pcap 'tcp[tcpflags] & tcp-syn != 0'
sudo tcpdump -nn -A -i any 'tcp port 80' | head -40
sudo timeout 10 tcpdump -nn -i any -s0 -w /tmp/cap.pcap 'port 53'
-nn 不解析主机名和端口名,输出更快、更稳定;-i any 抓所有网卡;-c 限制包数;-w 写 pcap,之后可以用 Wireshark/tshark 分析;-r 读 pcap;-A 以 ASCII 显示 payload;-s0 抓完整包(默认已足够,但旧版本默认截断)。过滤器用 BPF 语法:host、net、port、tcp、udp、icmp,组合用 and/or/not。
$ sudo tcpdump -nn -i any -c 4 'tcp port 443'
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
10:46:01.123456 IP 192.168.1.20.54321 > 93.184.216.34.443: Flags [S], seq 123456789, win 64240, options [mss 1460,sackOK,TS val 123456 ecr 0,nop,wscale 7], length 0
10:46:01.234567 IP 93.184.216.34.443 > 192.168.1.20.54321: Flags [S.], seq 987654321, ack 123456790, win 65535, options [mss 1460,nop,wscale 8], length 0
10:46:01.235000 IP 192.168.1.20.54321 > 93.184.216.34.443: Flags [.], ack 1, win 502, length 0
10:46:01.240000 IP 192.168.1.20.54321 > 93.184.216.34.443: Flags [P.], seq 1:518, ack 1, length 517
读这段:Flags [S] 是 SYN,[S.] 是 SYN-ACK,[.] 是 ACK,[P.] 是带数据的 ACK,[R] 是 RST,[F.] 是 FIN。先看有没有 SYN、有没有 SYN-ACK、有没有 RST。只有 SYN 没有 SYN-ACK,说明请求出去了但没回来(路由/防火墙/对端没监听且不回 RST);SYN 后立刻 RST,说明对端端口关闭或防火墙 reject;SYN-ACK 后客户端不回 ACK,通常是客户端防火墙或路由不对称。
抓包文件可能含明文密码、Cookie、内网地址,不要随手发到聊天工具。抓之前用过滤器缩小范围,抓之后 chmod 600,用完删除。长期抓包用 -C/-W 轮转,别把磁盘写满。
0.13.7 防火墙:ufw 与 nftables
Ubuntu 默认装 ufw(Uncomplicated Firewall),底层是 nftables。ufw 适合主机防火墙的常见规则,nft 适合看底层规则和做复杂匹配。
$ sudo ufw status verbose Status: active Logging: on (low) Default: deny (incoming), allow (outgoing), disabled (routed) New profiles: skip To Action From -- ------ ---- 22/tcp ALLOW IN Anywhere 80,443/tcp ALLOW IN Anywhere $ sudo nft list ruleset | head -12 table inet filter { chain input { type filter hook input priority filter; policy drop; ct state established,related accept iif "lo" accept tcp dport 22 accept } }
Default: deny (incoming) 是默认拒绝入站;allow (outgoing) 允许出站;disabled (routed) 表示不做转发。22/tcp 必须先放行再 ufw enable,否则 SSH 立刻断。远程机器上的顺序是:
sudo ufw allow 22/tcp
sudo ufw allow 80,443/tcp
sudo ufw enable
sudo ufw status numbered
ufw enable 会提示可能中断 SSH,输入 y 前确认 22 已放行。改错了用 sudo ufw delete <编号> 或 sudo ufw disable;ufw reload 重新加载规则。ufw limit 22/tcp 对 SSH 做速率限制,防暴力破解;ufw allow from 192.168.1.0/24 to any port 5432 只允许内网访问数据库。
nft 看底层(下面是简化示意,实际 ufw 会生成 ufw-before-input 等链):
sudo nft list ruleset
sudo nft add rule inet filter input tcp dport 8080 accept
sudo nft -f /etc/nftables.conf
nft 的规则按表和链组织,hook input priority filter 表示挂在入站钩子;policy drop 是默认拒绝。ct state established,related accept 放行已建立连接的回包,没有它,出站请求的回包也会被拒。iif "lo" accept 放行回环。
两个容易踩的坑:云安全组在主机防火墙之外,ufw 放行了但云控制台没放行,仍然不通;Docker 发布的端口绕过 ufw,Docker 直接写 nat/filter 规则,ufw deny 8080 不一定拦得住 docker run -p 8080:80。容器场景要同时看 docker ps 的端口映射、nft list ruleset 里的 DOCKER 链,以及云安全组。
旧式的 iptables 命令在 Ubuntu 24.04 上默认是 iptables-nft 前端,语法兼容但规则最终落到 nftables;ifconfig/route/netstat 的对照在 A.1。
0.13.8 故障树:链路→地址→路由→端口→应用
应用 curl -v / 服务日志 / HTTP 5xx / TLS 证书 ↑ 端口 ss -lntp / nc -vz / ufw status / 云安全组 ↑ 路由 ip route get / mtr / traceroute / 策略路由 ↑ 地址 ip addr / DHCP 租约 / netplan / 重复 IP ↑ 链路 ip link / ethtool / dmesg / 网线、VLAN、bond
读图:从下往上查,每一层都要拿到一条证据再往上走。链路层的证据是 LOWER_UP 和 ethtool 的 Link detected: yes;地址层是 ip addr 有期望的地址;路由层是 ip route get 指向正确的出口;端口层是 ss -lntp 有监听且 nc 能握手;应用层是 curl -v 拿到期望的状态码。任何一层没证据就往上猜,都会把问题归错类。
三个典型场景:
ping 通、端口不通。 ping 只证明 ICMP 通。先 nc -vz host port 看是 refused 还是 timeout:refused 说明包到了主机、端口没监听或被 reject,查 ss -lntp 和 ufw;timeout 说明包没到或被 DROP,查路由、云安全组、中间防火墙。tcpdump 同时抓两端,看 SYN 有没有到、有没有回。
DNS 时好时坏。 先 getent hosts 和 dig 对比,排除 /etc/hosts 和 NSS;再 resolvectl query 看命中哪台服务器;最后 dig @server 逐个测上游。/etc/resolv.conf 里多个 nameserver 时,解析器按顺序试,第一个超时会拖慢所有查询;options timeout:1 attempts:2 能缩短等待。
偶发超时、重传高。 ss -ti 看 retrans 和 rtt,mtr 看逐跳丢包,tcpdump 看是否有重复 ACK、RST、零窗口。链路错误看 ip -s link 的 errors/dropped,网卡错包看 ethtool -S。这类问题不要先改应用超时,先把丢包位置定位出来。
排障时把每条命令的输出留在 ticket 里,而不是只写「网络不通」。证据链是:ip -br link/ip addr/ip route get/ss -lntp/nc/curl -v/tcpdump,按顺序贴出来,别人才能复现你的判断。
- 「ping 通就说明网络通」——ICMP 只是 IP 层的一种协议,TCP 端口、DNS、TLS、代理都可能独立失败;云环境经常主动拦 ICMP。
- 「
/etc/resolv.conf可以直接改」——它通常是指向systemd-resolvedstub 的符号链接,改了会被覆盖;改 netplan 的nameservers或用resolvectl dns。 - 「
ss的Recv-Q都是 backlog」——只有 LISTEN 状态是等待 accept 的连接数;ESTAB 状态是接收缓冲里未读的字节数。 - 「
ufw能管住 Docker 发布的端口」——Docker 直接写 nat/filter 规则,绕过 ufw;容器端口还要看docker ps和云安全组。 - 「tcpdump 抓到 SYN 就说明服务收到了」——包可能被内核防火墙、backlog 或 netfilter 丢掉;要对照
ss的监听队列和内核计数器。 - 「
curl超时就是带宽不够」——先看-w的各阶段耗时,DNS、TCP、TLS、代理、服务端处理都可能超时,带宽只是其中一种。
0.13.9 路由表、策略路由与 network namespace
ip route 的默认输出只显示 main 表。Linux 实际有三张表参与普通 IPv4 选路:local(本机地址)、main(默认路由)、default(兜底,通常为空),ip rule 决定按什么顺序查哪张表。
ip rule show
# 0: from all lookup local
# 32766: from all lookup main
# 32767: from all lookup default
优先级数字小的先匹配;local 表优先级 0,保证「发给本机地址的包不会绕到外部」。main 表就是 ip route show 看到的内容。ip route show table all 能看到全部表,包括 NetworkManager 或 Docker 创建的规则。
一条路由的字段:
default via 192.168.1.1 dev enp0s31f6 proto dhcp src 192.168.1.20 metric 100
| 字段 | 含义 | 排障价值 |
|---|---|---|
default / 网段 | 目的 | 是否命中这条路由 |
via | 下一跳 | 网关是否可达、是否在同一广播域 |
dev | 出接口 | 是否绑错网卡(多网卡机器常见) |
proto | 路由来源 | dhcp/ra 说明是自动生成,重启会被覆盖 |
src | 选路时优先使用的源地址 | 多地址时决定从哪个 IP 出去 |
metric | 优先级,小的优先 | 多条默认路由时决定主备 |
proto 的值值得记住:kernel(内核自动)、boot(启动参数)、static(管理员)、dhcp、ra(IPv6 路由通告)、redirect。看到 proto dhcp 就知道手工 ip route del 改不持久,要在 netplan/NetworkManager 里改。
ip route get 是「不实际发包,让内核走一遍选路」:
ip route get 8.8.8.8
ip route get 10.20.0.5 from 10.0.0.7 iif eth1
ip -6 route get 2001:4860:4860::8888
输出会给出 via、dev、src、uid、cache。多网卡机器上「为什么从错误的源 IP 出去」用它一眼就能看出来。ip route get 不考虑防火墙与 NAT,只回答「内核选哪条路」。
策略路由用 ip rule + 自定义表:
# 把 10.0.0.0/24 的流量强制走表 100,表 100 的默认网关是 192.168.1.254
sudo ip route add default via 192.168.1.254 dev eth1 table 100
sudo ip rule add from 10.0.0.0/24 lookup 100 pref 1000
ip rule show
ip route show table 100
ip route get 8.8.8.8 from 10.0.0.7
# 删除规则用 pref,不要用序号猜
sudo ip rule del pref 1000
pref(优先级)相同会报错;ip rule add 默认插在 32765 附近。规则按优先级从 0 往上匹配,命中即停。fwmark 让 iptables/nftables 打标后选表,是 VPN、多出口、策略路由的常用组合:
sudo ip rule add fwmark 0x1 lookup 100 pref 1000
sudo nft add rule inet filter output ip daddr 10.20.0.0/16 meta mark set 0x1
ip route 的特殊类型不是「下一跳」,而是「立即失败」:
| 类型 | 行为 | 用途 |
|---|---|---|
blackhole | 静默丢弃 | 黑洞路由,防环 |
unreachable | 回 ICMP unreachable | 明确拒绝某网段 |
prohibit | 回 ICMP prohibited | 管理性禁止 |
throw | 让选路继续查下一条规则 | 策略路由的 fallback |
ip neigh 是 ARP/NDP 邻居表。ip neigh show 的 REACHABLE/STALE/DELAY/PROBE/FAILED 反映邻居可达性;FAILED 通常意味着同网段主机没回 ARP,先查链路/VLAN,而不是查路由。
network namespace 是「一台机器里的独立网络栈」:每个 namespace 有自己的接口、地址、路由、防火墙、socket 表。容器网络就是靠它实现的。
sudo ip netns add ns1
sudo ip netns add ns2
sudo ip link add veth1 type veth peer name veth2
sudo ip link set veth1 netns ns1
sudo ip link set veth2 netns ns2
sudo ip -n ns1 addr add 10.10.0.1/24 dev veth1
sudo ip -n ns2 addr add 10.10.0.2/24 dev veth2
sudo ip -n ns1 link set lo up
sudo ip -n ns2 link set lo up
sudo ip -n ns1 link set veth1 up
sudo ip -n ns2 link set veth2 up
sudo ip netns exec ns1 ping -c 2 10.10.0.2
sudo ip netns exec ns1 ip route
sudo ip netns exec ns1 ss -lntp
ip -n ns1 ... 等价于 ip netns exec ns1 ip ...,但更短。veth 是一对虚拟网卡,从一端进去的包从另一端出来;把它想成一根虚拟网线。namespace 里的 lo 默认是 down 的,不 up 会导致本机回环都不通。
查看 namespace 里的进程与 socket:
sudo ip netns pids ns1
sudo nsenter -t "$(sudo ip netns pids ns1 | head -1)" -n ss -lntp
sudo ip netns exec ns1 tcpdump -i veth1 -nn -c 4
nsenter -n 进入的是目标进程的网络 namespace,不需要知道 namespace 名字。容器排障常用 nsenter -t <pid> -n -m -p,比 docker exec 更底层;-m 进 mount namespace,-p 进 PID namespace。
删除 namespace 会连同其中的 veth 一起消失:
sudo ip netns del ns1
sudo ip netns del ns2
ip link show type veth
ip netns del 不会删除镜像文件或配置,只删运行时对象;重启后全部消失。要持久化,得写 netplan/NetworkManager 或 systemd-networkd 配置。
0.13.10 DNS 记录、ndots 与解析失败分类
dig 的输出分五段:HEADER、QUESTION、ANSWER、AUTHORITY、ADDITIONAL。排障主要看 HEADER 的 status 与 ANSWER 的条数。
status | 含义 | 下一步 |
|---|---|---|
NOERROR + 有 answer | 解析成功 | 看 TTL 与地址 |
NOERROR + 0 answer | NODATA:名字存在,但没有该类型记录 | 换记录类型(A/AAAA/MX) |
NXDOMAIN | 名字不存在 | 查拼写、search 域、权威 DNS |
SERVFAIL | 上游失败/超时/DNSSEC 验证失败 | 换 @server 对比,查 resolver 日志 |
REFUSED | 服务器拒绝递归/查询 | 换允许递归的服务器 |
FORMERR | 查询格式错 | 少见,多为 EDNS/代理问题 |
记录类型与用途:
| 类型 | 内容 | 例子 |
|---|---|---|
A | IPv4 地址 | example.com. 300 IN A 93.184.216.34 |
AAAA | IPv6 地址 | example.com. 300 IN AAAA 2606:2800:... |
CNAME | 别名 | www.example.com. CNAME example.com. |
MX | 邮件交换 | example.com. MX 10 mail.example.com. |
TXT | 文本(SPF、验证) | v=spf1 -all |
NS / SOA | 权威与区域元数据 | 委派链、负缓存 TTL |
PTR | 反向解析 | dig -x 8.8.8.8 |
SRV | 服务定位 | _https._tcp.example.com. |
CAA | 证书颁发授权 | 限制哪些 CA 能签证书 |
dig 的常用开关:
dig +short example.com A
dig +short example.com AAAA
dig +noall +answer example.com MX
dig @1.1.1.1 example.com A +tcp # 强制 TCP,排查 UDP 53 被拦
dig +trace example.com # 从根开始逐级查委派
dig -x 93.184.216.34 +short # 反向解析
dig +dnssec example.com A # 看 AD/DO 位
dig +bufsize=1232 example.com # 控制 EDNS 缓冲,避免分片
dig +search api # 显式启用 search 域
+trace 直接向根、顶级域、权威服务器逐级查询,绕过本地递归;它显示的是「委派链是否完整」,不是「本地 resolver 是否正常」。+tcp 用于确认「UDP 53 被中间设备丢弃,但 TCP 53 通」这类问题;dig 输出里出现 ;; Truncated, retrying in TCP mode 说明响应超过 UDP 缓冲,不是错误。
search 与 ndots 是「明明能解析,却解析到错误主机」的根源:
search lab.example.com example.com
options ndots:5
规则是:查询名里的点数 少于 ndots 时,先依次拼接 search 里的后缀,再查原名;点数达到 ndots 才先查原名。ndots:5 意味着 api、api.prod 这种短名会先变成 api.lab.example.com。要强制按绝对名字查,末尾加点:
getent hosts api.lab.example.com.
curl http://api./
末尾的点表示「这是 FQDN,不要再加 search 后缀」。很多应用配置里写 http://api,在开发机(ndots:1)正常,到服务器(ndots:5)就解析到别的域名,原因就在这里。
/etc/nsswitch.conf 决定「先查谁」:
hosts: files mdns4_minimal [NOTFOUND=return] dns myhostname
顺序是 files(/etc/hosts)→ mDNS → DNS → myhostname。[NOTFOUND=return] 表示 mDNS 明确返回「没有」就停止,不再查 DNS;如果 mDNS 超时,则继续。getent hosts 走这条链,dig 不走,所以两者结果可以不同。
systemd-resolved 的 stub 监听 127.0.0.53:53,/etc/resolv.conf 通常指向 /run/systemd/resolve/stub-resolv.conf。检查 stub 是否在:
ss -lunp | grep ':53'
resolvectl status
resolvectl query example.com
resolvectl statistics
resolvectl flush-caches
resolvectl query 会显示「使用的协议、服务器、是否缓存、是否 DNSSEC 验证」,比 dig 更接近应用的真实路径。resolvectl statistics 的 cache hit/cache miss 能判断「是不是缓存了旧记录」;改完 DNS 记录后 TTL 没到,flush-caches 只清本地,清不了上游递归的缓存。
DNS 失败按这个顺序分:
cat /etc/nsswitch.conf、cat /etc/hosts:本机是否被覆盖;getent hosts name:应用路径是否通;resolvectl query name:stub/resolved 是否通;dig @<配置的 DNS> name:上游是否通;dig @1.1.1.1 name:换一个递归,排除本地网络;dig +trace name:委派链是否断;dig +tcp name:UDP 53 是否被拦。
curl 还有自己的解析路径:--resolve 可以绕过 DNS 直接指定 IP,用来区分「DNS 问题」与「TLS/HTTP 问题」:
curl -v --resolve example.com:443:93.184.216.34 https://example.com/
curl -v --connect-timeout 3 --max-time 10 https://example.com/
curl -w 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} total=%{time_total}\n' -o /dev/null -s https://example.com/
time_namelookup 高是 DNS 慢,time_connect 高是 TCP 握手慢,time_appconnect 高是 TLS 慢,time_starttransfer 高是服务端处理慢。四个数把「网络慢」拆成四段。
0.13.11 TCP 状态机与 ss 的深水区
ss 的 State 列直接来自内核的 TCP 状态机。把状态和「谁在等谁」对上,才能判断问题在客户端、服务端还是中间设备。
| 状态 | 谁在等 | 正常/异常 | 排查 |
|---|---|---|---|
LISTEN | 等服务端 accept | 正常 | Recv-Q 接近 backlog 说明 accept 慢 |
SYN-SENT | 等 SYN-ACK | 客户端发起后短暂 | 大量堆积:服务端不回或中间丢包 |
SYN-RECV | 等第三次 ACK | 服务端半开 | 大量堆积:SYN flood 或客户端不回 ACK |
ESTAB | 双方可收发 | 正常 | Recv-Q/Send-Q 持续增长要查应用 |
FIN-WAIT-1 | 等对方 ACK/ FIN | 主动关闭方 | 堆积:对方不回 ACK |
FIN-WAIT-2 | 等对方 FIN | 主动关闭方 | 堆积:对方不关,应用没读干净 |
CLOSE-WAIT | 等本应用 close | 被动关闭方 | 堆积:应用泄漏 fd/忘记 close |
LAST-ACK | 等对方 ACK | 被动关闭方 | 少见 |
TIME-WAIT | 等 2MSL | 主动关闭方 | 大量是短连接正常现象 |
CLOSING | 双方同时关闭 | 少见 | 双方同时发 FIN |
CLOSE-WAIT 堆积是最有诊断价值的一种:内核已经收到对方的 FIN,等本地应用调用 close();应用不关,fd 就泄漏。ss -tan state close-wait 配合 ss -tanp 找到进程,再看它的 fd 表:
ss -tan state close-wait
ss -tanp state close-wait | awk 'NR>1 {print $NF}' | sort | uniq -c
ls -l /proc/<pid>/fd | wc -l
TIME-WAIT 不是泄漏。主动关闭方进入 TIME-WAIT 等 2MSL(Linux 约 60 秒),是为了处理迟到的重复包、保证最后一个 ACK 能重传。短连接服务(HTTP/1.0、未复用连接的客户端)出现几万个 TIME-WAIT 是正常的;真正的问题是本地端口耗尽或 NAT 表满。
ss -tan state time-wait | wc -l
cat /proc/sys/net/ipv4/ip_local_port_range
ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn | head
tcp_tw_reuse=1 只对主动发起连接的客户端有效,且要求时间戳;tcp_tw_recycle 在 Linux 4.12 已被移除,看到旧教程让你开它,直接跳过。服务端要减少 TIME-WAIT,正确做法是让客户端主动关闭、使用长连接/连接池,而不是调内核参数。
ss -i 的拥塞控制字段:
| 字段 | 含义 | 怎么读 |
|---|---|---|
rtt | 平滑 RTT / 偏差 | 与物理距离对照,突增说明排队 |
rto | 重传超时 | 应略大于 RTT;异常大说明 RTT 抖动 |
cwnd | 拥塞窗口 | 小且不涨:丢包或受限 |
ssthresh | 慢启动阈值 | 与 cwnd 一起看 |
retrans | 已重传/总重传 | 比例高说明链路丢包 |
lost | 被判定丢失的段 | 持续增长要抓包 |
reordering | 乱序容忍 | 多路径/负载均衡可能偏高 |
bytes_acked / bytes_received | 已确认/已接收字节 | 估算吞吐 |
segs_out / data_segs_out | 发送段数 | 判断小包多不多 |
ss -ti state established
ss -ti '( dport = :443 or sport = :443 )'
nstat -az TcpExtListenOverflows TcpExtListenDrops TCPTimeouts
nstat 读的是内核 SNMP 计数器,不受采样间隔影响。TcpExtListenOverflows 增长说明 accept 队列满,TcpExtListenDrops 增长说明内核直接丢了新连接;这时调 net.core.somaxconn 只是把队列加长,根因还是应用 accept 太慢。
ss 的过滤器语法不是 shell glob,而是 ss 自己的表达式:
ss -tan 'sport = :22'
ss -tan '( sport = :80 or sport = :443 )'
ss -tan dst 192.168.1.50
ss -tan 'dport >= :8000 and dport <= :8080'
ss -ltnp 'sport = :443'
ss -K 可以强制关闭匹配的连接(需要内核 CONFIG_INET_DIAG_DESTROY 与权限):
sudo ss -K dst 203.0.113.9 dport = :443
它是「不 restart 服务就踢掉一条连接」的最后手段,会直接发 RST,应用看到 ECONNRESET。不要在批量脚本里随手用。
0.13.12 nftables:表、链、hook 与 NAT
nftables 的模型是「表(table)→ 链(chain)→ 规则(rule)」。表按协议族划分,链决定挂在哪个 hook、什么优先级、默认策略是什么。
| 协议族 | 管什么 | 典型表名 |
|---|---|---|
ip | IPv4 | filter、nat |
ip6 | IPv6 | filter |
inet | IPv4 + IPv6 一起 | filter(推荐) |
arp | ARP | arp_filter |
bridge | 网桥 | bridge_filter |
netdev | 进入协议栈之前 | ingress |
链的 hook 与优先级:
| hook | 时机 | 常见 priority |
|---|---|---|
prerouting | 刚进协议栈,路由前 | dstnat(-100) |
input | 发往本机 | filter(0) |
forward | 转发 | filter(0) |
output | 本机发出 | filter(0) |
postrouting | 路由后、发出前 | srcnat(100) |
一个最小可用的 inet 防火墙:
sudo nft add table inet lab
sudo nft 'add chain inet lab input { type filter hook input priority filter; policy drop; }'
sudo nft add rule inet lab input ct state established,related accept
sudo nft add rule inet lab input iif lo accept
sudo nft add rule inet lab input ip protocol icmp accept
sudo nft add rule inet lab input tcp dport 22 accept
sudo nft list ruleset
规则顺序就是匹配顺序,命中 accept/drop 即停;没有命中才落到链的 policy。ct state established,related accept 必须放在 drop 之前,否则出站连接的回包会被丢掉。iif lo accept 放行回环;ip protocol icmp accept 放行 ping,但没放行 ICMPv6,IPv6 网络会残废,inet 表里要写 meta l4proto ipv6-icmp accept。
nft 的表达式可以组合成很紧凑的规则:
# 多个端口 + 源网段 + 限速
sudo nft add rule inet lab input ip saddr 10.0.0.0/8 tcp dport { 22, 80, 443 } ct state new accept
sudo nft add rule inet lab input tcp dport 22 limit rate 10/minute burst 5 packets accept
# 命名的集合(可增量维护)
sudo nft add set inet lab blackhole '{ type ipv4_addr; flags interval; }'
sudo nft add element inet lab blackhole '{ 203.0.113.0/24, 198.51.100.7 }'
sudo nft add rule inet lab input ip saddr @blackhole drop
# 计数与日志
sudo nft add rule inet lab input tcp dport 8080 counter log prefix "lab8080 " accept
{ 22, 80, 443 } 是匿名集合,内核会编译成区间匹配;大量地址用命名集合,增删元素不需要重写规则。limit rate 是令牌桶,burst 是桶容量;counter 让 nft list ruleset 显示命中包数/字节数,是验证规则有没有生效的最直接证据。
NAT 在 prerouting/postrouting 的 nat 链:
sudo nft add table inet nat
sudo nft 'add chain inet nat postrouting { type nat hook postrouting priority srcnat; }'
sudo nft add rule inet nat postrouting oifname "eth0" masquerade
sudo nft 'add chain inet nat prerouting { type nat hook prerouting priority dstnat; }'
sudo nft add rule inet nat prerouting iifname "eth0" tcp dport 8080 dnat to 10.0.0.5:80
masquerade 自动使用出接口地址做 SNAT,适合动态 IP;固定 IP 用 snat to 203.0.113.10。dnat 只改目的地址,回包要靠 conntrack 自动反向转换;如果 filter 链的 forward 策略是 drop,还要放行转发流量,否则 DNAT 成功但包过不去。
nft 的持久化:
sudo nft list ruleset > /etc/nftables.conf
sudo nft -c -f /etc/nftables.conf # 只检查语法,不加载
sudo nft -f /etc/nftables.conf
sudo systemctl enable --now nftables
nft list ruleset 的输出可以直接被 nft -f 读回,这是最简单的备份/恢复方式。nft -c -f 是语法检查,不检查冲突;加载失败时旧规则集保持不变,不会把你锁在外面(但 flush ruleset 会)。
ufw 与 nft 的关系:ufw 是 nftables 的前端,规则最终落到 table inet filter 与 table inet ufw-*。混用两者会让规则来源不清:
sudo ufw status numbered
sudo ufw allow from 10.0.0.0/8 to any port 5432 proto tcp
sudo ufw limit 22/tcp
sudo ufw delete 3
sudo ufw reload
ufw 的规则按编号删除,编号会随增删变化,删之前重新 status numbered。ufw default deny incoming 只影响新连接,已建立的连接不受影响;ufw disable 会清空规则,远程操作前先确认能通过其他方式登录。
0.13.13 tcpdump 与 BPF 过滤表达式
tcpdump 的价值在于「看到实际线上的包」,而不是「猜」。它的过滤表达式是 BPF 语法,编译进内核后只把匹配的包复制到用户态。
最常用的组合:
sudo tcpdump -i eth0 -nn -tttt -c 20 'tcp port 443'
sudo tcpdump -i any -nn -s0 -w /tmp/cap.pcap 'host 203.0.113.9'
sudo tcpdump -r /tmp/cap.pcap -nn -tttt | head
sudo tcpdump -i eth0 -nn -A 'tcp port 80 and host 10.0.0.5'
sudo tcpdump -i eth0 -nn -e -vv 'arp or icmp'
| 选项 | 作用 | 注意 |
|---|---|---|
-i any | 所有接口 | 用 cooked capture,部分链路层过滤不可用 |
-nn | 不解析主机名/端口名 | 排障时避免 DNS 反向查询拖慢 |
-tttt | 可读时间戳 | 与日志对齐 |
-c N | 抓 N 个包后退出 | 脚本里必须加,否则挂住 |
-s0 | 抓完整包 | 默认已不截断,但旧版默认 96 字节 |
-w / -r | 写/读 pcap | 分析用 Wireshark/tshark |
-A / -X | ASCII / hex+ASCII | 看 HTTP 明文 |
-e | 链路层头 | 看 MAC/VLAN |
| `-Q in | out` | 方向(Linux) |
-Z user | 降权 | root 抓包后切到普通用户 |
BPF 的常用原语:
tcpdump -nn 'host 10.0.0.5'
tcpdump -nn 'src 10.0.0.5 and dst port 443'
tcpdump -nn 'net 10.0.0.0/8'
tcpdump -nn 'portrange 8000-8080'
tcpdump -nn 'tcp[tcpflags] & tcp-syn != 0'
tcpdump -nn 'tcp[tcpflags] & (tcp-syn|tcp-ack) == (tcp-syn|tcp-ack)'
tcpdump -nn 'icmp[icmptype] == icmp-echo'
tcpdump -nn 'vlan 100 and tcp port 80'
tcpdump -nn 'less 128'
tcp[tcpflags] 是 TCP 头第 13 字节的位掩码;tcp-syn、tcp-ack、tcp-fin、tcp-rst、tcp-push 是符号名。抓 SYN 用 tcp[tcpflags] & tcp-syn != 0,抓 SYN-ACK 用 tcp[tcpflags] & (tcp-syn|tcp-ack) == (tcp-syn|tcp-ack)。
读握手:
10:01:02.123456 IP 10.0.0.5.54321 > 10.0.0.9.443: Flags [S], seq 1000, win 64240, options [mss 1460,sackOK,TS val ...], length 0
10:01:02.123789 IP 10.0.0.9.443 > 10.0.0.5.54321: Flags [S.], seq 9000, ack 1001, win 65160, options [mss 1460,...], length 0
10:01:02.124001 IP 10.0.0.5.54321 > 10.0.0.9.443: Flags [.], ack 1, win 502, length 0
只有 [S] 没有 [S.]:服务端没回或回包被拦;有 [S.] 但客户端没回 [.]:客户端到服务端的路径单向不通;出现 [R]:端口没监听或防火墙 reject。Flags [R.] 的 R 是 RST,[F.] 是 FIN。
抓包文件与环形缓冲:
sudo tcpdump -i eth0 -nn -s0 -C 100 -W 20 -w /var/log/net/cap.pcap
sudo tcpdump -i eth0 -nn -s0 -G 3600 -w '/var/log/net/cap-%Y%m%d-%H%M%S.pcap'
-C 100 表示每个文件 100 MB,-W 20 表示保留 20 个文件循环覆盖;-G 3600 按秒切分,文件名支持 strftime。生产机上不要不加限制地 -w,磁盘会被抓包文件写满。
tcpdump 自己也会丢包:用户态来不及消费时,内核环形缓冲溢出,退出时打印 N packets dropped by kernel。高流量场景用 -w 直接落盘、加 -B 调大缓冲,或者用 dumpcap/tshark。-i any 在高流量下更容易丢,能指定接口就指定。
0.13.14 SSH:密钥、跳板与端口转发
前十几节都在看「包到没到」;运维每天真正敲的是 SSH。它同时做四件事:主机认证、用户认证、加密信道、多路复用与转发。出问题时,这四层都会表现成同一句 Connection refused 或 Permission denied,所以先按阶段拆开,再决定改哪一层。
一次 ssh host 的顺序:
| 阶段 | 发生什么 | 失败时的典型输出 |
|---|---|---|
| TCP 握手 | 连 22/tcp | Connection refused / Connection timed out |
| 版本交换 | 双方报 SSH-2.0-... | Connection closed by remote host |
| 密钥交换 | 协商 KEX、验证主机密钥 | Host key verification failed |
| 用户认证 | publickey / password / keyboard-interactive | Permission denied (publickey) |
| 会话与通道 | pty、exec、端口转发 | PTY allocation request failed |
Connection refused 是 TCP 层:sshd 没监听、端口不对、防火墙 reject;timed out 多半是 DROP、路由或云安全组。先 nc -vz host 22 和 ss -lntp | grep ':22',不要一上来就改 sshd_config。
主机密钥解决「对面是不是那台机器」。第一次连接时客户端把主机公钥写进 ~/.ssh/known_hosts;以后指纹变了就拒绝连接:
ssh-keygen -F db.example.com # 查 known_hosts 里有没有记录
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub # 在服务器上取指纹
ssh-keygen -R db.example.com # 确认换机/重装后再删旧记录
REMOTE HOST IDENTIFICATION HAS CHANGED 只有两种可能:机器重装/换 IP,或者中间人。先带外核对指纹(控制台、配置管理、云厂商),确认是前者再 -R。把 StrictHostKeyChecking=no 写进 ~/.ssh/config 等于把这层保护关掉;首次连接可以用 accept-new,长期不要用 no。
密钥登录比密码可靠,但「把公钥贴进去」只是第一步:
ssh-keygen -t ed25519 -a 100 -C "ops@laptop"
ssh-copy-id -i ~/.ssh/id_ed25519.pub ops@db.example.com
ssh -o PreferredAuthentications=publickey -o PasswordAuthentication=no ops@db.example.com
服务端只在权限正确时认这把钥匙:~/.ssh 700、authorized_keys 600、家目录不能 group-writable(StrictModes yes)。RHEL 系还要看 SELinux 上下文,restorecon -RV ~/.ssh。sshd -T 打印合并后的有效配置,比逐行读 sshd_config 可靠:
sudo sshd -T | grep -Ei '(passwordauthentication|permitrootlogin|pubkeyauthentication|allowtcpforwarding|authorizedkeysfile)'
服务端最小加固可以放在 /etc/ssh/sshd_config.d/10-hardening.conf(Ubuntu 24.04 的 sshd_config 会 Include 这个目录):
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
AllowUsers ops deploy
MaxAuthTries 3
LoginGraceTime 20
客户端的 ~/.ssh/config 按 Host 匹配,适合把跳板、端口、key 固定下来;ssh -G host 打印最终生效的配置,排查「为什么没走这个 key」很快:
Host *
ServerAliveInterval 30
ServerAliveCountMax 3
IdentitiesOnly yes
AddKeysToAgent yes
Host bastion
HostName 203.0.113.10
User jump
IdentityFile ~/.ssh/id_ed25519
Host db
HostName 10.0.2.15
User ops
ProxyJump bastion
IdentitiesOnly yes 只让 ssh 提供配置里指定的 key,避免 agent 里十几把钥匙把 MaxAuthTries 用完,报 Too many authentication failures。ProxyJump 只做 TCP 跳转,不在跳板机上留明文;-A(agent forwarding)则把本机 agent 暴露给跳板机,跳板机被攻破就能借用你的所有 key——能用 -J 就不要用 -A。
ssh 的常用选项:
| 选项 | 作用 | 注意 |
|---|---|---|
-p 2222 | 指定端口 | 配置里写 Port |
-i key | 指定私钥 | 配合 IdentitiesOnly yes |
-J user@bastion | 经跳板连接 | 等价于 ProxyJump |
-L [bind:]lport:host:hport | 本地端口转发 | 默认只绑 127.0.0.1 |
-R [bind:]rport:host:hport | 远程端口转发 | 受服务端 GatewayPorts 限制 |
-D 1080 | 动态 SOCKS5 代理 | 配合 --socks5-hostname |
-N | 不执行远程命令 | 纯隧道 |
-f | 转后台 | 配 ExitOnForwardFailure |
-T | 不分配 pty | 跑命令/脚本 |
-v / -vvv | 调试输出 | 看 Offering 与 Authentications |
-o KEY=VALUE | 临时配置 | 脚本里最常用 |
-G host | 打印有效配置 | 不连接,只解析 |
端口转发把「能 SSH 到跳板」变成「能访问跳板背后的服务」,三种方向的语义完全不同:
| 形式 | 命令 | 监听在哪 | 典型用途 |
|---|---|---|---|
| 本地转发 | -L 5432:db.internal:5432 | 本机 | 通过跳板连内网数据库 |
| 远程转发 | -R 8080:localhost:80 | 远端 | 把本机服务临时暴露给远端 |
| 动态转发 | -D 1080 | 本机 | SOCKS5 代理,按目标动态连 |
# 本地转发:访问本机 5432 等于访问 db.internal 的 5432
ssh -N -L 127.0.0.1:5432:db.internal:5432 ops@bastion
ss -lntp | grep 5432
psql -h 127.0.0.1 -p 5432 -U app
# 远程转发:远端的 8080 转到本机 80
ssh -N -R 127.0.0.1:8080:localhost:80 ops@203.0.113.10
curl -sS http://203.0.113.10:8080/health
# 动态转发:本地 1080 提供 SOCKS5
ssh -N -D 127.0.0.1:1080 ops@bastion
curl --socks5-hostname 127.0.0.1:1080 https://internal.example.com
-L 默认只绑 127.0.0.1;要给别人用加 -g 或显式写 0.0.0.0,但那等于把内网端口开到你的网段,先确认防火墙。-R 默认也只绑服务端回环,GatewayPorts no 时请求非回环地址会失败(remote port forwarding failed for listen port)。-N 不执行命令,-f 转后台,配 ExitOnForwardFailure yes 才能在转发失败时直接退出,而不是留一个看似成功的空隧道。
长期隧道不要裸 ssh -f:用 systemd unit(Restart=always)或 autossh -M 0,并把 ServerAliveInterval 配上,否则 NAT 超时后连接僵死、ss 还显示 ESTAB。
排障按「服务端监听 → 服务端配置 → 客户端认证」三段走:
# 1. 端口与监听(在目标机上)
ss -lntp | grep ':22'
sudo systemctl status ssh # Debian/Ubuntu 是 ssh.service,RHEL 是 sshd.service
# 2. 服务端有效配置与日志
sudo sshd -T | grep -Ei 'passwordauthentication|permitrootlogin|allowusers|allowtcpforwarding'
sudo journalctl -u ssh -n 50 --no-pager
# 3. 客户端逐层看
ssh -vvv -o ConnectTimeout=5 ops@db.example.com 2>&1 \
| grep -E 'Connecting|Offering|Authentications|Server accepts|Permission'
-vvv 的重点不是整段输出,而是三行:Connecting to ...(TCP)、Offering public key(认证)、Authentications that can continue(服务端还接受什么)。BatchMode=yes 让客户端在需要交互时直接失败,适合脚本探活。
| 现象 | 常见原因 | 证据与修复 |
|---|---|---|
Permission denied (publickey) | key 未被接受:用户不对、公钥没进 authorized_keys、权限/SELinux | ssh -vvv 看 Offering;sshd -T;restorecon |
Too many authentication failures | agent 里 key 太多,超过 MaxAuthTries | IdentitiesOnly yes + -i |
Connection refused | sshd 没监听/端口错/防火墙 reject | ss -lntp、nc -vz、ufw status |
Connection timed out | DROP、路由错、云安全组 | ip route get、两端 tcpdump |
Host key verification failed | 主机密钥变了 | 带外核对指纹,确认后 ssh-keygen -R |
PTY allocation request failed | 服务端 PermitTTY no 或不需要 pty | 跑命令加 -T |
Administratively prohibited | 服务端禁了转发 | AllowTcpForwarding、PermitOpen |
channel 2: open failed | 跳板到目标不通 | 在跳板机上 nc -vz target port |
- 「改端口就安全了」——扫描器全端口扫,改端口只减少日志噪音;真正有效的是禁密码、禁 root 登录、
ufw limit 22/tcp或 fail2ban。 - 「
StrictHostKeyChecking=no只是少个提示」——它关掉的是中间人检测;首次连接用accept-new,长期不要设no。 - 「
-A和-J都是跳板」——-J只做 TCP 跳转;-A把 agent 转发到跳板,跳板上的 root 可以复用你的 key。 - 「
authorized_keys放进去就能登」——权限、属主、StrictModes、SELinux 上下文、AuthorizedKeysFile任一不对都会静默忽略;sshd -T和-vvv一起看。 - 「
-R写0.0.0.0就该监听所有地址」——服务端GatewayPorts no时会拒绝,或只绑回环。 - 「隧道 established 就代表转发可用」——目标端口可能连不上;
ExitOnForwardFailure只覆盖监听失败,端到端要用curl/nc验证。
0.13.15 三条网络巡检脚本
0.13.15.1 五层体检
#!/usr/bin/env bash
# net-diag.sh [target] —— 只读:链路/地址/路由/端口/DNS/出口
set -euo pipefail
target=${1:-1.1.1.1}
echo "== links"; ip -br link
echo "== addrs"; ip -br addr
echo "== routes"; ip route
echo "== rules"; ip rule
echo "== dns"; resolvectl status 2>/dev/null | sed -n '1,12p' || cat /etc/resolv.conf
echo "== listen"; ss -lntup
echo "== summary"; ss -s
echo "== route to $target"; ip route get "$target"
echo "== ping gateway"
gw=$(ip route | awk '/^default/ {print $3; exit}')
[[ -n ${gw:-} ]] && ping -c 2 -W 1 "$gw" || echo "no default gateway"
echo "== tcp 443"
timeout 3 bash -c "</dev/tcp/$target/443" 2>/dev/null && echo "connect ok" || echo "connect failed"
</dev/tcp/host/port 是 bash 内建的 TCP 连接,不依赖 nc;timeout 防止卡住。它只能证明「能建立 TCP」,不证明 TLS/HTTP 正常。
0.13.15.2 DNS 分层核对
#!/usr/bin/env bash
# dns-check.sh DOMAIN
set -euo pipefail
d=${1:?usage: dns-check.sh DOMAIN}
pub=${2:-1.1.1.1}
echo "== nsswitch"; grep '^hosts:' /etc/nsswitch.conf || true
echo "== resolv.conf"; cat /etc/resolv.conf
echo "== hosts file"; grep -i " $d" /etc/hosts || true
echo "== getent"; getent ahosts "$d" | head -4 || echo "getent failed"
echo "== resolved"; resolvectl query "$d" 2>&1 | head -8 || true
echo "== dig A"; dig +short "$d" A
echo "== dig AAAA"; dig +short "$d" AAAA
echo "== dig @$pub"; dig +short @"$pub" "$d" A
echo "== tcp fallback"; dig +tcp +short "$d" A
echo "== status"; dig "$d" A | awk '/status:/ {print}'
getent ahosts 会返回 STREAM/DGRAM/RAW 多行,head -4 只取前几条。dig 的 status 在 HEADER 段,awk '/status:/' 能直接抓出来;SERVFAIL 与 NXDOMAIN 的处理完全不同。
0.13.15.3 防火墙与监听面审计
#!/usr/bin/env bash
# fw-audit.sh —— 只读:ufw、nft、监听端口、Docker 映射
set -euo pipefail
echo "== ufw"; sudo ufw status verbose 2>/dev/null || echo "ufw unavailable"
echo "== nft tables"; sudo nft list tables 2>/dev/null || true
echo "== nft ruleset (head)"; sudo nft list ruleset 2>/dev/null | sed -n '1,60p' || true
echo "== listening on all addresses"
ss -lntup | awk 'NR==1 || $5 ~ /^(0\.0\.0\.0|\*|\[::\]):/'
echo "== docker published ports"
command -v docker >/dev/null && docker ps --format '{{.Names}}\t{{.Ports}}' || true
echo "== warnings"
ss -lntup | awk '$5 ~ /^0\.0\.0\.0:/ {print "EXPOSED", $1, $5, $7}' || true
ss 的第 5 列是 Local Address:Port;0.0.0.0:22 表示所有 IPv4 地址都可连,127.0.0.1:5432 只本机。docker ps --format 的 Ports 会显示 0.0.0.0:8080->80/tcp,这类映射绕过 ufw 的 INPUT 链,是「防火墙开了但端口仍暴露」的常见原因。
0.13.16 两个实验:veth 与 netns 防火墙
实验一:用 veth 连接两个 namespace
sudo ip netns add ns1
sudo ip netns add ns2
sudo ip link add veth1 type veth peer name veth2
sudo ip link set veth1 netns ns1
sudo ip link set veth2 netns ns2
sudo ip -n ns1 addr add 10.10.0.1/24 dev veth1
sudo ip -n ns2 addr add 10.10.0.2/24 dev veth2
sudo ip -n ns1 link set lo up
sudo ip -n ns2 link set lo up
sudo ip -n ns1 link set veth1 up
sudo ip -n ns2 link set veth2 up
sudo ip netns exec ns1 ping -c 2 10.10.0.2
sudo ip netns exec ns1 ip route
sudo ip netns exec ns1 ss -lntp
预期:ping 两次成功,ip route 显示 10.10.0.0/24 dev veth1 proto kernel scope link src 10.10.0.1,ss 没有监听。此时在 ns1 里 tcpdump -i veth1 -nn 抓 ping,再在 ns2 里 ping,能看到 ICMP request/reply。
清理:
sudo ip netns del ns1
sudo ip netns del ns2
ip link show type veth
ip netns del 会删除 namespace 内的所有接口;veth 的对端也会随之消失。如果顺序反了(先删一端),对端会变成 down 状态但仍在,需要手动 ip link del。
实验二:netns 里的默认拒绝防火墙
namespace 有自己独立的 nftables 规则集,适合做「不影响宿主机」的防火墙实验。先启动一个监听:
sudo ip netns add fw1
sudo ip netns add fw2
sudo ip link add fw1v type veth peer name fw2v
sudo ip link set fw1v netns fw1
sudo ip link set fw2v netns fw2
sudo ip -n fw1 addr add 10.20.0.1/24 dev fw1v
sudo ip -n fw2 addr add 10.20.0.2/24 dev fw2v
sudo ip -n fw1 link set lo up; sudo ip -n fw2 link set lo up
sudo ip -n fw1 link set fw1v up; sudo ip -n fw2 link set fw2v up
sudo ip netns exec fw1 nc -l -p 8080 &
在 fw1 里建默认拒绝的链:
sudo ip netns exec fw1 nft add table inet lab
sudo ip netns exec fw1 nft 'add chain inet lab input { type filter hook input priority filter; policy drop; }'
sudo ip netns exec fw1 nft add rule inet lab input ct state established,related accept
sudo ip netns exec fw1 nft add rule inet lab input iif lo accept
sudo ip netns exec fw1 nft add rule inet lab input ip protocol icmp accept
sudo ip netns exec fw1 nft add rule inet lab input tcp dport 8080 accept
sudo ip netns exec fw1 nft list ruleset
验证:
# 放行前先测:应该超时(不是 refused)
sudo ip netns exec fw2 timeout 2 bash -c '</dev/tcp/10.20.0.1/8080' || echo "blocked as expected"
# 加放行规则后再测
sudo ip netns exec fw2 bash -c '</dev/tcp/10.20.0.1/8080' && echo "allowed"
注意 nc -l -p 8080 的语法随实现不同(OpenBSD netcat 用 nc -l 8080),实验里先 nc -h 确认。policy drop 会让未匹配的包被静默丢弃,客户端看到超时;如果看到 Connection refused,说明包到了但端口没监听,不是防火墙拦的。
清理:
sudo ip netns del fw1
sudo ip netns del fw2
0.13.17 十四个网络陷阱
| # | 现象 | 原因 | 修复/判断 |
|---|---|---|---|
| 1 | ip -br link 有 UP 无 LOWER_UP | 物理链路/VLAN 没通 | ethtool、dmesg、交换机端口 |
| 2 | 多网卡从错误源 IP 出去 | 路由 src 或 metric 不对 | ip route get ... from ... |
| 3 | 改 /etc/resolv.conf 重启失效 | 被 resolved/NetworkManager 覆盖 | 改 netplan 或 resolvectl dns |
| 4 | 短名解析到错误域名 | search + ndots | 用 FQDN 末尾加点 |
| 5 | dig 通、getent 不通 | NSS//etc/hosts/mDNS | 查 nsswitch.conf |
| 6 | NXDOMAIN 与 SERVFAIL 混为一谈 | 一个是名字不存在,一个是上游失败 | 看 dig 的 status |
| 7 | LISTEN 的 Recv-Q 接近 Send-Q | accept 队列满 | 查应用 accept 循环、ListenOverflows |
| 8 | 大量 TIME-WAIT 就调内核参数 | 短连接正常现象 | 先算端口是否耗尽,再谈复用 |
| 9 | ping 不通但 TCP 可用 | ICMP 被防火墙/云厂商拦 | 用 nc/curl 验证 TCP |
| 10 | 小包通、大包卡 | MTU 黑洞/PMTUD 失败 | ping -M do -s 1472、tracepath |
| 11 | tcpdump 看不到包 | 抓错接口、-i any 的 cooked 头、过滤表达式错 | 先 -i any -nn 不加过滤 |
| 12 | tcpdump 报 checksum 错 | 网卡 offload,抓包点在计算前 | 忽略或关 offload,不代表真错 |
| 13 | ufw 放行但 Docker 端口仍暴露 | Docker 写自己的 nat/filter 链 | 绑 127.0.0.1、DOCKER-USER、ufw-docker |
| 14 | 加 policy drop 后 SSH 断 | 没放行 established/22 | 先加规则再切 policy,保留带外控制台 |
第 13 条展开:docker run -p 8080:80 会在 nat 表加 DNAT,在 filter 表加 DOCKER 链,流量在进入 ufw 的 INPUT 链之前就被 DNAT 到容器,ufw deny 8080 拦不住。三种修法:发布时绑 127.0.0.1:8080:80;在 DOCKER-USER 链里加限制;用 ufw-docker 生成规则。云上还要再叠一层安全组。
0.13.18 本章出口检查
| 检查项 | 达标线 | 没达标去哪 |
|---|---|---|
读出 ip -br link/addr 的 UP 与 LOWER_UP | 能区分管理状态与链路状态 | 0.13.1 |
用 ip route get 确认出口 | 能说出下一跳和源地址 | 0.13.1 |
用 netplan try 安全改地址 | 知道超时回滚 | 0.13.2 |
| 解释 LISTEN 与 ESTAB 的 Recv-Q | 能说出 backlog 与字节数 | 0.13.3 |
用 getent 与 dig 区分 NSS/DNS | 能解释两者为何不一致 | 0.13.4 |
用 nc/curl -w 分段定位 | 能说出 refused 与 timeout 的差别 | 0.13.5 |
| 用 tcpdump 看 SYN/SYN-ACK/RST | 能读出握手卡在哪 | 0.13.6 |
| 说出 ufw 与 Docker/安全组的边界 | 能举端口绕过的例子 | 0.13.7 |
| 用密钥登录并做一次端口转发 | 能说清 -L/-R 监听在哪、GatewayPorts 限制 | 0.13.14 |
Q1 ip -br link 显示 enp0s31f6 UP,但没有 LOWER_UP,ip addr 也没有地址。下一步查什么?为什么不是先查 DNS?
Q2 ss -lntp 显示 127.0.0.1:8080 有监听,但同网段另一台机器 curl http://192.168.1.20:8080 连接被拒。最可能的原因是什么?
Q3 dig @8.8.8.8 example.com 正常,getent hosts example.com 失败,curl https://example.com 报 Could not resolve host。问题在哪一层?
Q4 ufw status 是 inactive,云主机安全组放行了 443,但外部访问 443 超时,ss -lntp 没有 443 监听。按故障树,下一步做什么?
答案
A1 没有 LOWER_UP 说明物理链路没通:查网线、交换机端口、VLAN、ethtool enp0s31f6 的 Link detected、dmesg 里的 link up/down。没有地址是链路层之上的问题,链路没通时查 DNS 没有意义,因为包根本发不出去。
A2 服务只绑在回环 127.0.0.1,没有绑 0.0.0.0 或 192.168.1.20。ss 的 Local Address 是证据;改应用监听地址,或用 socat/反向代理转发。另一个可能是本机防火墙只允许 lo,但 refused 更像没监听在该地址。
A3 NSS 层。dig 直接查 DNS,绕过了 /etc/nsswitch.conf;getent 走 NSS,可能被 /etc/hosts、mDNS 或 resolvectl 的 stub 影响。查 nsswitch.conf 的 hosts: 行、/etc/hosts、resolvectl query example.com,再看 systemd-resolved 是否把查询转给了错误的链路。
A4 端口层:先确认应用是否该监听 443(systemctl status、ss -lntp),再看应用日志为什么没起来;如果应用只监听 80,需要改配置或加反向代理。防火墙 inactive 不代表云安全组和上游负载均衡放行,也不代表应用在跑;按「应用进程→监听→主机防火墙→安全组→网络路径」逐层查。
母本对应
| 母本 | 章节 | PDF 页 | 本书落点 | 程度 |
|---|---|---|---|---|
| 鸟哥 | 第 17 章 认识系统服务(网络服务部分) | p.841–885 | 0.13.1–0.13.3 | 背景(服务与端口) |
| Shotts | 第 16 章 联网 | p.181–192 | 0.13.1–0.13.5、0.13.14 | 完整(ip、ping、curl、SSH) |
| OpenSSH | ssh_config(5)、sshd_config(5) | man | 0.13.14 | 精选(密钥、跳板、转发、加固) |
| 谢希仁 | 第 1–6 章 协议分层与 IP | — | 0.13.1、0.13.8 | 背景(分层模型) |
iproute2 文档 | ip(8)、ss(8) | man | 0.13.1–0.13.3 | 精选(对象与字段) |
systemd-resolved 文档 | resolvectl(1)、resolved.conf(5) | man | 0.13.4 | 精选(stub、缓存、DNSSEC) |
| nftables / ufw 文档 | nft(8)、ufw(8) | man | 0.13.7、0.13.12 | 精选(主机防火墙) |
下一站:0.14 性能工具与命令行工程化把「网络通不通」扩展到「系统慢不慢」:CPU、内存、磁盘、网络四类症状分别先看什么,以及怎么把诊断命令固化成可复用的脚本。