部署一个 Java 服务后,我们通常会用下面的命令确认端口是否监听:
bash
ss -lntp | grep ':8080 '
如果看到:
text
LISTEN 0 128 0.0.0.0:8080
说明应用已经创建了监听 Socket。
但这并不意味着外部客户端一定能访问它。实际工作中经常出现这样的情况:
- Java 进程正在运行;
- 8080端口已经监听;
- 云平台安全组已经放行;
- 服务器本机访问正常;
- 远程客户端访问却一直超时;
- Java应用日志中没有任何请求记录。
这是因为,外部数据不会直接进入Java程序。它需要先经过服务器网卡、Linux内核网络协议栈、路由判断和Netfilter规则,最后才有机会进入Socket。
本文从应用程序最熟悉的Socket开始,逐层向下解释Linux网络处理过程,再沿着数据包的实际路径,说明iptables究竟在什么位置发挥作用。
文中的路径、地址和端口均为示例。
一、先建立整体认识:应用程序通过Socket使用网络
Java程序不会直接操作网卡,也不会自己实现TCP协议。
例如,一个简单的TCP服务端:
java
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept();
System.out.println(
"客户端已连接:" + socket.getRemoteSocketAddress()
);
}
这段代码主要完成两件事:
- 请求操作系统创建一个监听8080端口的Socket;
- 调用
accept(),等待操作系统交付已经建立的TCP连接。
TCP三次握手、报文确认、丢包重传和拥塞控制等工作,全部由Linux内核完成。
应用程序与网络之间的关系可以表示为:
text
用户空间
┌──────────────────────────────┐
│ Java应用程序 │
│ accept、read、write │
└──────────────┬───────────────┘
│ 系统调用
▼
内核空间
┌──────────────────────────────┐
│ Socket层 │
├──────────────────────────────┤
│ TCP、UDP传输层 │
├──────────────────────────────┤
│ IP、路由、Netfilter │
├──────────────────────────────┤
│ 链路层、网卡驱动 │
└──────────────┬───────────────┘
│
▼
物理或虚拟网络
接收数据时,数据大致从下向上流动:
text
网卡
↓
链路层
↓
IP、路由和Netfilter
↓
TCP或UDP
↓
Socket
↓
应用程序
发送数据时,方向相反:
text
应用程序
↓
Socket
↓
TCP或UDP
↓
IP、路由和Netfilter
↓
网卡
这里需要特别强调:
TCP和UDP本身就是Linux内核网络协议栈的一部分。
不能把"Linux内核网络协议栈"和"TCP/UDP处理"理解成两个独立系统。Linux内核网络协议栈是一个整体,TCP/UDP只是其中的传输层部分。
二、iptables并不直接处理数据包
日常交流中,我们经常说"iptables把数据包拦截了"。从使用角度看没有问题,但从实现上说,真正处理数据包的是Linux内核中的 Netfilter。
两者的分工是:
| 组件 | 所在空间 | 职责 |
|---|---|---|
| iptables | 用户空间 | 添加、删除、查看防火墙和NAT规则 |
| Netfilter | 内核空间 | 在数据路径上匹配并执行规则 |
当我们执行:
bash
iptables -A INPUT \
-p tcp --dport 8080 \
-j ACCEPT
大致发生以下过程:
- iptables解析命令;
- iptables把规则提交给内核;
- 内核保存这条规则;
- iptables命令执行完成并退出;
- 后续数据包经过Netfilter时,由内核匹配这条规则。
因此,iptables不是一个必须持续运行的防火墙进程。
更准确的说法应该是:
管理员通过iptables配置规则,Linux内核中的Netfilter负责执行规则。
一些较新的系统可能使用nftables作为底层实现,iptables命令则工作在兼容模式下。本文主要讨论经典的iptables规则模型和Netfilter数据路径。
三、Netfilter在网络协议栈中设置了五个关键位置
Netfilter在数据包经过内核网络路径时,设置了多个钩子点。iptables中最常见的五条链与这些处理位置相对应:
text
PREROUTING
INPUT
FORWARD
OUTPUT
POSTROUTING
它们分别回答五个问题:
| 链 | 处理时机 | 主要解决的问题 |
|---|---|---|
PREROUTING |
数据进入服务器后、路由判断前 | 是否需要修改目的地址 |
INPUT |
确认目标是本机后 | 是否允许交给本机协议栈和Socket |
FORWARD |
确认需要经过本机转发后 | 是否允许转发到其他主机或容器 |
OUTPUT |
本机产生数据后 | 是否允许本机程序向外发送 |
POSTROUTING |
路由完成、数据离开前 | 是否需要修改源地址 |
不是每个数据包都会依次经过全部五条链。不同方向的数据有不同路径。
最重要的三条路径是:
text
外部访问宿主机:
PREROUTING → INPUT → TCP/UDP → Socket
外部流量经过本机转发:
PREROUTING → FORWARD → POSTROUTING
本机应用访问外部:
Socket → TCP/UDP → OUTPUT → POSTROUTING
下面沿着这三条路径逐层展开。
四、第一条路径:外部客户端访问宿主机应用
假设Java应用监听:
text
0.0.0.0:8080
客户端访问:
text
服务器IP:8080
数据包在服务器中的典型路径是:
text
网卡接收数据
↓
链路层处理
↓
IP层处理
↓
PREROUTING
↓
路由判断
↓
目标地址属于本机
↓
INPUT
↓
TCP或UDP处理
↓
查找Socket
↓
进入Socket接收队列
↓
应用程序读取
1. 数据先到达服务器网卡
客户端发送的是网络数据。以TCP为例,一个数据帧通常包含:
text
以太网头
IP头
TCP头
应用数据
服务器网卡接收到数据帧后,通过DMA等机制将数据送入内存。网卡驱动和Linux内核开始处理这份数据。
在Linux内核中,网络数据通常由一种称为sk_buff,简称skb的数据结构描述。
此时,Java程序还没有看到这个请求。
2. 内核进入IP接收路径
内核完成必要的链路层处理后,会识别数据帧承载的是IPv4、IPv6还是其他协议。
对于IPv4数据包,内核会检查:
- IP头是否合法;
- 目的IP是什么;
- 是否需要转发;
- 上层协议是什么;
- 数据包是否可以继续处理。
IP头中的协议字段可以说明上层协议。例如:
| 协议 | IP协议号 |
|---|---|
| ICMP | 1 |
| TCP | 6 |
| UDP | 17 |
虽然内核已经知道数据包承载的是TCP还是UDP,但此时还不会立即把它交给TCP或UDP模块,因为还要先经过Netfilter和路由判断。
3. 数据进入PREROUTING
PREROUTING的含义是:
数据包已经进入服务器,但内核还没有完成最终路由判断。
这里经常执行DNAT,也就是修改目的地址。
例如:
bash
iptables -t nat -A PREROUTING \
-p tcp --dport 18080 \
-j DNAT --to-destination 172.20.0.10:8080
数据包原来访问:
text
宿主机IP:18080
经过规则处理后,目的地址变成:
text
172.20.0.10:8080
因为路由判断发生在后面,所以目的地址发生变化后,数据包接下来可能不再进入INPUT,而是进入FORWARD。
4. 内核进行路由判断
Linux根据数据包当前的目的地址查询路由信息,并决定:
text
目的地址属于本机
↓
INPUT
目的地址不属于本机,需要转发
↓
FORWARD
因此,INPUT和FORWARD的区别并不是由端口决定的,而是由路由判断结果决定的。
访问宿主机上直接运行的Java服务,通常进入INPUT。
访问经过DNAT映射的Docker容器,通常进入FORWARD。
5. 本机流量经过INPUT
假设目的地址属于本机,数据包进入INPUT。
放行8080端口的规则可能是:
bash
iptables -A INPUT \
-p tcp --dport 8080 \
-j ACCEPT
如果规则是:
bash
iptables -A INPUT \
-p tcp --dport 8080 \
-j DROP
数据包会被静默丢弃。
这时:
- 数据已经到达服务器;
- 网卡已经接收到数据;
- IP层已经解析了数据;
- TCP模块还没有正常接收这个报文;
- 监听8080的Java程序看不到请求;
- 应用访问日志不会出现记录;
- 客户端通常不断重传,最后连接超时。
如果使用:
bash
iptables -A INPUT \
-p tcp --dport 8080 \
-j REJECT --reject-with tcp-reset
服务器会明确拒绝TCP请求。客户端通常会更快收到连接被拒绝的信息。
DROP与REJECT的典型区别如下:
| 动作 | 服务端行为 | 客户端常见表现 |
|---|---|---|
DROP |
静默丢弃 | 等待重传,最后超时 |
REJECT |
返回拒绝信息 | 较快失败 |
ACCEPT |
继续交给后续协议栈 | 继续进行TCP/UDP处理 |
客户端出现Connection refused时,也可能是端口没有程序监听。因此,不能只根据一个错误信息判断一定是iptables导致的。
6. INPUT放行后才进入TCP或UDP处理
INPUT放行后,IP层根据协议字段,把数据交给TCP、UDP或其他协议模块。
如果是TCP,进入TCP处理。
如果是UDP,进入UDP处理。
传输层模块会根据以下信息查找通信状态和Socket:
text
源IP
源端口
目的IP
目的端口
传输层协议
因此,更准确的数据路径是:
text
Linux内核网络协议栈内部:
IP接收
↓
PREROUTING
↓
路由判断
↓
INPUT
↓
TCP或UDP
↓
Socket
TCP和UDP并不位于Linux内核网络协议栈之外,它们就是协议栈的一部分。
五、TCP三次握手是如何经过Netfilter的?
假设客户端向服务器8080端口建立TCP连接。
第一次握手:
text
客户端发送SYN
服务端接收路径:
text
网卡
↓
PREROUTING
↓
路由判断
↓
INPUT
↓
TCP模块
如果INPUT允许,TCP模块查找监听8080的Socket,然后准备返回SYN-ACK。
第二次握手:
text
服务端发送SYN-ACK
发送路径:
text
TCP模块
↓
IP层
↓
OUTPUT
↓
POSTROUTING
↓
网卡
↓
客户端
第三次握手:
text
客户端发送ACK
再次进入:
text
PREROUTING
↓
INPUT
↓
TCP模块
三次握手完成后,内核会把连接放入监听Socket的已完成连接队列。
随后,Java程序中的:
java
serverSocket.accept();
才能获得这个连接。
所以:
text
网卡收到SYN
不等于:
text
Java的accept()立即返回
在两者之间,还存在PREROUTING、路由判断、INPUT、TCP状态机和Socket队列等处理环节。
如果SYN在INPUT处被DROP,TCP模块无法正常处理握手,应用程序也无法感知这次连接尝试。
六、UDP数据是如何到达Socket的?
UDP接收路径的前半部分与TCP相同:
text
网卡
↓
链路层
↓
IP层
↓
PREROUTING
↓
路由判断
↓
INPUT
↓
UDP模块
↓
Socket接收队列
↓
应用程序
UDP没有TCP中的:
- 三次握手;
- 序号和确认;
- 自动重传;
- 有序字节流;
- 四次挥手。
INPUT放行后,UDP模块根据目的IP和目的端口查找对应Socket,然后把完整数据报放入Socket接收队列。
假设Java程序等待UDP报文:
java
DatagramSocket socket = new DatagramSocket(9000);
byte[] buffer = new byte[1024];
DatagramPacket packet =
new DatagramPacket(buffer, buffer.length);
socket.receive(packet);
如果iptables配置:
bash
iptables -A INPUT \
-p udp --dport 9000 \
-j DROP
数据报在进入UDP模块之前被丢弃,receive()会继续等待。
客户端发送方法返回成功,也不能证明服务端已经收到。它通常只说明数据已经交给客户端本机内核。
因此,UDP应用通常需要在业务协议中自行设计:
- 请求编号;
- 响应报文;
- 超时;
- 重试;
- 去重;
- 幂等处理。
七、第二条路径:Docker端口映射为什么走FORWARD?
假设Docker发布了一个端口:
text
宿主机18080 → 容器172.20.0.10:8080
客户端访问:
text
宿主机IP:18080
数据进入宿主机后,大致经历:
text
客户端请求
↓
宿主机网卡
↓
PREROUTING
↓
DNAT:目的地址修改为172.20.0.10:8080
↓
路由判断
↓
目标是容器地址
↓
FORWARD
↓
POSTROUTING
↓
Docker网桥
↓
容器网络命名空间
↓
容器中的Socket
这里的关键是DNAT。
数据刚进入宿主机时,目的地址是:
text
宿主机IP:18080
Docker创建的NAT规则把它修改为:
text
172.20.0.10:8080
随后内核进行路由判断,发现新的目的地址不是宿主机,而是Docker网络中的容器,于是数据走FORWARD。
这就解释了一个常见现象:
INPUT链没有放行18080,但Docker映射的18080仍然可能被外部访问。
这不是iptables失效,而是该流量没有经过宿主机的INPUT链。
排查Docker端口时,通常要检查:
bash
iptables -t nat -L DOCKER -n -v --line-numbers
iptables -L DOCKER-USER -n -v --line-numbers
iptables -L FORWARD -n -v --line-numbers
它们分别关注:
| 位置 | 作用 |
|---|---|
nat/DOCKER |
把宿主机端口转换成容器地址和端口 |
DOCKER-USER |
执行用户自定义的容器转发控制 |
FORWARD |
决定数据是否允许经过宿主机转发 |
具体规则结构可能随Docker版本、网络模式和防火墙后端发生变化,应以系统实际规则为准。
八、第三条路径:本机应用主动访问外部服务
假设Java应用连接远程数据库:
text
Java应用 → 数据库IP:5432
数据由本机应用产生,典型路径是:
text
Java调用connect()
↓
Socket层
↓
TCP模块创建连接状态并生成SYN
↓
IP层和路由处理
↓
OUTPUT
↓
POSTROUTING
↓
网卡发送
↓
远程数据库
这种流量不是从外部网卡进入的,因此不会经过本机的PREROUTING和INPUT。
OUTPUT控制本机产生的流量
例如:
bash
iptables -A OUTPUT \
-p tcp \
-d 192.0.2.30 \
--dport 5432 \
-j DROP
即使数据库正常运行、地址正确、Java代码也没有问题,连接仍可能超时,因为SYN在离开服务器之前被OUTPUT丢弃。
对Java服务而言,下列通信都可能受到OUTPUT规则影响:
- 访问数据库;
- 访问Redis;
- 注册到Nacos;
- 连接Kafka;
- 调用其他微服务;
- 访问外部模型接口。
POSTROUTING处理即将离开的数据
数据完成路由选择后,在离开本机前经过POSTROUTING。
这里经常执行:
- SNAT;
- MASQUERADE;
- 部分数据包标记处理。
例如容器访问外部网络:
bash
iptables -t nat -A POSTROUTING \
-s 172.20.0.0/16 \
! -o docker0 \
-j MASQUERADE
容器使用内部地址,外部网络通常没有返回该内部网段的路由。MASQUERADE会把源地址转换成宿主机出口地址,返回数据再根据连接跟踪记录交回容器。
九、理解"链"和"表"的区别
学习iptables时,经常会混淆两组概念。
第一组是:
text
PREROUTING
INPUT
FORWARD
OUTPUT
POSTROUTING
第二组是:
text
raw
mangle
nat
filter
security
前者表示数据包经过的处理位置,后者表示规则的主要用途。
| 概念 | 回答的问题 |
|---|---|
| Hook或链 | 数据包在什么阶段被检查? |
| 表 | 这组规则主要用于什么目的? |
| 规则 | 匹配什么数据,执行什么动作? |
常见表的用途如下:
| 表 | 主要用途 |
|---|---|
raw |
对连接跟踪等进行特殊控制 |
mangle |
修改标记或部分数据包属性 |
nat |
DNAT、SNAT、MASQUERADE |
filter |
ACCEPT、DROP、REJECT |
security |
与安全模块相关的额外控制 |
例如:
bash
iptables -t nat -A PREROUTING ...
表示:
在PREROUTING阶段执行地址转换规则。
而:
bash
iptables -t filter -A INPUT ...
表示:
在INPUT阶段执行访问控制规则。
不同表可以存在同名链,但它们属于不同规则集合。并不是每张表都拥有全部五条链。
十、连接跟踪与TCP状态有什么区别?
Linux Netfilter通常会使用conntrack记录通信关系。
常见连接跟踪状态包括:
text
NEW
ESTABLISHED
RELATED
INVALID
而TCP协议栈自身维护的状态包括:
text
LISTEN
SYN_SENT
SYN_RECV
ESTABLISHED
FIN_WAIT
CLOSE_WAIT
TIME_WAIT
这两套状态不能直接等同。
| 状态体系 | 维护者 | 用途 |
|---|---|---|
| TCP状态 | TCP协议栈 | 管理TCP连接生命周期 |
| conntrack状态 | Netfilter连接跟踪模块 | 对数据包进行状态化识别 |
例如:
bash
iptables -A INPUT \
-m conntrack \
--ctstate ESTABLISHED,RELATED \
-j ACCEPT
这条规则表示:
允许属于已有通信或关联通信的数据包进入。
它并不是单纯查询某个TCP Socket是否处于ESTABLISHED状态。
UDP为什么也有ESTABLISHED?
UDP本身没有建立连接的握手过程,但conntrack可以根据五元组维护一段时间内的UDP通信记录:
text
源IP
源端口
目的IP
目的端口
协议
当内核观察到反方向数据后,可以把这条UDP通信记录视为双向通信,并标记为ESTABLISHED。
这是Netfilter的连接跟踪状态,不表示UDP协议真的建立了TCP式连接。
十一、NAT为什么需要连接跟踪?
假设存在端口映射:
text
宿主机18080 → 容器8080
一条新通信的首个数据包到达后,内核根据NAT规则确定转换关系,并把它与conntrack记录关联起来。
处理过程可以简化为:
text
首个数据包
↓
匹配NAT规则
↓
确定地址和端口映射
↓
关联到conntrack记录
↓
后续同一通信沿用映射
返回方向的数据也根据连接跟踪记录完成反向转换。
例如:
text
客户端 → 宿主机18080
被转换为:
text
客户端 → 容器8080
容器响应时,内核会完成相应的反向转换,让客户端仍然认为自己在与宿主机18080通信。
NAT通常在新通信的首个数据包上确定映射关系,但后续数据包仍然会经过相关的Netfilter路径。它们并不会绕过防火墙。
十二、为什么新连接失败,已有连接却还能使用?
假设INPUT链中先放行已有通信:
bash
iptables -A INPUT \
-m conntrack \
--ctstate ESTABLISHED,RELATED \
-j ACCEPT
然后再拒绝8080:
bash
iptables -A INPUT \
-p tcp --dport 8080 \
-j DROP
已有连接的数据包先匹配ESTABLISHED规则,因此能够继续传输。
新的TCP SYN不属于已有连接,会继续向后匹配,最终被DROP。
这时就可能出现:
- 已有SSE连接继续输出;
- 已有WebSocket继续工作;
- 新客户端无法连接;
- 运维人员误以为规则没有生效。
如果DROP规则放在状态放行规则前面,并且匹配所有到达8080的数据包,那么已有连接也可能中断。
所以分析iptables不能只看"有没有某条规则",还要看:
- 规则属于哪条链;
- 规则位于什么顺序;
- 数据是否满足匹配条件;
- 数据处于什么conntrack状态;
- 规则计数是否增长。
十三、为什么客户端超时,应用日志却什么都没有?
如果数据包在INPUT处被DROP:
text
客户端请求
↓
服务器网卡
↓
IP和Netfilter处理
↓
INPUT规则DROP
↓
处理结束
后面的TCP/UDP模块和Socket没有获得这个报文。
应用程序自然不会产生:
- HTTP访问日志;
- Controller日志;
- 业务异常;
- 调用链记录。
TCP服务的accept()可能继续等待,UDP服务的receive()也可能继续阻塞。
因此,当出现:
text
客户端连接超时
+
应用没有任何请求日志
应当优先确认请求是否真正到达应用,而不是立即修改业务代码。
十四、沿着数据路径逐层排查
面对"端口无法访问"问题,可以按照数据包的真实路径逐层排查。
第一层:进程是否存在
bash
systemctl status example-app --no-pager -l
或者:
bash
ps -ef | grep java
这一层只能证明进程状态,不能证明端口和业务正常。
第二层:Socket是否监听
bash
ss -lntp | grep ':8080 '
重点观察监听地址。
text
127.0.0.1:8080
通常只允许本机回环访问。
text
0.0.0.0:8080
表示监听所有本机IPv4地址。
text
192.0.2.20:8080
表示只监听指定本地地址。
iptables放行不会改变Socket绑定范围。
第三层:本机是否可以访问
bash
curl -v \
--connect-timeout 3 \
--max-time 10 \
http://127.0.0.1:8080/health
如果本机也失败,应优先检查:
- 应用是否真正启动完成;
- 端口是否正确;
- 接口路径是否正确;
- 应用是否只支持HTTPS;
- 业务依赖是否异常。
如果本机成功、远程失败,再检查外部网络和防火墙。
第四层:数据是否到达服务器
在服务端执行:
bash
tcpdump -ni any tcp port 8080
同时从远程客户端发起请求。
| 抓包结果 | 排查方向 |
|---|---|
| 完全看不到SYN | 安全组、上游路由、客户端出口 |
| 看到重复SYN,没有SYN-ACK | INPUT、监听状态、响应路径 |
| 看到SYN和SYN-ACK,没有最终ACK | 客户端侧或回程链路 |
| 三次握手完成,没有HTTP响应 | 应用处理或应用层协议 |
| 请求和响应都能看到 | 网络基本连通,检查客户端或代理 |
tcpdump的捕获位置与Netfilter钩子并不完全相同。抓到数据只能证明数据经过了某个抓包点,不能单独证明它已经通过INPUT并进入应用。
第五层:检查iptables规则和计数
宿主机服务重点检查:
bash
iptables -L INPUT -n -v --line-numbers
Docker或转发流量重点检查:
bash
iptables -L FORWARD -n -v --line-numbers
iptables -L DOCKER-USER -n -v --line-numbers
NAT映射检查:
bash
iptables -t nat -L PREROUTING -n -v --line-numbers
iptables -t nat -L DOCKER -n -v --line-numbers
重点观察:
- 是否存在匹配端口的规则;
- ACCEPT是否位于DROP或REJECT之前;
- 来源IP是否满足规则;
- 请求是否走INPUT还是FORWARD;
- 测试请求发生时,哪条规则的计数增长。
如果某条DROP规则计数随着测试增长,就说明请求命中了它。
第六层:确认应用是否收到请求
如果端口监听、TCP握手和防火墙都正常,再检查:
- 应用访问日志;
- URL路径;
- HTTP方法;
- 请求头和请求体;
- TLS和证书;
- 认证信息;
- 线程池与连接池;
- 数据库、Redis、Nacos等依赖。
这样能够避免网络尚未打通时,就在业务代码中反复寻找问题。
十五、把整套逻辑浓缩成三条主路径
外部客户端访问宿主机服务:
text
网卡
↓
PREROUTING
↓
路由判断
↓
INPUT
↓
TCP/UDP
↓
Socket
↓
应用程序
外部流量经宿主机转发到容器或其他主机:
text
网卡
↓
PREROUTING
↓
路由判断
↓
FORWARD
↓
POSTROUTING
↓
容器或其他主机
本机应用访问外部服务:
text
应用程序
↓
Socket
↓
TCP/UDP
↓
IP和路由
↓
OUTPUT
↓
POSTROUTING
↓
网卡
理解这三条路径后,很多现象都可以得到解释:
- 应用端口监听,但远程访问超时:检查INPUT及上游网络;
- Docker发布端口不受INPUT规则限制:流量主要经过FORWARD;
- Java连接数据库超时:还要检查OUTPUT和返回路径;
- 客户端超时但应用无日志:请求可能在到达Socket前就被丢弃;
- 已有长连接正常,新连接失败:检查conntrack状态与规则顺序;
- 修改NAT规则后部分连接仍沿用旧映射:已有连接可能还关联旧的conntrack记录。
iptables不会出现在Java的业务代码中,却能决定数据包是否有机会到达Java Socket。严谨地说,iptables负责配置规则,Netfilter在Linux内核网络路径中执行规则;TCP、UDP、IP、Socket和Netfilter都属于Linux内核网络处理体系的不同部分。
排查网络问题时,不要从"应用是否报错"单点判断,而应沿着数据的实际路径逐层确认:
text
进程
↓
监听Socket
↓
本机访问
↓
外部数据是否到达
↓
Netfilter是否允许
↓
TCP/UDP是否完成处理
↓
请求是否进入应用
↓
业务是否正确响应
当每一层都有证据,连接超时、端口不通和容器转发异常就不再是一团模糊的"网络问题",而会变成一个可以逐步定位、验证和解决的具体问题。