通过 Docker 容器路由网络流量

本文假设您具备 Linux、网络和容器的基本知识。下面演示配置可作为 Github 上的公共 Git 仓库 https://github.com/safelyup/routing-via-containers 获取。

为了提供与远程网络或互联网的连接,容器可以作为可扩展且可维护的解决方案。无需从本地网络的多个源创建到远程网络(或多个远程网络)的 VPN 连接,本地网络流量可以通过容器进行路由。

VPN 客户端容器

在本文中,我将使用 OpenVPN https://openvpn.net/ 客户端来演示不同场景。但相同或类似的路由和 NAT 设置适用于任何 VPN 客户端,甚至无需 VPN 客户端。

首先,我们需要一个 OpenVPN 服务器。您可以使用安装指南 https://openvpn.net/community-resources/how-to/#installing-openvpn 自行托管一个。服务器启动并运行后,生成 OpenVPN 客户端所需的 ovpn 文件。

接下来,我们需要为容器镜像创建 Dockerfile

FROM ubuntu:24.04

RUN apt update \
  && apt install curl iproute2 iptables openvpn -y \
  && rm -rf /var/lib/apt/lists/*

WORKDIR /
COPY ./run.sh /run.sh

CMD ["/run.sh"]

容器的主要 CMD run.sh

#!/bin/sh
set -eu

# 设置 NAT
iptables -A FORWARD -i eth0 -o tun0 -j ACCEPT
iptables -A FORWARD -i tun0 -o eth0 -j ACCEPT
iptables -t nat -A POSTROUTING -o tun0 -j MASQUERADE

cd /vpn
exec openvpn --config client.ovpn

iptables 已安装在我们的镜像中,脚本在容器内创建了所需的NAT https://en.wikipedia.org/wiki/Network_address_translation 规则,用于出站流量。

让我们构建镜像并使用必要选项运行 Docker 容器。确保将正确的 ovpn 文件作为卷挂载,以便 openvpn 进程在容器内可访问。

$ docker build -t ovpn-client .

$ docker run --cap-add=NET_ADMIN --device=/dev/net/tun \
  -v './client.ovpn:/vpn/client.ovpn' --dns='1.1.1.1' \
  --name=vpn1 -d ovpn-client

检查容器日志以确保客户端已连接到服务器。您还可以在容器内执行 curl ifconfig.me 以获取其公共 IP 地址,该地址应返回 OpenVPN 服务器的 IP。

$ docker ps
CONTAINER ID   IMAGE         COMMAND     CREATED          STATUS          PORTS     NAMES
fd2aafd9e4ab   ovpn-client   "/run.sh"   12 minutes ago   Up 12 minutes             vpn1

$ docker logs vpn1
...
2024-11-12 12:43:30 Initialization Sequence Completed
2024-11-12 12:43:30 Data Channel: cipher 'AES-256-GCM', peer-id: 0
2024-11-12 12:43:30 Timers: ping 10, ping-restart 120

$ docker exec vpn1 curl ifconfig.me
79.229.10.xx

在本地网络上路由流量

我们已经部署了至少一个 VPN 客户端容器——您可以根据需要部署多个——但没有流量通过它。

为了使容器可用于我们的本地网络,我们仍然需要配置主机服务器——本地网络的网关主机。在本演示中,我使用 Ubuntu 24.04 作为主机。

启用 IPv4 数据包转发并添加 NAT 规则(类似于我们在容器中执行的操作):

sudo sysctl -w net.ipv4.ip_forward=1

sudo iptables -A FORWARD -i eth0 -o docker0 -j ACCEPT
sudo iptables -A FORWARD -i docker0 -o eth0 -j ACCEPT
sudo iptables -t nat -A POSTROUTING -o docker0 -j MASQUERADE

我们还应通过取消注释或添加行 net.ipv4.ip_forward=1 到文件 /etc/sysctl.conf 来使更改持久化。同时使 iptables 配置持久化——使用 iptables-persistent 包来实现。

现在,我们准备好为主机设置路由规则。

在此设置中,我们希望将网关主机作为本地网络上所有其他主机的默认网关。为了支持在网关主机上拥有多个 VPN 容器,我们需要能够将网络中任何特定主机的流量通过正确的容器进行路由。例如,如果 VM1 需要连接到网络 A,我们需要将其流量通过容器 A 进行路由,依此类推。

通过基于策略的路由通过容器进行路由

源策略路由 用于根据数据包的来源路由数据包。为了实现基于源的路由,我们将使用路由表。我们希望为每个 VPN 容器提供一个定制的路由表,并将该容器的 IP 作为表的默认网关。

首先,让我们在文件 /etc/iproute2/rt_tables 末尾为每个 VPN 容器添加一个新的表。例如,参见 ab

# 保留值
255     local
254     main
253     default
0       unspec
# local
#1      inr.ruhep
10 a
11 b

现在我们可以将 VM1 和 VM2 添加到正确的路由表中,并为每个表设置默认网关到正确的容器 IP。

$ sudo ip rule add from 192.168.64.3 table a
$ sudo ip route add default via 172.17.0.2 table a

$ sudo ip rule add from 192.168.64.4 table b
$ sudo ip route add default via 172.17.0.3 table b

要查找本地容器 IP,您可以使用带有正确容器名称的 docker inspect 命令。例如 vpn1

$ docker inspect \
  -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' vpn1

172.17.0.2

最后,确保您已将 VM1 和 VM2 的默认网关替换为网关主机的 IP。

您可以通过在任何 VM 的浏览器中打开 https://ifconfig.me 或执行 curl ifconfig.me 来测试设置。您应该能够看到 OpenVPN 服务器的 IP 作为结果。

此设置允许更新网关主机上的路由表并在不接触主机的情况下动态更改路由。

使用 MacVLAN 简化设置

如果控制网关主机上的路由规则不是必需的,我们可以通过MacVLAN 网络驱动 简化设置,将容器直接连接到本地网络,并在 VM1 和 VM2 上将默认网关设置为正确的本地容器 IP。

通过 MacVLAN 通过容器进行路由

MacVLAN 网络驱动程序可用于为每个容器的虚拟网络接口分配 MAC 地址,使其看起来像是直接连接到本地网络的物理网络接口。

您可以使用 docker network 命令创建一个带有 MacVLAN 驱动的新网络:

docker network create -d macvlan \
  --subnet=192.168.0.0/24 --gateway=192.168.0.1 \
  --opt="parent=eth0" vpn-transit

要使用新网络部署容器,您可以简单地使用带有选项 --net=vpn-transit --ip=192.168.0.xdocker run 命令。

查看 compose.yaml 文件以了解如何使用 docker compose 部署网络和容器。

类生产环境设置

在公共或私有云环境中的更类生产的多租户设置中,我们可以在 VM 上部署 VPN 客户端容器,连接到“中转网络”;并拥有多个隔离网络(租户网络),每个网络包含多个 VM。

在类生产环境中通过容器进行路由

与 VM/租户相关的路由规则可以集中在中转网络路由表中;中转网络表中的每个 VM 一条规则,以控制所有租户的流量流向。

原文阅读