Debian多网卡Docker-MySQL回程路由排查
在一台 Debian 服务器上配置了多块网卡,每块网卡都有独立公网 IP。MySQL 通过 Docker 部署,发布端口为 3306。
实际使用时发现:
- 使用 eth0 的公网 IP,可以连接 MySQL。
- 使用其他网卡的公网 IP,连接超时。
- 已经配置了基于公网源 IP 的策略路由,但问题依然存在。
最终通过抓包确认:请求从 eth1 进入,经过 Docker 转发到 MySQL;MySQL 的回复却从 eth0 发出。
下面记录排查过程和经过验证的修复方法。文中所有 IP 地址均为文档保留地址(RFC 5737),仅作示意:203.0.113.10 是 eth0 公网地址,198.51.100.20 是 eth1 公网地址,203.0.113.1 / 198.51.100.1 是两块网卡的网关,172.20.0.2 是 MySQL 容器地址,0.0.0.0 表示监听所有本机 IPv4 地址。命令和输出均为示意,请替换为你自己的实际配置后再执行。
一、环境与现象
以 eth0、eth1 为例:
| 对象 | 地址或配置 |
|---|---|
| eth0 | 203.0.113.10/24 |
| eth0 网关 | 203.0.113.1 |
| eth1 | 198.51.100.20/24 |
| eth1 网关 | 198.51.100.1 |
| Docker 网桥 | br-cb0d8db03272 |
| MySQL 容器 IP | 172.20.0.2 |
| Docker 端口映射 | 0.0.0.0:3306->3306/tcp |
主路由表默认出口为 eth0:
default via 203.0.113.1 dev eth0 |
同时,服务器已经配置了 eth1 的源地址策略路由:
from 198.51.100.20 lookup 199 |
路由表 199 内容为:
default via 198.51.100.1 dev eth1 |
在 Windows 客户端测试 eth1 的 MySQL 端口:
Test-NetConnection 198.51.100.20 -Port 3306 |
结果为:
TcpTestSucceeded : False |
二、先排除监听地址和反向路径过滤
查看容器端口映射:
docker ps --format 'table {{.Names}}\t{{.Ports}}'
|
MySQL 显示:
0.0.0.0:3306->3306/tcp |
查看宿主机监听:
ss -lntp |
对应输出:
LISTEN 0 4096 0.0.0.0:3306 0.0.0.0:* users:(("docker-proxy",...))
|
这说明宿主机的 3306 发布在所有 IPv4 地址上,并没有只绑定 eth0。不过,监听所有地址不代表端到端网络一定可达。Docker 端口发布文档
接着查看反向路径过滤:
sysctl net.ipv4.conf.all.rp_filter \
net.ipv4.conf.eth0.rp_filter \
net.ipv4.conf.eth1.rp_filter
|
本次环境输出均为 0。
因此,可以排除 eth0、eth1 上的 rp_filter 检查导致丢包。其他环境如果使用严格模式 1,多网卡的非对称路由可能触发丢包,需要单独核查。Linux 内核文档
三、抓包发现:eth1 进,eth0 出

在服务器执行:
tcpdump -ni any -nn \ 'tcp port 3306 and not (src net 172.20.0.0/16 and dst net 172.20.0.0/16)' |
这里过滤掉了 Docker 子网内部的通信,避免把其他容器访问 MySQL 的正常流量误当成外部测试流量。
随后,在外部 Windows 客户端重新测试:
Test-NetConnection 198.51.100.20 -Port 3306 |
抓包得到的关键过程如下:
eth1 In: 客户端 → 198.51.100.20:3306 SYN Docker 网桥 Out: 客户端 → 172.20.0.2:3306 SYN Docker 网桥 In: 172.20.0.2:3306 → 客户端 SYN-ACK eth0 Out: 198.51.100.20:3306 → 客户端 SYN-ACK |
这段输出证明了三件事:
- 外部请求已经到达 eth1。
- Docker 已将请求转发给 MySQL,MySQL 也发出了回复。
- 回复使用 eth1 的公网源地址,却从 eth0 发出。
之后可以看到重复的 SYN 和 SYN-ACK,但没有完成握手。
回程出口错误已经确定。 至于回复具体在哪里被丢弃,仅靠服务器侧抓包无法确定;上游网络可能对源地址与出口的对应关系有限制。
四、为什么已有源 IP 策略路由仍然无效?
已有规则匹配的是公网源地址:
from 198.51.100.20 lookup 199 |
但 Docker 容器回复经过宿主机转发时,在选择路由的阶段,源地址仍然是:
172.20.0.2 |
它无法匹配上面的公网源地址规则,于是使用主路由表,从 eth0 出口发送。
之后,连接跟踪对应的反向 NAT 才将源地址恢复成:
198.51.100.20 |
因此,在物理网卡上抓到的最终数据包表现为:
eth0 Out,源地址却是 eth1 的公网 IP |
这也解释了为什么 eth0 能连接:主路由表的默认出口恰好就是 eth0,而其他网卡需要正确的回程选路。
五、修复:识别连接的回包,再按标记选路

本次采用的办法是:
- 利用连接跟踪,识别原本访问
198.51.100.20:3306的连接。 - 只给该连接的回复方向数据包添加标记。
- 根据标记,使用已有的路由表
199,从 eth1 发出。
--ctorigdst 和 --ctorigdstport 匹配连接的原始目的地址及端口;--ctdir REPLY 限定回复方向。这样即使当前数据包的源地址还是容器 IP,也能识别它属于哪个公网入口的连接。iptables 扩展手册
以下命令中的 IP、网桥名、路由表编号需要替换为实际配置;规则优先级和标记也应避免与已有用途冲突。以下添加命令执行一次即可,重复执行会累积规则。
先添加按标记选路的规则:
ip rule add priority 10199 \ fwmark 0x199/0xffffffff lookup 199 |
再对指定连接的回包打标:
iptables -t mangle -I PREROUTING 1 \ -i br-cb0d8db03272 \ -p tcp \ -m conntrack --ctdir REPLY \ --ctorigdst 198.51.100.20 \ --ctorigdstport 3306 \ -j MARK --set-xmark 0x199/0xffffffff |
这里的 0x199 是自定义的数据包标记,不是路由表编号的自动转换;是 ip rule 将该标记关联到了路由表 199。ip rule 手册
规则只匹配回复方向,进入容器的原始请求不会被这条规则标记。
六、验证结果
客户端再次执行:
Test-NetConnection 198.51.100.20 -Port 3306 |
结果变为:
TcpTestSucceeded : True |
实际验证表明,新增规则后 eth1 的 MySQL 端口恢复了 TCP 连通。 这也支持了此前关于 Docker 回程路由的判断。
如果需要进一步确认,可以重新抓包,检查 SYN-ACK 是否从 eth1 发出,并查看规则命中计数:
iptables -t mangle -nvL PREROUTING --line-numbers |
最后再用 MySQL 客户端登录。TCP 连通和数据库认证是两个阶段;如果此时出现 Access denied,应继续检查 MySQL 用户授权和认证配置。
七、回滚与持久化说明
撤销本次规则:
iptables -t mangle -D PREROUTING \ -i br-cb0d8db03272 \ -p tcp \ -m conntrack --ctdir REPLY \ --ctorigdst 198.51.100.20 \ --ctorigdstport 3306 \ -j MARK --set-xmark 0x199/0xffffffff ip rule del priority 10199 \ fwmark 0x199/0xffffffff lookup 199 |
本次验证的是 eth1 的 MySQL 3306 端口临时修复,尚未验证其他网卡,也没有完成重启持久化。
扩展到其他公网 IP 时,需要为各入口配置对应的连接匹配、标记和路由表。持久化时则要同时恢复策略路由和防火墙规则,并确保执行顺序正确、重复执行不会累积规则。另外,Docker 网络重建后网桥名称可能变化,本文规则中的网桥名也需要同步检查。
这次排查最关键的证据,是抓包中的这一行:eth1 的公网源地址出现在了 eth0 Out 上。 端口确实开放、数据库也确实回复了,但回复没有沿着正确的出口返回。