1. 引言

在生产环境中,Docker 默认的 bridge 模式为容器提供了网络连通性,同时借助 MASQUERADE(IP 伪装)实现容器访问外网。但在某些场景下,我们希望局域网内其他主机能够 直接通过容器自身的 IP 地址 访问服务(例如 ssh://git@10.20.1.200:22),而不必依赖 -p 端口映射或反向代理。这便是“扁平网络”所追求的效果:虽然容器处于独立私网(10.20.1.0/24),但整个局域网都能通过路由直达,感觉就像和宿主机处于同一网段。

本文将基于 Fedora,演示如何在不依赖 Docker 桥接的 NAT 功能、仅使用 Linux 内核路由表和 iptables 的条件下,让物理主机网段 10.10.1.0/24 中的客户端能够直连 Docker 容器网段 10.20.1.0/24,并以 Gitea 的 22 端口(SSH) 拉取代码作为完整验证场景。宿主机将扮演 路由器 角色,将发往容器网络的报文正确转发。

核心思想:保留 Docker 默认的 bridge 网络作为容器二层隔离手段,但 剥离其 snat(源地址转换) 规则,转而由宿主机的 net.ipv4.ip_forward 和显式路由完成数据包转发,使容器网络对外完全“透明”,不再由 Docker 包揽一切。

2. 环境信息与设计思路

2.1 网络拓扑

角色IP 地址说明
宿主机 (Fedora)10.10.1.100物理接口 enp0s3,连接局域网
宿主机 bridge 网关10.20.1.1Docker docker0 网桥地址
Gitea 容器10.20.1.200运行于 Docker bridge,分配固定 IP
客户端10.10.1.50需要直连容器拉取代码

简化拓扑(需加静态路由):

[客户端 10.10.1.50] ──── 局域网 ──── [宿主机 10.10.1.100] ──── [docker0 10.20.1.1] ──── [Gitea 容器 10.20.1.200]
         ↑                                 ↑                                 ↑

客户端发往 10.20.1.200 的报文必须先送到宿主机,宿主机再根据自身路由表转发至 docker0 网桥,最终到达容器。

2.2 与默认 Docker 桥接的区别

项目Docker 默认行为本文目标
容器出网MASQUERADE 成宿主机 IP保留容器源 IP(无 NAT)
外部访问容器不可达(私网不可路由)添加静态路由 + 允许转发
端口映射-p DNAT 规则直接使用容器 IP:端口
Docker iptables自动创建大量规则禁用 Docker 的 iptables 干预(或手动清理)

我们不删除 docker0 网桥(它依然是容器通信的必需组件),但会 阻止 Docker 添加 NAT 规则,让宿主机变成真正的三层路由器。

3. 前置准备

3.1 系统环境

  • Fedora ≥ 38
  • Docker Engine 已安装
  • 确保 iptables / firewalld 可被脚本操作

3.2 开启宿主机 IP 转发

sudo sysctl -w net.ipv4.ip_forward=1
echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-ipforward.conf
sudo sysctl --system

3.3 禁用 Docker 的 iptables 干涉(关键)

Docker 默认会操作 iptables 增加 MASQUERADE 和 DNAT 规则,这会导致容器出去的流量被 SNAT,破坏我们的扁平目标。需要在 Docker 守护进程配置中关闭此行为:

sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<EOF
{
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" },
  "iptables": false
}
EOF

# 重启 Docker
sudo systemctl restart docker

⚠️ 警告:将 iptables 设为 false 后,Docker 将不再自动创建任何 iptables 规则,包括容器间通信所需规则。因此容器之间通过 docker0 的互通性需要手动确保(本文通过 iptables FORWARD 链保证,后文有详细配置)。

3.4 创建 Docker bridge 网络并固定 IP 范围

为了让容器 IP 固定,我们使用自定义桥接网络(避免默认 bridge 的 IP 分配不确定性):

sudo docker network create \
  --driver bridge \
  --subnet=10.20.1.0/24 \
  --gateway=10.20.1.1 \
  --ip-range=10.20.1.192/27 \
  -o "com.docker.network.bridge.name"="br-flat" \
  flat_bridge
  • --driver bridge:使用默认的桥接驱动(所以是“一般模式”)。
  • --subnet / --gateway:容器私网 IP 网段,宿主机侧以 10.20.1.1 作为网关。
  • --ip-range:控制 DHCP 分配范围,避免冲突。
  • -o ...bridge.name:指定网桥名称为 br-flat,方便管理。

之后我们所有的容器都连接到 flat_bridge 网络,并通过 ipv4_address 参数固定 IP。

4. 防火墙(iptables / firewalld)配置

4.1 核心转发规则

由于 Docker 的 iptables 已禁用,我们需要手工添加转发规则,确保 docker0 / br-flat 与外部网卡 enp0s3 之间的流量能够双向转发,且 不进行源地址转换。

创建自定义 iptables 规则(临时,重启不丢失则需写成 systemd 脚本或 rc.local):

# 添加 FORWARD 规则:允许从局域网到容器网络的流量
sudo iptables -I FORWARD -i enp0s3 -o br-flat -d 10.20.1.0/24 -j ACCEPT
sudo iptables -I FORWARD -i br-flat -o enp0s3 -s 10.20.1.0/24 -j ACCEPT

# 如果之前 Docker 残留了 lxcbr0 或 docker0 规则,可予保留;但确保没有 MASQUERADE 针对 10.20.1.0/24
sudo iptables -t nat -L POSTROUTING -n | grep 10.20.1
# 如果有输出,应将其删除
# sudo iptables -t nat -D POSTROUTING <规则行号>

以上规则保证 enp0s3 <-> br-flat 的双向流量被允许,且不发生 NAT。回程报文源地址仍然是 10.20.1.200。

4.2 允许 SSH 流量(端口 22)

若宿主机自身开启了防火墙(firewalld 或 iptables),需要显式放行目标为容器 10.20.1.200:22 的流量。对于 iptables(已关闭 Docker iptables,但系统可能仍有基础规则):

sudo iptables -I FORWARD -d 10.20.1.200 -p tcp --dport 22 -j ACCEPT

如果使用 firewalld,推荐通过 direct 规则或添加 rich-rule:

sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 -d 10.20.1.200 -p tcp --dport 22 -j ACCEPT
sudo firewall-cmd --reload

务必确认 没有针对 10.20.1.0/24 的 REJECT/DROP 规则 架在 FORWARD 链的前面。

5. 启动 Gitea 容器并验证路由

5.1 启动 Gitea 容器

使用 docker-compose.yml:

version: "3.8"

services:
  gitea:
    image: gitea/gitea:latest
    container_name: gitea
    networks:
      flat_bridge:
        ipv4_address: 10.20.1.200
    volumes:
      - ./data:/data
    environment:
      - USER_UID=1000
      - USER_GID=1000
    restart: always

networks:
  flat_bridge:
    external: true

启动:

sudo docker compose up -d

5.2 宿主机本地验证

宿主机自身能否直接 ping 容器?

ping -c 2 10.20.1.200

由于宿主机上已有 br-flat 接口且 IP 转发开启,直接可达。

确认容器监听了 SSH:

sudo docker exec gitea ss -tlnp | grep 22

5.3 从客户端验证本地路由缺失

在客户端 10.10.1.50 上先 ping 容器地址,很可能会失败,因为客户端上并没有到 10.20.1.0/24 的路由。这正是我们需要手动添加静态路由的原因。

6. 客户端一侧:添加静态路由

在任何需要直接访问容器的客户端(或局域网网关/核心交换机)上添加路由,使得下一跳指向宿主机:

Linux 客户端:

sudo ip route add 10.20.1.0/24 via 10.10.1.100 dev eth0

Windows 客户端(管理员 PowerShell):

route add 10.20.1.0 mask 255.255.255.0 10.10.1.100

添加后,客户端就能将发往 10.20.1.0/24 的报文送往 10.10.1.100,并由宿主机转发至容器。

💡 规模化建议:如果局域网内有多台设备需要访问,可在局域网默认网关(路由器)上添加这条静态路由,这样全网所有设备都能直接访问容器网络,无需逐台配置。

验证连通性:

ping 10.20.1.200   # 应成功

6.1 确保回程路径

当容器回复客户端的报文到达容器网关(br-flat,即宿主机 10.20.1.1)后,宿主机内核查找路由表,发现目标 10.10.1.50 属于直连的 enp0s3 所在网段,因此直接通过 enp0s3 发出。由于我们在 iptables 中 未做 SNAT,回复报文的源 IP 依然是 10.20.1.200,客户端收到后完成握手。

7. 验证 Gitea SSH 拉取代码

首先创建测试仓库,并确保 Gitea 配置允许 SSH 连接(默认即已开启)。

在客户端上执行:

# 测试 SSH 认证(按提示输入密钥,如果已配置)
ssh -T git@10.20.1.200

# 克隆仓库(替换 username 和 repo)
git clone ssh://git@10.20.1.200:22/username/repo.git

如果一切顺利,Git 将会通过直连容器 IP 的 SSH 服务拉取数据,整个过程 没有任何 NAT 或者端口映射,真正展现了“扁平网络”下的容器直连。

8. 持久化与故障恢复

以上 iptables 规则和路由添加都是临时的,重启后失效。为了生产环境,建议:

  • 宿主机 iptables:将规则写入 /etc/sysconfig/iptables 或通过 firewalld 的 direct 永久配置,也可以使用 iptables-persistent 或 systemd 服务。
  • 静态路由持久化:在 Fedora 宿主机上,可通过 NetworkManager 的连接配置文件添加静态路由;在客户端上同样采用对应系统的持久化方式。
  • Docker 守护进程:daemon.json 中的 "iptables": false 已为持久配置,重启不影响。

9. 注意事项

  • 不可见性:由于关闭 Docker iptables,Docker 的容器间通信完全由内核的 br-flat 二层转发实现,无需额外规则。但若需要容器访问公网,宿主机仍需手动添加 MASQUERADE 规则,此时应精确匹配源地址,避免破坏扁平网络效果。
  • 安全性:容器端口全部暴露于局域网,相当于直接附加在物理网络中。务必在容器内部使用防火墙(如 Gitea 的 SSH 白名单)、或通过 iptables 在宿主机 FORWARD 链上限制来源。
  • 路由规划:确保容器 IP 段 10.20.1.0/24 没有与任何其他网络冲突,且 LAN 路由器能够接受此类路由注入。
  • 其他 Docker 网络:如果还有其他容器使用默认 bridge(docker0),其流量规则不受影响(因为 Docker 不再自动添加规则),需要额外配置才能互访。

10. 总结

本文演示了在 Fedora 上,通过禁用 Docker 自带的 NAT 规则、手工配置 iptables FORWARD 规则以及客户端静态路由,成功将默认桥接网络下的容器 IP(10.20.1.0/24)暴露给物理网络(10.10.1.0/24),实现“扁平网络”效果。客户端可以直接使用 ssh://git@10.20.1.200:22 克隆 Gitea 仓库,体现了 Linux 内核路由能力 替代 Docker 内置 NAT 的强大灵活性。这种架构适用于需要固定容器 IP、且不希望大量端口映射的中小型生产环境。

更多推荐