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。
Docker nftables 防火墙