docker-proxy 端口映射和 nftables 的相处方式
ufw 里明明 deny 了 5432,外面还是能连上。这种「防火墙不生效」
十有八九跟 docker 有关——它不走 ufw 那套,而是自己往 netfilter 里插链。
现象
sudo ufw status
# 5432/tcp DENY Anywhere
nc -vz 1.2.3.4 5432 # 从外部:succeeded!
sudo ss -tlnp | grep 5432
# LISTEN 0 4096 *:5432 *:* users:(("docker-proxy",pid=1234,fd=4))
监听在 *:5432 的是 docker-proxy,
它在 nat 表里做了 DNAT,而这条 DNAT 规则挂在
DOCKER 链上,优先级早于 ufw 的 ufw-before-input。
于是包还没走到 ufw 的规则就已经被转进容器了。
查证
sudo nft list ruleset | grep -A5 'chain DOCKER'
sudo iptables -t nat -nL DOCKER | head
会看到类似 dnat to :172.17.0.2:5432 的规则,注释里带着容器 ID。
这就实锤了:不是 ufw 写错,是包根本没轮到它。
三种收口
1. 把发布地址收到回环(最省事)
如果这个端口只需要本机访问,压根别用 0.0.0.0:
ports:
- "127.0.0.1:5432:5432"
改了之后 ss 里就只剩 127.0.0.1:5432,
外网连不上,也不影响同机其他进程。
2. 在 DOCKER-USER 链里拦
DOCKER-USER 是留给用户自定义的,docker 自己不会动它,
而且它在 DOCKER 之前执行,所以能拦住转发的包:
sudo iptables -I DOCKER-USER -p tcp --dport 5432 \
! -s 203.0.113.7 -j DROP
注意只对「转发」生效,如果你用的是 userland proxy(*:5432
由 docker-proxy 监听),包是进本机 INPUT 的,得写 INPUT 规则。
先 ss 看一眼是谁在听,别想当然。
3. 关掉 docker 的 iptables 管理
# /etc/docker/daemon.json
{
"iptables": false,
"userland-proxy": false
}
这个最彻底也最危险:关掉之后容器没有 NAT,出网直接断, 需要自己写 SNAT/DNAT。只有在网络模型完全自己掌控时才这么干。
我最后的选择
只对内网提供服务的端口一律 127.0.0.1 发布,再由本机的
nginx 做转发;确实要对外开的,写进 DOCKER-USER 并限制来源。
两层都做,是因为 docker compose down && up
之后 DOCKER 链会重建,而 DOCKER-USER 是持久的。
排查口诀:先ss -tlnp看谁在听,再nft list ruleset看规则顺序,最后才怀疑 ufw。