楼层: 首页/ 软件技术/ Linux/ 网络配置与抓包:把包看穿
14

网络配置与抓包:把包看穿

netfilter · iptables · nftables · tcpdump · ss

第 8 章教你把网络接通,这一章教你把网络看清。"连不上""时快时慢""莫名被拒"这三类悬案,靠猜是猜不出来的,只能靠三条命令给出事实:ss 看连接状态、tcpdump 看包到底有没有到、nftables 看包该不该放行。先把"事实"拿到手,再谈结论。

netfilter 五个钩子:包在 Linux 里到底走的哪条路

是什么:netfilter 是内核里的一套包处理框架,它是真正干活的;iptables、nftables、ufw、firewalld 全都只是"给 netfilter 下命令的前端"。内核在包的处理路径上埋了五个钩子(hook),包经过哪个钩子,决定了你能不能用某条规则拦住它。

为什么:新手最常见的困惑就是"我明明写了放行规则,为什么还是不通"——十有八九是规则写在了错误的链上。比如宿主机上的容器流量走的是 FORWARD 而不是 INPUT,你在 INPUT 上写再多也没用。

路一个包进出的完整路径

① 从网卡进来 → PREROUTING(还没决定去哪,DNAT 目标地址转换在这里做)→ 内核判断"目的地址是不是本机":

  ├─ 是本机 → INPUT → 交给本机进程;本机进程回包时走 OUTPUT → POSTROUTING(SNAT 源地址转换在这)→ 出网卡。

  └─ 不是本机(要转发) → FORWARD → POSTROUTING → 出网卡。容器、NAT 网关、路由器上的流量走的都是这条路。

记法:本机自己的流量走 INPUT/OUTPUT,帮别人转的流量走 FORWARD。只放行 INPUT,等于只允许别人访问你,没允许你帮别人转发。

五个钩子:想拦什么,就写在哪个钩子上
钩子处理的是哪些包典型用途
PREROUTING刚从网卡进来、还没判断去向的包。DNAT:把 外网IP:8080 转到 内网IP:80。
INPUT目标是本机进程的包。放行 SSH / HTTP,拦扫描(最常见的一条链)。
FORWARD只是路过、要转给别人的包。容器、K8s、NAT 网关、软路由。
OUTPUT本机进程自己发出去的包。限制本机访问外网(出站管控)。
POSTROUTING马上要离开网卡的包(无论来自本机还是转发)。SNAT / MASQUERADE:让内网机器共用出口 IP。

"连不上"的排队排查顺序:从上往下,哪一步断,问题就在哪一层

# ① 服务真的在监听吗?监听在哪个地址上?(127.0.0.1 和 0.0.0.0 差别巨大) sudo ss -tlnp | grep :8080 # ② 本机自己能连吗?—— 排除"服务本身起不来" curl -v --connect-timeout 3 http://127.0.0.1:8080/ # ③ 用本机 IP 能连吗?—— 排除"只绑了 127.0.0.1" curl -v --connect-timeout 3 http://192.168.1.10:8080/ # ④ 从别的机器能连吗?—— 排除防火墙 / 安全组 nc -vz 192.168.1.10 8080 # ⑤ 包到底到没到网卡?—— 抓包看事实(换一个终端跑) sudo tcpdump -i any -nn -c 10 port 8080
坑:127.0.0.1 和 0.0.0.0 差着一个互联网

服务监听在 127.0.0.1:8080 时,只有本机能连,外面怎么调防火墙都没用。这是"本机能 curl 通、外部连不上"的头号原因。判断方法就一条:ss -tlnp 的 Local Address 列写的是 127.0.0.1:8080 还是 0.0.0.0:8080。先改监听地址,再怀疑防火墙。

iptables:四张表、五条链,老牌但还没死

是什么:iptables 用"表(table)+ 链(chain)+ 规则(rule)"描述防火墙策略。表决定"干什么事",链决定"在哪个钩子干"。四张表按优先级从高到低是:raw → mangle → nat → filter,不写 -t 时默认操作 filter 表(我们 90% 的时间只用它)。

为什么还要学它:虽然新发行版底层已经换成 nftables,但 Docker、大量运维脚本、无数网上的教程还在用 iptables 语法;而且 RHEL 9 / Ubuntu 24.04 上你敲的 iptables 命令其实是 iptables-nft 兼容层,它在背后翻译成 nftables 规则。看得懂 iptables,才看得懂 Docker 往你机器里塞了什么。

怎么用:先看,再改;改完立刻保存。

# ── 看 ── sudo iptables -L -n -v --line-numbers # -n 不做域名反查(不加会卡好几秒),--line-numbers 便于删除 sudo iptables -S # 以"可回放的命令"形式打印,比 -L 好读得多,排查首选 sudo iptables -t nat -L -n # 看 NAT 表:Docker 的端口映射就在这 sudo iptables -t nat -L DOCKER -n # 看 Docker 到底做了哪些转发 # ── 改:先放行必要流量,最后再改默认策略 ── sudo iptables -A INPUT -i lo -j ACCEPT # 回环必须放行 sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT # 已建立的连接直接放行 sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT sudo iptables -A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT sudo iptables -P INPUT DROP # 默认策略:没匹配上的全丢(放最后一步做) # ── 删:按规则序号删,或按整条规则删 ── sudo iptables -L INPUT -n --line-numbers # 先看序号 sudo iptables -D INPUT 3 # 删第 3 条 sudo iptables -D INPUT -p tcp --dport 22 -j ACCEPT # 或者按规则原文删(只删第一条匹配的) # ── 存:不保存的话,重启就全没了 ── sudo apt install -y iptables-persistent && sudo netfilter-persistent save # Debian / Ubuntu sudo dnf install -y iptables-services && sudo service iptables save # RHEL 系
四个动作的区别:规则顺序就是匹配顺序,这一点决定了 90% 的"规则不生效"
动作含义与典型场景
-AAppend,追加到链的末尾。前面如果有 DROP 规则,你这条永远轮不到——这是"我放行了却还是不通"的第一嫌疑人。
-IInsert,插到最前面(或 -I INPUT 2 插到第 2 条)。救火时补放行 SSH,一律用它。
-DDelete,按序号或规则原文删除。
-PPolicy,设置链的默认策略。只对链自身生效,对 FORWARD 等其他链无效,别以为改了 INPUT 就全关了。
坑:远程改防火墙把自己关在门外

远程服务器上执行 iptables -P INPUT DROP 之前,必须先确认 22 端口已经在放行规则里、且位置在前面。改完不要关掉当前终端,新开一个窗口验证还能连上,再结束工作。真锁死了,只能去云厂商控制台的 VNC / 救援终端救砖。同一台机器上执行 iptables -F(清空规则)更是高危动作——它同时会清掉放行 SSH 的规则,而默认策略若是 DROP,你就立刻失联。

Docker 共存注意:Docker 会自己往 iptables 里插规则,别跟它抢

# Docker 发布端口时,靠的是 nat 表的 DOCKER 链做 DNAT # 你在 filter 表的 INPUT 里加规则,拦不住已经 DNAT 到容器里的流量 # 官方给的"自定义拦截位"是 DOCKER-USER 链,规则要写在这里: sudo iptables -I DOCKER-USER -i eth0 -s 10.0.0.0/8 -j DROP # 重启 Docker 会重建 DOCKER 链,但 DOCKER-USER 链里的规则会被保留

别别和 Docker 抢方向盘

不建议手工去改 Docker 建的 DOCKER / DOCKER-ISOLATION 链——它们在 dockerd 重启或容器增删时会被重建,你的规则会凭空消失。想限制容器端口对外的访问,用 DOCKER-USER;想彻底不让 Docker 动 iptables,用 dockerd --iptables=false(但那样端口映射也就失效了)。

nftables:新发行版的默认后端,也是 iptables 的接班人

是什么:nftables 是内核 3.13 引入、用来取代 xtables(iptables/ip6tables/arptables/ebtables)的新一代框架。它不再有四张固定表的包袱,而是由你自己定义表和链,一条规则里可以同时写 IPv4、IPv6、端口集合、接口条件。

为什么值得学:三个实打实的好处——① 一套规则管 IPv4 + IPv6(iptables 时代你得写两遍,然后其中一份一定会忘);② 原子替换,用 nft -f ruleset.nft 整体加载,不会出现"改到一半规则乱七八糟"的窗口,也不会有 iptables 那种几十条规则逐条生效的慢;③ 集合匹配是原生的,{ 22, 80, 443 } 直接写,不用 multiport 扩展模块。

iptables 与 nftables 对照:写法和行为都不一样
维度iptablesnftables
表/链filter / nat / mangle / raw 四张固定表,链名固定(INPUT/OUTPUT/FORWARD)。自定义表(按 inet/ip/ipv6/bridge/netdev 家族),链要自己写 type hook priority。
协议族iptables / ip6tables / arptables 各写一套。inet 家族同时覆盖 IPv4 与 IPv6,写一遍即可。
生效方式逐条命令、逐条生效,大规则集加载慢。整份 ruleset 一次性原子替换,不存在中间状态。
默认后端Debian 10 之前的默认;现在敲 iptables 多被翻译成 nft 规则。RHEL 9 / Debian 12 / Ubuntu 22.04+ 的默认后端。
持久化rules.v4 + netfilter-persistent,或 iptables-services。写进 /etc/nftables.conf,由 nftables.service 开机加载。
适合谁存量脚本、老文档、Docker 生成的规则。新机器、自己写规则、需要 v4/v6 统一管理。

从零配一份 nftables 规则:注意顺序——先放行,最后才 drop

sudo nft list ruleset # 看一眼当前全部规则(这就是 /etc/nftables.conf 的内容) # 1) 建表和链。先 policy accept,别一上来就 drop,否则远程会话当场断掉 sudo nft add table inet filter sudo nft add chain inet filter input '{ type filter hook input priority 0; policy accept; }' # 2) 放行必要流量 sudo nft add rule inet filter input iif lo accept # 回环 sudo nft add rule inet filter input ct state established,related accept # 已建立的连接 sudo nft add rule inet filter input tcp dport { 22, 80, 443 } accept # 集合匹配,原生支持 sudo nft add rule inet filter input icmp type echo-request accept # 允许 ping # 3) 确认上面都对了,最后再把默认策略改成 drop sudo nft add chain inet filter input '{ policy drop; }' sudo nft list chain inet filter input # 再确认一遍 22 在里面,然后才退出会话 # 4) 持久化:nftables 不会自动保存,必须写文件 + 开服务 sudo nft list ruleset | sudo tee /etc/nftables.conf sudo systemctl enable --now nftables sudo nft -f /etc/nftables.conf # 手动加载一份规则文件(原子生效)
坑:nftables 规则默认不持久化

很多人的"配好了防火墙,重启后全没了"就是这个原因:nftables 只在内存里生效,必须把规则写进 /etc/nftables.conf 并 systemctl enable nftables。另外,RHEL 9 上老教程教的 /etc/sysconfig/iptables 已经不再被读取,照抄会白忙活一场。

坑:DROP 和 REJECT 的差别,决定了你排查时要熬多久

-j DROP 是静默丢弃:客户端只能一直重试到超时(默认要等十几秒甚至几分钟),日志里什么都没写。-j REJECT 会立刻回一个 ICMP 不可达或 TCP RST,客户端秒失败。所以排错阶段用 REJECT 能立刻定位,稳定后再考虑改成 DROP(对外网扫描更"隐形")。如果抓包发现只有 SYN 没有 SYN-ACK,也没有 RST,那基本就是被 DROP 了。

ss 与 netstat:谁在监听、谁在连接

是什么:ss(socket statistics)直接读内核里的 socket 表,比老的 netstat 快得多。netstat 属于 net-tools 包,早已停止维护,Ubuntu 18.04+ / RHEL 8+ 默认都不装了——所以别再把 netstat 写进新脚本。

为什么它是排障第一站:它一次性回答三个最关键的问题:端口在不在听、听在哪个地址、被哪个进程占着。这三个答案拿到,一半的问题就已经定性了。

ss 常用姿势(加 sudo 才能看到别人的进程名)
命令干什么
ss -tulnp最常用:t=TCP、u=UDP、l=只列监听、n=不解析域名、p=带进程。看"服务到底起没起"。
ss -s连接数总览:各状态各有多少、占了多少内存。判断"连接数是不是打满了"。
ss -tan state established只看已建立的连接。后面可以带条件:( sport = :443 or dport = :443 )。
ss -tnp dst 10.0.0.5看"我这台机器哪些进程在连 10.0.0.5"。
ss -tlnp 'sport = :8080'8080 到底被谁占了(比 lsof -i:8080 更快)。
ss -ti进阶神器:打印 TCP 内部状态 rtt、cwnd、retrans、rto,排查"连接慢、吞吐上不去"。
ss -K按条件强杀连接(ss -K dst 1.2.3.4),比重启服务轻。

实战:一次典型输出怎么读

$ sudo ss -tlnp State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 0 511 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=812,fd=3)) LISTEN 0 511 127.0.0.1:6379 0.0.0.0:* users:(("redis-server",pid=1503,fd=6)) LISTEN 511 511 0.0.0.0:8080 0.0.0.0:* users:(("java",pid=2044,fd=58)) # 逐列翻译(就用这张表判断问题): # Local Address: 0.0.0.0:22 → 全网卡监听,外部可连 # Local Address: 127.0.0.1:6379 → 只本机能连,外部连不上(Redis 默认如此,是安全的好设计) # Recv-Q 511 / Send-Q 511 在 LISTEN 状态下不是队列长度,而是 backlog 上限 # Recv-Q 在 ESTAB 状态下长期不为 0 → 应用读得太慢,连接堆积,通常是应用层卡住了
netstat 老命令的现代替代(老文档里全是左边这一列)
netstat(已退役)现在用
netstat -tulnpss -tulnp
netstat -an | grep ESTABLISHEDss -tan state established
netstat -sss -s(或 nstat -az 看更细的计数)
netstat -rnip route
netstat -iip -s link
坑:TIME-WAIT 一大堆,是不是要爆了?

不一定。TIME-WAIT 是主动关闭连接的一方必须经历的正常状态(默认持续 60 秒),它的作用是防止最后一个 ACK 丢了导致对端重传时收到错误的旧连接数据。判断标准是端口够不够而不是"数量吓不吓人":本机连出去的可用端口由 net.ipv4.ip_local_port_range 决定(通常 3 万多个),短连接服务确实可能被耗尽。此时该做的是连接池化/长连接,而不是去搜"怎么最快干掉 TIME-WAIT"。

tcpdump:用事实说话,别猜

是什么:tcpdump 是命令行抓包工具,基于 libpcap,能在网卡层面把流经的包原样记下来。它是网络排障里唯一的"现场录像"。

为什么必须会:因为"服务端没收到请求"和"客户端没发出请求"这两种情况的日志长得一模一样——都是两边都没报错。只有抓包能一刀切开:包到网卡了吗?回包了吗?回的什么?

# ── 三句最常用的开场 ── sudo tcpdump -i any -nn -c 20 port 80 # 抓 20 个包(-nn:不做域名和端口反查,否则会卡到你以为服务器挂了) sudo tcpdump -i eth0 -nn 'host 10.0.0.5 and tcp port 3306' # 只看某台机器 + 某个端口 sudo tcpdump -i any -nn -A 'tcp port 80' # -A 以 ASCII 打印,明文 HTTP 请求头直接可读 # ── 存下来慢慢看(推荐:拉回本地用 Wireshark) ── sudo tcpdump -i any -nn -s 0 -w /tmp/web.pcap 'port 443' # -s 0 抓完整包,不截断 sudo tcpdump -r /tmp/web.pcap -nn # 读回来 -- 离线分析 sudo tcpdump -i any -nn -C 100 -W 5 -w /tmp/log.pcap port 80 # 每个文件 100MB,最多留 5 个,自动轮转 # ── 只看某类控制包 ── sudo tcpdump -i any -nn 'tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn' # 只看 SYN:判断握手到底开始没 sudo tcpdump -i any -nn 'tcp[tcpflags] & tcp-rst != 0' # 只看 RST:谁在拒绝连接 sudo tcpdump -i eth0 -nn -e 'arp' # -e 打印链路层头,看 MAC / VLAN 标签

判三个"一句话定性"的抓包套路

① 只有 SYN,没有 SYN-ACK,也没有 RST。请求发出去了,对方(或中间设备)静默丢弃了。基本可以断定是 DROP 类防火墙,或者路由不通。此时服务端日志当然什么都没有——因为包根本没到应用。

② 有 SYN,回的是 RST。网络是通的,但没有进程在监听那个端口(内核代回 RST),或者被 REJECT 规则拦下。这时候该回去查 ss -tlnp 和进程日志。

③ 握手顺利、请求也发了,但对端长时间不回应。包到了、服务在跑,那就是应用层慢(数据库卡、锁等待、下游超时)。抓包到此结束,接下来该上 strace 或应用自己的监控了。

坑:容器里抓包抓不到东西

容器有自己的网络命名空间,在宿主机上 tcpdump -i any 默认看不到容器内部 veth 之外的流量(同一个 veth 上的包可能只在容器侧可见)。正确姿势是进容器的网络命名空间里抓:先用 docker inspect -f '{{.State.Pid}}' 容器名 拿到主进程 PID,再执行

sudo nsenter -t <PID> -n tcpdump -i any -nn port 8080 —— 这也是排查"容器里服务连不上"的万能入口。如果机器上根本没有 tcpdump,宿主机装一次然后 nsenter 进去用即可(二进制是共享的,只有网络视图被隔离)。

坑:在生产机上裸跑 tcpdump

三条铁律:① 一定加过滤条件(port 443、host 10.0.0.5),裸抓 -i any 在高流量机器上会瞬间吃满 CPU 并产生 GB 级文件;② 一定加 -c 或 -C/-W 轮转,-w 写出去的文件能把 /var 撑爆,进而拖死整台机器;③ 别在生产上长时间开着,抓完立刻 Ctrl-C。抓包本身也是"观测者效应"的典型——它会改变你正在观测的系统。

四个真实故障现场:从现象到命令

按现象查表,第一刀就该切在最可能的地方
现象第一条命令常见结论
本机 curl 通,外部连不上ss -tlnp | grep 端口监听在 127.0.0.1 而非 0.0.0.0;其次是本机防火墙没放行;第三是云厂商的安全组还没开(这一层在机器外面,本机改什么都没用)。
端口能连上但立刻断开sudo tcpdump -nn 'tcp[tcpflags] & tcp-rst != 0'服务端进程拒绝后主动关闭;或 conntrack 表满(dmesg | grep -i conntrack 出现 "table full");或连接数到了 ulimit -n 上限。
能连但很慢ss -ti 看 retrans 和 rttretrans 持续增长说明丢包(看 mtr 定位是哪一跳);rtt 突然翻倍多为跨网/拥塞;两者都正常那就是应用层慢,别赖网络。
规则明明放行了还是不通sudo iptables -S / sudo nft list ruleset规则顺序(DROP 在前面先匹配了);写错表(该写 -t nat);容器流量走 FORWARD;SELinux 或安全组挡着。
记
本章小结

① 先理解 netfilter 五个钩子:DNAT 在 PREROUTING、本机流量走 INPUT/OUTPUT、帮别人转发走 FORWARD、SNAT 在 POSTROUTING。规则写错链,等于没写。

② iptables 看规则用 -S、改规则用 -I(插最前)、远程操作前先放行 22;新机器优先学 nftables,记得写 /etc/nftables.conf 并 enable 服务。

③ ss -tulnp 是第一站、ss -ti 是进阶、tcpdump 是终审法官:有 SYN 无回应=DROP,有 RST=没人听,握手完但慢=应用层问题。