TL;DR; 我正在尝试在我的 LAN 上运行的机器上设置一堆面向 Internet 的服务(Web、SMTP、其他),并使用 Wireguard 将流量从面向公众的 VPS 机器转发到它,这样可以保留该流量的源 IP(即不破坏 fail2ban 和朋友)。这真是让我头疼……
我正在尝试重新设计我的家庭实验室/家庭服务器设置,并且我已经进入了绞尽脑汁的阶段。
我有一台 Ubuntu 服务器(我们称之为“后端”),它通过 Docker 托管一系列服务(HTTP、HTTPS、SMTP、IMAP)。这台机器隐藏在 NAT 后面,我希望它保持这种状态。
我有一台轻量级 VPS 机器(也是 Ubuntu)(我们称之为“前端”)。我们假设它有众所周知的 IP 123.456.789.123。
我想在两台机器之间创建一个 Wireguard 隧道,并将到“前端” VPS(80,443,25,465,587,993,...)的入站流量转发到“后端”上的相应服务,如下所示:
- 我可以物理移动后端机器,它就会“正常工作”(即“后端”启动隧道)
- 连接的源 IP 地址对于 VPS 上的底层服务是可见的(即我不想只使用 rinetd、Caddy 或伪装连接之类的东西,因为它们不适用于非代理友好流量)
- 我不希望后端机器的所有流量都通过前端转储,只希望源自前端的内容。
- 我希望能够从 VPS 上的其他容器访问这些服务(即发夹式 NAT 类型的东西 - 具体来说,几个容器需要连接到 SMTP 服务器容器)
这是我脑海中所想的粗略图画...
我找到的最接近的资源如下(特别是策略路由的示例)但我认为我对 Docker 的使用使这一点变得复杂。
https://www.procustodibus.com/blog/2022/09/wireguard-port-forward-from-internet/#default-route
还有这篇文章:
(以下示例中我将重点介绍 HTTP)
在“前端”上使用以下 Wireguard 配置:
[Interface]
PrivateKey = ###
Address = 10.99.1.2
ListenPort = 51822
# packet forwarding
PreUp = sysctl -w net.ipv4.ip_forward=1
# port forwarding (HTTP) // repeat for each port
PreUp = iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j DNAT --to-destination 10.99.1.1
PostDown = iptables -t nat -D PREROUTING -i eth0 -p tcp --dport 80 -j DNAT --to-destination 10.99.1.1
# remote settings for the private server
[Peer]
PublicKey = ###
AllowedIPs = 10.99.1.1
以及“后端”的 wireguard 配置。
[Interface]
PrivateKey = ####
Address = 10.99.1.1
Table = vpsrt
PreUp = sysctl -w net.ipv4.ip_forward=1
PreUp = ip rule add from 10.99.1.1 table vpsrt priority 1
PostDown = ip rule del from 10.99.1.1 table vpsrt priority 1
# remote settings for the public server
[Peer]
PublicKey = ####
Endpoint = 123.456.789.123:51822
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
有趣的是,使用上述方法,我可以从“前端”ping 到“后端”,但不能反向ping。我不确定这是否是我第一次做错事的迹象。
通过上述配置,如果我直接在“后端”机器(即python3 -m http.server 80
)上运行 Web 服务器,它就可以很好地运行。
具体来说:
- 来自互联网:
curl http://123.456.789.123
有效(源 IP = 客户端 IP!耶!) - 从前端:
curl http://10.99.1.1
工作(源 IP = 10.99.1.2) - 从后端:
curl http://10.99.1.1
工作(源 IP = 10.99.1.1) - 从后端:
curl http://123.456.789.123
大部分工作正常(源 IP = 我的 LAN 的公共 IP,希望此流量保持在内部)
巨大的成功!
问题:我无法使用在“后端”上运行的容器化服务来实现这一点
但是...如果我将 HTTP 服务器移到容器中,它就会停止工作......
services:
whoami:
image: "containous/whoami"
ports:
- 80:80
restart: always
我认为问题在于我实际上并没有在后端机器的端口 80 上运行服务,而是在某个 Docker 分配的内部 IP 上运行服务,然后 iptables 规则将端口 80 上的流量转发到该容器。不幸的是,我在这方面的知识仍然有很多空白,我不确定如何排除故障或解决这个问题。
我尝试过的事情:
- 将所有流量从“后端”转发到“前端”(从“后端”配置中删除基于策略的路由表和 PreUp/PostDown,并向“前端”添加伪装)。这几乎可以正常工作,但现在我通过 VPS 转储所有流量(嗯),发夹式路由不起作用。
- 我尝试为邮件和 Web 服务器的容器分配静态 IP。我更改了“前端”配置以使用这些 IP,并将它们添加到服务器上的 AllowedIP。但我无法让这个方法奏效。
答案1
您的分析是正确的:它不适用于容器化服务,因为与容器的连接被转发(好像它们在另一台主机上),而不是直接绑定到后端主机上的本地端口。
您无需对 WireGuard 配置进行任何更改;您需要做一些额外的工作来将容器的响应路由回 WireGuard 隧道。您可以采取两种方法:
- 为每个容器或网络添加策略路由规则
- 标记 WireGuard 连接并添加回复流量规则
1.为每个容器或网络添加策略路由规则
这是更简单的方法,但缺点是每个容器都将使用 WireGuard 隧道所有外部出站流量. 它们发起的任何外部出站流量(例如软件更新或与第三方服务的连接)或它们尝试回复其他外部连接(例如来自 LAN 上的其他主机)都将不起作用,除非您添加更高优先级的规则来豁免该特定流量。
要使用此方法,请在后端主机上为要使用 WireGuard 隧道的每个容器的 IP 地址(或网络)添加策略路由规则。例如,如果您的 Docker Web 服务器的 IP 地址为 ,172.17.0.2
而您的 Docker 邮件服务器的 IP 地址为172.17.0.3
,请添加以下两条规则:
ip rule add from 172.17.0.2 table vpsrt priority 2
ip rule add from 172.17.0.3 table vpsrt priority 3
172.17.0.0/16
或者,如果您希望通过 WireGuard 隧道路由整个 Docker 网络的所有流量,则可以只添加一条规则172.17.0.0/16
,而不是为各个容器添加单独的规则:
ip rule add from 172.17.0.0/16 table vpsrt priority 2
2. 标记 WireGuard 连接并添加回复流量规则
这是一种更复杂的方法,但好处是它只适用于通过 WireGuard 隧道发起的入站连接 - 而所有其他流量都不会受到干扰。
您需要了解的背景是,您可以将一个任意的 32 位“标记”值应用于单个数据包(又名 fwmark,又名防火墙标记,又名 nfmark,又名 netfilter 标记,又名数据包标记);并将单独的“标记”值应用于双向连接流(又名 ctmark,又名 connmark,又名连接标记)。Linux 策略路由仅与数据包标记交互;但您还需要使用连接标记来跟踪哪些数据包是对最初通过 WireGuard 隧道发起的入站连接的回复。
这种方法的第一步是使用连接标记来标记新连接1
何时首次通过 WireGuard 隧道进入后端主机:
iptables -t mangle -A PREROUTING -i wg0 -m state --state NEW -j CONNMARK --set-mark 1
第二步是,1
当连接标记为时,将数据包标记为1
,并且数据包没有通过 WireGuard 接口进入(即来自 Docker 网络的回复数据包):
iptables -t mangle -A PREROUTING ! -i wg0 -m connmark --mark 1 -j MARK --set-mark 1
1
第三步是使用自定义表路由带有数据包标记的数据包vpsrt
(它将把它们发送出 WireGuard 隧道):
ip rule add fwmark 1 table vpsrt priority 2
标记值1
完全是任意的 - 您可以使用任何 32 位值(并且可以对连接标记和数据包标记使用不同的值);请记住,每个连接或数据包只有一个标记字段,因此如果您需要为其他目的标记其他连接/数据包,则需要使用不同的标记值来区分它们。