用 IPv6 承载 WireGuard,把公网 IPv4 全端口 DNAT 到内网服务器

道锋潜鳞
2026-06-22 / 0 评论 / 1 阅读 / 正在检测是否收录...

用 IPv6 承载 WireGuard,把公网 IPv4 全端口 DNAT 到内网服务器

本文记录一次真实搭建思路,但所有公网地址、域名和密钥均已替换为文档示例值。请不要直接复制示例密钥;生产环境应重新生成。

背景

手里有一台物理服务器,位于内网或弱公网环境,但它有稳定的 IPv6 出口。另有一台云服务器,拥有公网 IPv4 和 IPv6。目标是:

  • 公网 IPv4 的 TCP、UDP、ICMP 尽量全部转发到物理服务器;
  • 物理服务器上的服务能看到真实客户端 IP,而不是中转机 IP;
  • 物理服务器自己的默认公网出口不被 WireGuard 接管;
  • WireGuard 控制面走 IPv6,这样公网 IPv4 的端口可以完整留给 DNAT;
  • 不影响机器上已有的 Tailscale、Docker、libvirt 等网络。

最终架构如下:

IPv4 客户端
    |
    | 访问 203.0.113.10:任意端口
    v
双栈云服务器
    IPv4: 203.0.113.10
    IPv6: 2001:db8:100::10
    wg0: 10.88.0.1/24
    |
    | WireGuard over IPv6: [2001:db8:100::10]:60000/udp
    v
物理服务器
    IPv6: 2001:db8:200::20
    wg-sh: 10.88.0.2/32

公网 IPv4 入站路径:

客户端真实 IP -> 203.0.113.10 -> DNAT -> 10.88.0.2 -> 物理服务器服务

物理服务器回包路径:

源地址 10.88.0.2 的回包 -> 策略路由 -> wg-sh -> 云服务器 -> conntrack 反向 NAT -> 客户端

为什么用 IPv6 承载 WireGuard

如果 WireGuard 监听在公网 IPv4 上,比如 203.0.113.10:51820/udp,那么这个端口必须从“全端口 DNAT”里排除。SSH 管理端口也通常要排除,否则容易把自己锁出去。

但如果云服务器和物理服务器之间能用 IPv6 通信,就可以让 WireGuard endpoint 走 IPv6:

[2001:db8:100::10]:60000/udp

这样公网 IPv4 的全部端口都可以 DNAT 到后端物理机。云服务器本身的管理也改走 IPv6 SSH。

注意:Linux WireGuard 的 ListenPort 不是严格绑定某个地址,通常会显示同时监听 IPv4 和 IPv6:

0.0.0.0:60000
[::]:60000

这不影响方案。客户端明确用 IPv6 endpoint 连接即可,公网 IPv4 侧仍可被 DNAT 规则处理。

纯 IPv4 也可以实现

如果没有 IPv6,也可以在公网 IPv4 上实现同样的 DNAT 架构。区别是:不能做到字面意义上的“一个端口不留”,因为中转机自己至少需要保留两个本机入口:

SSH 管理端口,例如 22/tcp 或 2222/tcp
WireGuard 端口,例如 60000/udp

也就是说,IPv4-only 版本的架构是:

公网 IPv4 大部分端口 -> DNAT -> 物理服务器
公网 IPv4 SSH 管理端口 -> 留给中转机
公网 IPv4 WireGuard 端口 -> 留给中转机

WireGuard 客户端配置里的 endpoint 变成公网 IPv4:

[Peer]
PublicKey = <云服务器公钥>
Endpoint = 203.0.113.10:60000
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25

nftables 规则也要排除保留端口。比如保留 22/tcp 给 SSH,保留 60000/udp 给 WireGuard:

table ip wg_dnat {
  chain prerouting {
    type nat hook prerouting priority dstnat; policy accept;

    iifname "eth0" ct state new ip protocol tcp tcp dport != 22 dnat to 10.88.0.2
    iifname "eth0" ct state new ip protocol udp udp dport != 60000 dnat to 10.88.0.2
    iifname "eth0" ct state new ip protocol icmp dnat to 10.88.0.2
  }
}

如果中转机还有面板、监控或备用 SSH 端口,也要继续排除:

iifname "eth0" ct state new ip protocol tcp tcp dport != { 22, 2222, 8443 } dnat to 10.88.0.2
iifname "eth0" ct state new ip protocol udp udp dport != { 60000 } dnat to 10.88.0.2

物理服务器一侧的策略路由不变,仍然是:

from 10.88.0.2 lookup 60000
table 60000 default dev wg-sh

所以结论是:

IPv6 承载 WG:公网 IPv4 可以最接近完整全端口 DNAT
IPv4 承载 WG:同样能做,但必须保留 SSH 和 WG 等本机端口

地址规划

示例规划:

云服务器公网 IPv4: 203.0.113.10
云服务器公网 IPv6: 2001:db8:100::10
云服务器 WG 地址: 10.88.0.1/24

物理服务器公网 IPv6: 2001:db8:200::20
物理服务器 WG 地址: 10.88.0.2/32

WireGuard 端口: 60000/udp over IPv6
策略路由表: 60000

入口地域选择

这类架构的延迟不是只看“用户到云服务器”的 ping,而是两段相加:

用户 -> 入口云服务器
入口云服务器 -> 物理服务器

如果第二段很差,DNAT 后的实际体验会明显变差。比如同样是双栈轻量服务器,某个华东入口到物理机的 WG RTT 可能在三十多毫秒,而华南入口到物理机只有二十毫秒左右。即使用户到两个入口的纯 ICMP 差距不大,最终业务流量也可能是华南入口更快。

建议测试时不要只测:

ping 入口公网 IP

还要在物理服务器上测:

ping -c 100 入口公网 IP
mtr -rwzc 100 入口公网 IP

并在完整 DNAT 链路搭好后,从真实用户网络访问业务端口。最终以业务侧体验为准。对于游戏服、语音、远程桌面这类延迟敏感服务,入口到物理机这一段尤其关键。

如果只能保留一个入口,选择标准可以简单粗暴一点:

真实业务延迟更低
晚高峰不丢包
能跑满物理机带宽
IPv6 到物理机稳定

地理位置只是参考,实测链路质量才是答案。

云服务器配置

系统以 Debian 12 为例。

安装工具:

apt-get update
apt-get install -y wireguard wireguard-tools nftables

生成服务端密钥:

install -d -m 700 /etc/wireguard
umask 077
wg genkey > /etc/wireguard/server_private.key
wg pubkey < /etc/wireguard/server_private.key > /etc/wireguard/server_public.key

创建 /etc/wireguard/wg0.conf

[Interface]
Address = 10.88.0.1/24
ListenPort = 60000
PrivateKey = <云服务器私钥>
SaveConfig = false

[Peer]
# physical server
PublicKey = <物理服务器公钥>
AllowedIPs = 10.88.0.2/32

启动并设置开机自启:

systemctl enable wg-quick@wg0
systemctl restart wg-quick@wg0

开启 IPv4 转发,并确保重启后仍然生效。很多云镜像会在 /etc/sysctl.conf/etc/sysctl.d/99-sysctl.conf 里写死 net.ipv4.ip_forward = 0,需要确认最终结果:

sed -i 's/^net.ipv4.ip_forward.*/net.ipv4.ip_forward = 1/' /etc/sysctl.conf
cat > /etc/sysctl.d/zz-99-wg-dnat-forwarding.conf <<'EOF'
net.ipv4.ip_forward = 1
EOF
sysctl --system
sysctl net.ipv4.ip_forward

期望输出:

net.ipv4.ip_forward = 1

配置 nftables 做 IPv4 全端口 DNAT。写入 /etc/nftables.conf

#!/usr/sbin/nft -f

flush ruleset

table ip wg_dnat {
  chain prerouting {
    type nat hook prerouting priority dstnat; policy accept;

    iifname "eth0" ct state new ip protocol tcp dnat to 10.88.0.2
    iifname "eth0" ct state new ip protocol udp dnat to 10.88.0.2
    iifname "eth0" ct state new ip protocol icmp dnat to 10.88.0.2
  }
}

加载并开机自启:

nft -c -f /etc/nftables.conf
nft -f /etc/nftables.conf
systemctl enable nftables
systemctl restart nftables

如果希望云服务器自己的出站连接优先使用 IPv6,可以在 /etc/gai.conf 中提高 IPv6 优先级:

# BEGIN managed: prefer IPv6 outbound
label ::1/128       0
label ::/0          1
label 2002::/16     2
label ::/96         3
label ::ffff:0:0/96 4
precedence ::1/128       50
precedence ::/0          100
precedence 2002::/16     30
precedence ::/96         20
precedence ::ffff:0:0/96 10
# END managed: prefer IPv6 outbound

验证:

systemctl is-enabled wg-quick@wg0
systemctl is-active wg-quick@wg0
systemctl is-enabled nftables
systemctl is-active nftables
wg show wg0
nft list ruleset
sysctl net.ipv4.ip_forward

物理服务器配置

物理服务器上只新增一个独立 WireGuard 接口,例如 wg-sh。不要让 WireGuard 接管默认路由,不要碰 Tailscale 的规则和表。

安装 WireGuard:

apt-get update
apt-get install -y wireguard wireguard-tools

生成客户端密钥:

install -d -m 700 /etc/wireguard
umask 077
wg genkey > /etc/wireguard/wg-sh_private.key
wg pubkey < /etc/wireguard/wg-sh_private.key > /etc/wireguard/wg-sh_public.key

创建 /etc/wireguard/wg-sh.conf

[Interface]
Address = 10.88.0.2/32
PrivateKey = <物理服务器私钥>
Table = off
MTU = 1420
PostUp = ip route replace default dev wg-sh table 60000
PostUp = ip rule add from 10.88.0.2/32 table 60000 priority 10000 2>/dev/null || true
PostDown = ip rule del from 10.88.0.2/32 table 60000 priority 10000 2>/dev/null || true
PostDown = ip route del default dev wg-sh table 60000 2>/dev/null || true

[Peer]
PublicKey = <云服务器公钥>
Endpoint = [2001:db8:100::10]:60000
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25

这里最关键的是:

Table = off

这会阻止 wg-quick 自动把默认路由改到 WireGuard。

真正用于回包的路由由 PostUp 添加:

ip route replace default dev wg-sh table 60000
ip rule add from 10.88.0.2/32 table 60000 priority 10000

意思是:

普通流量:继续走物理机原来的默认网卡
源地址为 10.88.0.2 的流量:走 wg-sh

启动并设置开机自启:

systemctl enable wg-quick@wg-sh
systemctl restart wg-quick@wg-sh

验证:

systemctl is-enabled wg-quick@wg-sh
systemctl is-active wg-quick@wg-sh
wg show wg-sh
ip -4 rule show
ip -4 route show table 60000

期望能看到:

10000: from 10.88.0.2 lookup 60000
default dev wg-sh scope link

同时确认普通出站没有被接管:

ip -4 route get 223.5.5.5

期望它仍然走原来的物理网卡,例如:

223.5.5.5 via 192.0.2.1 dev eth0 src 192.0.2.20

再确认 DNAT 回包会走 WireGuard:

ip -4 route get 8.8.8.8 from 10.88.0.2

期望:

8.8.8.8 from 10.88.0.2 dev wg-sh table 60000

为什么不做 SNAT

很多端口转发教程会写:

masquerade

这个方案里不要这么做。

如果云服务器对转发流量做 SNAT,物理服务器看到的源 IP 就会变成云服务器的 WG 地址,而不是真实客户端 IP。

本方案只做 DNAT:

目标地址:203.0.113.10 -> 10.88.0.2
源地址:保持真实客户端 IP 不变

回包交给物理服务器的策略路由和云服务器 conntrack 自动处理。

ICMP、TCP、UDP 都可以转发

nftables 规则里分别写了:

ip protocol tcp
ip protocol udp
ip protocol icmp

所以:

  • 访问公网 IPv4 的 TCP 端口会到物理服务器;
  • UDP 端口也会到物理服务器;
  • ping 203.0.113.10 实际会 ping 到物理服务器。

如果只想转发 ping 请求,可以把 ICMP 规则改窄:

iifname "eth0" ct state new icmp type echo-request dnat to 10.88.0.2

和 Tailscale 共存

如果物理服务器上已有 Tailscale,常见会有类似规则:

5210: from all fwmark 0x80000/0xff0000 lookup main
5230: from all fwmark 0x80000/0xff0000 lookup default
5250: from all fwmark 0x80000/0xff0000 unreachable
5270: from all lookup 52

不要改这些规则。

本方案使用:

10000: from 10.88.0.2 lookup 60000

它只匹配源地址为 10.88.0.2 的回包,不影响 Tailscale 自己的 tailscale0、fwmark 和 table 52。

Tailscale 可以继续作为管理和救援通道,WireGuard DNAT 作为高性能公网入口。

验证真实客户端 IP

可以在物理服务器上临时监听一个端口:

python3 -m http.server 22000 --bind 0.0.0.0

然后从外部访问:

curl http://203.0.113.10:22000/

更直观的方式是查看服务日志或用 ss/应用日志确认远端地址。真实效果应该是:

remote=<客户端真实公网 IP>
local=10.88.0.2:22000

也可以用 SSH 端口验证。用 HTTP 客户端访问 SSH 端口时,物理服务器 sshd 日志会出现类似:

banner exchange: Connection from <客户端真实公网 IP> port <端口>: invalid format

这说明连接确实已经转发到物理服务器,而且源 IP 没有被 SNAT。

实际业务也可以直接验证。比如 Minecraft 服务端日志里,如果玩家通过公网入口连接,服务端日志应该类似:

player_name[/198.51.100.25:13096] logged in with entity id ...

这里的 198.51.100.25 应该是玩家真实公网 IP,而不是:

10.88.0.1
10.88.0.2
云服务器内网地址

看到真实公网 IP,基本就能确认:

DNAT 没有做 SNAT
物理服务器回程策略路由正确
云服务器 conntrack 正常完成了反向 NAT

如果业务日志里看到的是 WG 地址或中转机地址,说明你大概率加了 SNAT/MASQUERADE,或者用了四层/七层代理而不是纯 DNAT。

多入口扩展

如果后续增加第二台入口机,例如广州、上海、北京各一台,建议不要共用同一个 WG 地址。可以这样规划:

上海入口:
  server wg: 10.88.0.1
  physical wg-sh: 10.88.0.2
  table 60000

广州入口:
  server wg: 10.88.1.1
  physical wg-gz: 10.88.1.2
  table 60001

这样从哪个入口进来的连接,就从对应入口回去,避免非对称路由。

一个公网 IPv4 的同一协议同一端口只能 DNAT 给一台后端。如果想让多台物理机共享同一个 IPv4,只能:

  • 按端口段切分;
  • 80/443 用反代按域名分流;
  • 按源地址或地区分流;
  • 或者购买更多公网 IPv4/入口机。

常见坑

1. 只配 DNAT,不配回程策略路由

这样物理服务器会看到真实客户端 IP,但回包走默认网卡出去,客户端会认为连接异常。

必须有:

ip rule add from 10.88.0.2/32 table 60000
ip route add default dev wg-sh table 60000

2. AllowedIPs 写得太窄

物理服务器客户端的 peer 需要:

AllowedIPs = 0.0.0.0/0

配合 Table = off 使用。这样不会接管默认路由,但允许任意公网客户端的回包进入 WireGuard。

3. ip_forward 被系统配置覆盖

有些镜像会在多个 sysctl 文件里写:

net.ipv4.ip_forward = 0

要跑:

sysctl --system
sysctl net.ipv4.ip_forward

确认最终值确实是 1。

4. IPv4 SSH 被转走后失去入口机管理

全端口 DNAT 后,203.0.113.10:22 会转到物理服务器。入口机管理要走:

ssh -6 root@2001:db8:100::10

在执行全端口 DNAT 前,务必确认 IPv6 SSH 可用。

5. 云厂商安全组

系统里配置好了不代表云安全组放通了。至少要确认:

  • IPv6 的 60000/udp 放通给物理服务器;
  • IPv6 SSH 放通给管理端;
  • IPv4 入站端口按需求放通。

最终效果

完成后得到的是:

IPv4 用户 -> 云服务器 IPv4 全端口 -> IPv6 WireGuard -> 物理服务器
IPv6 用户 -> 可直接访问物理服务器 IPv6
入口机管理 -> IPv6 SSH
物理机管理 -> Tailscale 或其他救援通道

这种方案的好处是:

  • IPv4 全端口几乎完整交给物理服务器;
  • 物理服务器能看到真实客户端 IP;
  • 物理服务器默认出口不受影响;
  • WireGuard 控制面不占 IPv4 端口;
  • 可以和 Tailscale 并行使用;
  • 后续可以横向增加多个地域入口。

如果入口到物理服务器的 IPv6 链路质量好,这种架构的延迟和带宽表现会非常接近直接公网机器,同时又保留了内网物理机的灵活性。

0

评论 (0)

取消