无聊的时候对一些新出的模型提了一个很久之前困扰我网络折腾上的难题。
题目如下:
我有一个需求,就是通过有公网IPv4的服务器+wireguard VPN将NAT里面服务器的端口暴露出去,并且能够让NAT服务器的服务能看得到请求客户端原始的IP,并且没有Proxy Protocol
第二题:
但是我希望NAT服务器对外面的请求(比如说Google或者Docker)还是走原来的Gateway,而不是被公网服务器全局代理,怎么改?
参与的模型如下:
Claude Opus 4.7 (Adaptive Thinking) (网页聊天 - 无痕)
Claude Sonnet 4.6 (Adaptive Thinking) (网页聊天 - 无痕)
GPT 5.5 (Cherry Studio 自动 Thinking) (官方API)
DeepSeek v4 Flash (Cherry Studio 自动 Thinking) (官方API)
GLM 5.1 (Cherry Studio 自动 Thinking) (官方API)
Deepseek V4 Pro (Cherry Studio 自动 Thinking) (官方API)
*为了避免Memory的影响就给Claude开了无痕模式
先说结果:两个Claude模型给出的回答质量最优;GPT 5.5给的方案第一题正确,第二题方案可用,但次于Claude;DS v4 Flash答对了第一题,但败在了第二题;剩下的两个模型第一题都做出问题了,并且错的方向相同。
Claude 给我的答案最满意,两个模型都在我问出第二个问题之前就已经给出了解决方案。Sonnet是在回答最后提了问2的解决方案,Opus是将问2的解决方案作为"必需操作"和问1的答案写在一起的。并且都是使用fwmark来分开VPN出站和默认网关出站数据包的,这点令我很意外和满意,因为对UDP更友好,并且Opus还强调了方案对UDP连接可能存在的问题。
GPT 5.5 给的方案中规中矩,第二题答案在我提出之后才补上来,使用的是根据源IP查找路由表的方案。能行,是我之前自己想到的方案,但是对UDP很不友好。
Deepseek Flash模型反而答对了一题是我没想到的
Deepseek v4 Pro和GLM 5.1犯了类似的错误,将AllowedIPs填写成了Wireguard服务器的内网IP或者WG的内网网段,即使提示了也不悔改,看起来是对Wireguard不熟悉导致的,但是思路基本上没大问题。
各个模型回答如下:
Opus 4.7:
这是一个比较典型的"反向暴露 + 不丢真实客户端 IP + 不引入应用层协议改造"的场景。我先讲思路,再给配置,最后说我哪些地方比较确定、哪些是推测。
## 为什么常见做法不行
最常见的端口转发是 **DNAT + SNAT/MASQUERADE**。但 SNAT 会把源地址改成公网服务器的 wg 地址,后端只能看到 `10.x.x.x`,丢了真实 IP——直接排除。
## 核心思路:只做 DNAT,由后端用策略路由把回包送回 wg
只在公网服务器做 DNAT、**不做 SNAT**,到达后端时源 IP 就是客户端真实 IP。但带来一个问题:后端回包时,目标是公网客户端 IP,默认走的是它自己的 NAT 网关,结果就是非对称路由——客户端永远收不到响应。
解决办法:在**后端**做策略路由,把"从 wg0 进来的连接"的回包也强制从 wg0 出去,再由公网服务器把响应转回客户端。
---
## 具体配置
假设:
- 公网服务器:公网 IP `5.6.7.8`,wg0 = `10.0.0.1`
- 后端:wg0 = `10.0.0.2`,服务监听 80
- 目标:把公网 80 端口暴露给互联网
### 1. 公网服务器
WireGuard 正常配置(peer 的 `AllowedIPs = 10.0.0.2/32`),然后:
```bash
sysctl -w net.ipv4.ip_forward=1
# 只 DNAT,不要 MASQUERADE
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 \
-j DNAT --to-destination 10.0.0.2:80
iptables -A FORWARD -i eth0 -o wg0 -p tcp -d 10.0.0.2 --dport 80 -j ACCEPT
iptables -A FORWARD -i wg0 -o eth0 -m conntrack \
--ctstate ESTABLISHED,RELATED -j ACCEPT
```
⚠️ **不要**对这条转发流加 MASQUERADE。如果你已经有给后端做出网用的 `MASQUERADE`,要用 `! -d 10.0.0.0/24` 之类把这种情况排除掉。
### 2. 后端
WireGuard 配置里,公网服务器作为 peer 的 `AllowedIPs` 必须包含所有可能客户端 IP,最简单是写 `0.0.0.0/0`——因为 WireGuard 的 cryptokey routing **既是路由也是过滤**,源 IP 不在 AllowedIPs 里的包会被静默丢弃(这是最容易踩的坑)。
但 `AllowedIPs = 0.0.0.0/0` 默认会让 wg-quick 把所有出网流量丢进 wg0,这不是我们想要的,所以:
```ini
[Interface]
PrivateKey = ...
Address = 10.0.0.2/24
Table = off # 关闭自动路由
[Peer]
PublicKey = ...
Endpoint = 5.6.7.8:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
```
然后手动只加 wg 子网的路由:
```bash
ip route add 10.0.0.0/24 dev wg0
```
接下来是策略路由部分:
```bash
# 自定义路由表
echo "100 wgback" >> /etc/iproute2/rt_tables
ip route add default via 10.0.0.1 dev wg0 table 100
# 给从 wg0 进来的"新连接"打 connmark
iptables -t mangle -A PREROUTING -i wg0 -m conntrack --ctstate NEW \
-j CONNMARK --set-mark 1
# 本机产生的回包恢复这个 mark(回包走 OUTPUT 不走 FORWARD)
iptables -t mangle -A OUTPUT -m conntrack ! --ctstate NEW \
-j CONNMARK --restore-mark
# 带 mark 的包查 100 表
ip rule add fwmark 1 table 100
# 必须放宽反向路径校验,否则 src=客户端公网IP 的包会被丢
sysctl -w net.ipv4.conf.all.rp_filter=2
sysctl -w net.ipv4.conf.wg0.rp_filter=2
```
到这一步,后端服务在 access log / `tcpdump -i wg0` 里看到的就是真实客户端 IP 了,不需要 PROXY protocol、不需要应用支持。
---
## 我比较确定的几点
1. **DNAT 不带 SNAT 是这个需求的必经之路**,没有 PROXY protocol 又要保留 IP,几乎只能在 L3 做。
2. **`rp_filter` 必须放宽**——这是最容易让人调一下午找不到原因的配置。
3. **CONNMARK 在 OUTPUT 链恢复**,因为回包是本机生成的,不走 FORWARD。
4. **WireGuard 的 `AllowedIPs` 是过滤器**,不只是路由表项。
## 我不太确定 / 需要你根据实际情况调的部分(推测)
- **后端原本如果还需要主动出网**:上面的 `Table = off` + 只加 wg 子网路由的写法,理论上不会影响后端原有的默认路由。但如果你后端原本路由表/iptables 比较复杂(比如有 docker、k8s、其它 VPN),可能会有相互干扰,需要单独看。
- **UDP 服务**:上面规则对 TCP 一定有效,UDP 因为没有真正的连接,依赖 conntrack 的"伪连接"。一般也工作,但如果你的 UDP 流是长时间且稀疏的(比如 WireGuard 套 WireGuard、某些游戏协议),可能会因为 conntrack 老化偶发问题,这种情况要再调 `nf_conntrack_udp_timeout*`。
- **多后端 / 多端口**:如果公网服务器要给多个后端做反代,上面 iptables 规则的范围、CONNMARK 的 mark 值需要细分,避免不同流向的回包互相串。
- **`OUTPUT` 链 mangle 之后内核重新做路由决策**:我记得 Linux 在 mark 改变后会重算 `ip rule`,所以 connmark restore 之后可以走到 table 100。这个行为我有印象但不是 100% 笃定,如果实测发现回包还是从默认网关走,可以加一句 `ip route flush cache` 或考虑用 `mangle PREROUTING` + 在转发场景下做(但这就要求是 forward 流量了)。
## 顺带提的几个替代方案
- **直接让后端默认路由走 wg0**:最简单,不需要策略路由,但后端**所有**出网流量都从公网服务器走,带宽和隐私都受影响。
- **nftables**:上面 iptables 写法可以一一翻成 nft,逻辑完全等价,更现代些。
- **现成工具**:rathole / frp / gost 这类反向穿透工具,要么用 PROXY protocol,要么本质上还是 SNAT,看你的描述都不符合要求,所以我没推荐。
如果你能告诉我:协议是 TCP 还是 UDP、应用是什么(HTTP / SSH / 数据库 / 游戏服)、以及后端是否需要"既被反代又自己访问外网",我可以把上面的规则进一步收紧或简化。
(Sonnet因为使用的是隐私模式忘记复制保存了…)
GPT 5.5:
可以实现。核心思路是:
> 公网 IPv4 服务器只做 **DNAT 端口转发**,不要做 SNAT/MASQUERADE;
> NAT 内服务器通过 WireGuard 收到连接后,回包必须走 WireGuard 回公网服务器;
> 这样后端服务看到的 TCP/UDP 源地址就是客户端真实 IP,不需要 Proxy Protocol。
下面给一个典型方案。
---
## 1. 网络假设
公网服务器:
```text
公网 IP:203.0.113.10
公网网卡:eth0
WireGuard IP:10.66.66.1
```
NAT 内服务器:
```text
WireGuard IP:10.66.66.2
后端服务监听:10.66.66.2:8080 或 0.0.0.0:8080
```
目标:
```text
客户端访问 203.0.113.10:80
实际转发到 NAT 内服务器 10.66.66.2:8080
后端服务看到客户端真实 IP
```
---
## 2. WireGuard 配置
### 公网服务器 `/etc/wireguard/wg0.conf`
```ini
[Interface]
Address = 10.66.66.1/24
ListenPort = 51820
PrivateKey = 公网服务器私钥
[Peer]
PublicKey = NAT内服务器公钥
AllowedIPs = 10.66.66.2/32
```
启动:
```bash
wg-quick up wg0
```
---
### NAT 内服务器 `/etc/wireguard/wg0.conf`
重点是:
1. `AllowedIPs = 0.0.0.0/0`
2. `Table = off`
3. 自己手动加策略路由,只让源地址是 `10.66.66.2` 的回包走 WireGuard
```ini
[Interface]
Address = 10.66.66.2/32
PrivateKey = NAT内服务器私钥
Table = off
PostUp = ip rule add from 10.66.66.2/32 table 100
PostUp = ip route add default dev wg0 table 100
PostDown = ip rule del from 10.66.66.2/32 table 100
PostDown = ip route del default dev wg0 table 100
[Peer]
PublicKey = 公网服务器公钥
Endpoint = 203.0.113.10:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
```
启动:
```bash
wg-quick up wg0
```
为什么 NAT 内服务器这里要用 `AllowedIPs = 0.0.0.0/0`?
因为后端服务器回包的目标地址是任意公网客户端 IP,例如:
```text
源地址:10.66.66.2
目标地址:客户端真实 IP
```
WireGuard 需要允许这些目标地址通过隧道发给公网服务器。
---
## 3. 公网服务器开启 IPv4 转发
在公网服务器上执行:
```bash
sysctl -w net.ipv4.ip_forward=1
```
持久化:
```bash
echo 'net.ipv4.ip_forward=1' > /etc/sysctl.d/99-ip-forward.conf
sysctl --system
```
建议关闭 rp_filter,避免策略路由/隧道场景下丢包:
```bash
cat > /etc/sysctl.d/99-wg-rpfilter.conf <<EOF
net.ipv4.conf.all.rp_filter=0
net.ipv4.conf.default.rp_filter=0
net.ipv4.conf.eth0.rp_filter=0
net.ipv4.conf.wg0.rp_filter=0
EOF
sysctl --system
```
NAT 内服务器上也建议关闭:
```bash
cat > /etc/sysctl.d/99-wg-rpfilter.conf <<EOF
net.ipv4.conf.all.rp_filter=0
net.ipv4.conf.default.rp_filter=0
net.ipv4.conf.wg0.rp_filter=0
EOF
sysctl --system
```
---
## 4. 公网服务器做 DNAT,不要做 SNAT
在公网服务器上执行:
```bash
iptables -t nat -A PREROUTING \
-i eth0 \
-p tcp --dport 80 \
-j DNAT --to-destination 10.66.66.2:8080
```
允许转发:
```bash
iptables -A FORWARD \
-i eth0 -o wg0 \
-p tcp -d 10.66.66.2 --dport 8080 \
-m conntrack --ctstate NEW,ESTABLISHED,RELATED \
-j ACCEPT
iptables -A FORWARD \
-i wg0 -o eth0 \
-m conntrack --ctstate ESTABLISHED,RELATED \
-j ACCEPT
```
注意:不要添加这种规则:
```bash
iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE
```
否则后端服务看到的源 IP 会变成公网服务器的 WireGuard IP,例如 `10.66.66.1`,客户端真实 IP 就丢了。
---
## 5. NAT 内服务器上的服务监听
后端服务需要监听 WireGuard 地址,或者监听所有地址。
例如服务监听:
```text
0.0.0.0:8080
```
或者:
```text
10.66.66.2:8080
```
不要只监听 `127.0.0.1:8080`,否则公网服务器转发过来的流量到不了服务。
---
## 6. 数据流解释
客户端访问:
```text
客户端真实 IP:50000 -> 203.0.113.10:80
```
公网服务器 DNAT 后:
```text
客户端真实 IP:50000 -> 10.66.66.2:8080
```
这个包通过 WireGuard 到达 NAT 内服务器。
NAT 内服务器上的服务看到的是:
```text
源 IP:客户端真实 IP
目标 IP:10.66.66.2
目标端口:8080
```
服务回包:
```text
10.66.66.2:8080 -> 客户端真实 IP:50000
```
由于 NAT 内服务器配置了策略路由:
```bash
ip rule add from 10.66.66.2/32 table 100
ip route add default dev wg0 table 100
```
所以这个回包会走 WireGuard 回到公网服务器。
公网服务器上的 conntrack 会把回包反向 NAT 成:
```text
203.0.113.10:80 -> 客户端真实 IP:50000
```
最终客户端认为自己一直在和 `203.0.113.10:80` 通信,而后端服务也能看到客户端真实 IP。
---
## 7. UDP 端口也可以
例如把公网服务器的 UDP 443 转发到 NAT 内服务器的 UDP 443:
```bash
iptables -t nat -A PREROUTING \
-i eth0 \
-p udp --dport 443 \
-j DNAT --to-destination 10.66.66.2:443
iptables -A FORWARD \
-i eth0 -o wg0 \
-p udp -d 10.66.66.2 --dport 443 \
-m conntrack --ctstate NEW,ESTABLISHED,RELATED \
-j ACCEPT
iptables -A FORWARD \
-i wg0 -o eth0 \
-m conntrack --ctstate ESTABLISHED,RELATED \
-j ACCEPT
```
---
## 8. 常见问题排查
### 后端服务看到的 IP 是 `10.66.66.1`
说明公网服务器做了 SNAT/MASQUERADE。
检查:
```bash
iptables -t nat -S
```
如果有类似规则:
```bash
-A POSTROUTING -o wg0 -j MASQUERADE
```
需要删除。
---
### NAT 内服务器能收到包,但客户端连接超时
一般是回程路由不对。
在 NAT 内服务器上检查:
```bash
ip rule
ip route show table 100
```
应当有:
```text
from 10.66.66.2 lookup 100
default dev wg0 table 100
```
同时确认 NAT 内服务器的 WireGuard peer 配置里有:
```ini
AllowedIPs = 0.0.0.0/0
```
---
### 公网服务器收不到客户端请求
检查:
1. 云厂商安全组是否放行端口
2. 系统防火墙是否放行端口
3. iptables/nftables 规则是否正确
4. 服务端口是否监听
可以抓包:
公网服务器:
```bash
tcpdump -ni eth0 tcp port 80
tcpdump -ni wg0 tcp port 8080
```
NAT 内服务器:
```bash
tcpdump -ni wg0 tcp port 8080
```
如果在 NAT 内服务器上看到:
```text
客户端真实 IP > 10.66.66.2.8080
```
就说明原始客户端 IP 已经保留下来了。
---
## 9. 总结
你要的方案是可行的,关键点是:
```text
公网服务器:DNAT,不要 SNAT
NAT 内服务器:基于源地址的策略路由,让回包走 WireGuard
WireGuard:NAT 内服务器 peer 的 AllowedIPs 使用 0.0.0.0/0
服务:监听 wg0 地址或 0.0.0.0
```
这样后端服务无需 Proxy Protocol,也能直接看到客户端真实公网 IP。
第二题:
你这个需求正好应该用:
```ini
Table = off
```
加:
```bash
ip rule add from 10.66.66.2/32 table 100
ip route add default dev wg0 table 100
```
也就是说:
> NAT 内服务器普通访问 Google、Docker Hub、apt/yum 等,仍然走原来的默认网关;
> 只有“源地址是 WireGuard IP 10.66.66.2”的流量,才走 WireGuard 回公网服务器。
---
## 推荐配置方式
假设 NAT 内服务器:
```text
内网网卡:eth0
内网地址:192.168.1.50
原默认网关:192.168.1.1
WireGuard 地址:10.66.66.2
```
### NAT 内服务器 `/etc/wireguard/wg0.conf`
```ini
[Interface]
Address = 10.66.66.2/32
PrivateKey = NAT内服务器私钥
Table = off
PostUp = ip rule add from 10.66.66.2/32 table 100
PostUp = ip route add default dev wg0 table 100
PostDown = ip rule del from 10.66.66.2/32 table 100
PostDown = ip route del default dev wg0 table 100
[Peer]
PublicKey = 公网服务器公钥
Endpoint = 公网服务器IP:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
```
重点就是这几个地方:
```ini
Table = off
AllowedIPs = 0.0.0.0/0
```
和:
```bash
ip rule add from 10.66.66.2/32 table 100
ip route add default dev wg0 table 100
```
---
## 为什么 `AllowedIPs = 0.0.0.0/0` 不等于全局代理?
很多人会误解这一点。
在 WireGuard 里:
```ini
AllowedIPs = 0.0.0.0/0
```
表示:
> 如果某个包被路由进 wg0,并且目标 IP 是任意 IPv4 地址,都允许发给这个 Peer。
但它本身不一定会修改系统默认路由。
真正让系统全局走 WireGuard 的,是 `wg-quick` 自动添加默认路由。
例如如果你没有写:
```ini
Table = off
```
那么 `wg-quick` 看到:
```ini
AllowedIPs = 0.0.0.0/0
```
可能会自动添加默认路由,让整台机器走 WireGuard。
但你写了:
```ini
Table = off
```
之后,`wg-quick` 不会自动改主路由表。
所以普通流量还是走原来的默认网关。
---
## 实际路由效果
NAT 内服务器访问 Google:
```bash
curl https://www.google.com
```
这类请求通常源地址是:
```text
192.168.1.50
```
不是:
```text
10.66.66.2
```
所以不会匹配这条策略路由:
```bash
ip rule add from 10.66.66.2/32 table 100
```
它会继续走主路由表:
```bash
ip route
```
例如:
```text
default via 192.168.1.1 dev eth0
```
所以还是走原来的 NAT 网关。
---
## 端口转发流量的回包为什么会走 WireGuard?
公网客户端访问:
```text
客户端IP:50000 -> 公网服务器IP:80
```
公网服务器 DNAT 后发给 NAT 内服务器:
```text
客户端IP:50000 -> 10.66.66.2:8080
```
NAT 内服务器上的服务看到:
```text
源 IP:客户端IP
目标 IP:10.66.66.2
```
服务回包时:
```text
10.66.66.2:8080 -> 客户端IP:50000
```
这个回包的源地址是:
```text
10.66.66.2
```
所以会匹配:
```bash
ip rule add from 10.66.66.2/32 table 100
```
然后查表 100:
```bash
ip route add default dev wg0 table 100
```
于是回包通过 WireGuard 回到公网服务器。
公网服务器再通过 conntrack 反向 NAT,把响应包变成:
```text
公网服务器IP:80 -> 客户端IP:50000
```
客户端就能正常收到响应。
---
## 检查是否还在走原网关
在 NAT 内服务器上查看主路由表:
```bash
ip route
```
你应该看到类似:
```text
default via 192.168.1.1 dev eth0
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.50
```
查看策略路由:
```bash
ip rule
```
应该有类似:
```text
from 10.66.66.2 lookup 100
```
查看表 100:
```bash
ip route show table 100
```
应该是:
```text
default dev wg0 scope link
```
测试普通访问走哪里:
```bash
ip route get 8.8.8.8
```
应该看到类似:
```text
8.8.8.8 via 192.168.1.1 dev eth0 src 192.168.1.50
```
测试 WireGuard 源地址访问走哪里:
```bash
ip route get 8.8.8.8 from 10.66.66.2
```
应该看到类似:
```text
8.8.8.8 from 10.66.66.2 dev wg0 table 100
```
这就说明:
```text
普通访问外网:走原网关
端口转发回包:走 WireGuard
```
---
## 注意不要这样配置
不要让 NAT 内服务器的 WireGuard 配置变成这样:
```ini
[Interface]
Address = 10.66.66.2/32
PrivateKey = xxx
# 没有 Table = off
[Peer]
PublicKey = xxx
Endpoint = 公网服务器IP:51820
AllowedIPs = 0.0.0.0/0
```
这种情况下,`wg-quick` 很可能会把默认路由改到 WireGuard,导致整台 NAT 服务器访问外网都经过公网服务器。
你要的是:
```ini
Table = off
```
然后自己用策略路由精确控制。
---
## 如果已经被全局代理了,怎么恢复?
先看路由:
```bash
ip route
ip rule
```
如果看到默认路由被 wg0 接管,可以先重启 WireGuard:
```bash
wg-quick down wg0
```
然后修改配置,加上:
```ini
Table = off
```
再重新启动:
```bash
wg-quick up wg0
```
之后确认:
```bash
ip route get 8.8.8.8
```
应该走原来的网关,而不是 wg0。
---
## 总结
你不需要把 NAT 内服务器全局代理到公网服务器。
正确方式是:
```ini
Table = off
AllowedIPs = 0.0.0.0/0
```
再加策略路由:
```bash
ip rule add from 10.66.66.2/32 table 100
ip route add default dev wg0 table 100
```
最终效果:
```text
NAT 内服务器主动访问 Google/Docker/apt/yum:
192.168.1.50 -> 原默认网关 -> Internet
外部客户端访问公网服务器转发端口:
客户端真实 IP -> 公网服务器 DNAT -> WireGuard -> NAT 内服务
NAT 内服务回包:
10.66.66.2 -> WireGuard -> 公网服务器 -> 客户端
```
这样后端服务能看到真实客户端 IP,同时 NAT 内服务器自己的普通外网访问不会被公网服务器全局代理。
GLM 5.1:
要实现这个需求,核心在于**不进行源地址转换(No SNAT)**的端口转发,同时解决回包路由问题。
如果公网服务器只做普通的 DNAT(目标地址转换),数据包到达 NAT 服务器时,源 IP 保留了客户端的真实 IP,但 NAT 服务器回包时会按照默认路由走本地物理网关,而不是走 WireGuard 隧道,导致连接无法建立(非对称路由)。
为了在不使用 Proxy Protocol 的前提下让服务看到真实 IP,并且支持 TCP/UDP,我们需要使用 **DNAT + 策略路由** 的方案。
以下是详细的架构和配置步骤:
### 架构说明
* **公网服务器**:
* 公网 IP:`1.1.1.1`(网卡 `eth0`)
* WireGuard IP:`10.0.0.1`(网卡 `wg0`)
* **NAT 服务器**:
* 内网 IP:`192.168.1.x`(网卡 `eth0`)
* WireGuard IP:`10.0.0.2`(网卡 `wg0`)
* **目标**:客户端访问 `1.1.1.1:8080`,NAT 服务器上的服务看到请求来自客户端真实 IP,而非 `10.0.0.1`。
---
### 第一步:配置 WireGuard 隧道
WireGuard 的配置比较简单,主要是打通 `10.0.0.0/24` 网络。
**公网服务器 (`/etc/wireguard/wg0.conf`)**:
```ini
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <公网服务器私钥>
# 允许转发来自 WG 的流量
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT
[Peer]
PublicKey = <NAT服务器公钥>
AllowedIPs = 10.0.0.2/32
```
**NAT 服务器 (`/etc/wireguard/wg0.conf`)**:
```ini
[Interface]
Address = 10.0.0.2/24
PrivateKey = <NAT服务器私钥>
[Peer]
PublicKey = <公网服务器公钥>
Endpoint = 1.1.1.1:51820
AllowedIPs = 10.0.0.1/32
PersistentKeepalive = 25
```
*注意:此时 NAT 服务器的 `AllowedIPs` 只写 `10.0.0.1/32`,我们将在下一步通过策略路由来处理非 WG 网段的流量,而不是粗暴地把 `0.0.0.0/0` 写进 AllowedIPs 导致断网。*
启动两端的 WireGuard:`systemctl start wg-quick@wg0`
---
### 第二步:公网服务器配置 DNAT(只转目标,不转源)
在公网服务器上开启 IP 转发,并配置 iptables 将访问 `8080` 端口的流量转发给 NAT 服务器。
```bash
# 开启内核转发
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
sysctl -p
# DNAT:将访问公网 8080 的 TCP/UDP 流量,目标地址改为 NAT 服务器的 WG IP
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 8080 -j DNAT --to-destination 10.0.0.2:8080
iptables -t nat -A PREROUTING -i eth0 -p udp --dport 8080 -j DNAT --to-destination 10.0.0.2:8080
# 注意:这里【没有】使用 MASQUERADE 或 SNAT,所以数据包的源 IP 依然是客户端的真实 IP!
```
---
### 第三步:NAT 服务器配置策略路由(核心)
此时,NAT 服务器收到了数据包(`Src: 客户端真实IP, Dst: 10.0.0.2`),但如果它直接回包,会走默认的物理网关 `192.168.1.x`,导致回包被丢弃。我们需要让**来自 WireGuard 的请求,其回包强制走 WireGuard 隧道**。
**1. 开启 IP 转发**
```bash
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
sysctl -p
```
**2. 创建路由表和规则**
我们将使用 `fwmark`(防火墙标记)和策略路由来实现。
```bash
# 1. 给从 wg0 进入的连接打上标记(mark 0xca6c 是随便选的一个十六进制数)
iptables -t mangle -A PREROUTING -i wg0 -j MARK --set-mark 0xca6c
# 2. 添加策略路由规则:如果数据包带有此标记,则查路由表 51820
ip rule add fwmark 0xca6c table 51820
# 3. 在 51820 路由表中,将默认网关指向公网服务器的 WG IP,并强制走 wg0 接口
ip route add default via 10.0.0.1 dev wg0 table 51820
```
**3. 持久化配置(写入 WireGuard 配置文件)**
为了防止重启后失效,将上述操作写入 NAT 服务器的 `/etc/wireguard/wg0.conf`:
```ini
[Interface]
Address = 10.0.0.2/24
PrivateKey = <NAT服务器私钥>
# 标记从 wg0 进来的包
PostUp = iptables -t mangle -A PREROUTING -i wg0 -j MARK --set-mark 0xca6c
# 添加策略路由规则
PostUp = ip rule add fwmark 0xca6c table 51820
# 添加专属路由表默认路由
PostUp = ip route add default via 10.0.0.1 dev wg0 table 51820
# 关闭时清理规则
PostDown = iptables -t mangle -D PREROUTING -i wg0 -j MARK --set-mark 0xca6c
PostDown = ip rule del fwmark 0xca6c table 51820
PostDown = ip route del default via 10.0.0.1 dev wg0 table 51820
[Peer]
PublicKey = <公网服务器公钥>
Endpoint = 1.1.1.1:51820
AllowedIPs = 10.0.0.1/32
PersistentKeepalive = 25
```
重启 WireGuard 使其生效:`systemctl restart wg-quick@wg0`
---
### 第四步:验证
1. 在 NAT 服务器上启动一个简单的 HTTP 服务(例如 `python3 -m http.server 8080`)。
2. 从外部网络访问 `http://1.1.1.1:8080`。
3. 查看 NAT 服务器上的 HTTP 日志,你会看到请求的来源 IP 是客户端的**公网真实 IP**,而不是 `10.0.0.1`。
### 方案优势与总结
* **无需 Proxy Protocol**:完全在网络层(L3/L4)解决,不需要应用程序支持解析 Proxy Protocol 头。
* **支持 UDP**:与 Proxy Protocol(主要支持 TCP)不同,此方案对 TCP 和 UDP 同样有效(如游戏服务器、DNS、流媒体等)。
* **避免全局接管**:通过 `fwmark` 和路由表,精准地只将“回包流量”送入隧道,NAT 服务器自身的默认上网流量(如 `apt update`)仍然走本地物理网关,互不干扰。
* **注意**:NAT 服务器上的服务如果绑定的是 `0.0.0.0`,它会正常响应目标为 `10.0.0.2` 的请求;如果服务只绑定了内网 IP(如 `192.168.1.x`),则还需要在 NAT 服务器本地做一次 DNAT(`iptables -t nat -A PREROUTING -d 10.0.0.2 -p tcp --dport 8080 -j DNAT --to 192.168.1.x:8080`),但这会导致真实 IP 丢失。因此,**服务必须监听 `0.0.0.0` 或 `10.0.0.2`**。
Deepseek v4 Pro:
你可以通过 WireGuard 将 NAT 后的服务器与公网服务器组成一个虚拟三层网络,再利用路由和**仅做 DNAT(不改源地址)**的 iptables 规则,实现“端口暴露 + 真实客户端 IP 透传”,全程无需 Proxy Protocol。整个过程对后端服务完全透明,就像客户端直接访问内网服务器一样。
---
## 原理简述
- **A**:有公网 IP 的服务器(如 `1.2.3.4`),作为 WireGuard 中心节点和流量入口。
- **B**:NAT 内的服务器,运行实际服务(如 Web 监听 `80`),通过 WireGuard 连接到 A。
- **虚拟网络**:A 与 B 之间建立 WireGuard 隧道,分配虚拟内网 IP,例如 A=`10.0.0.1`,B=`10.0.0.2`。
- **流量路径**:
1. 客户端 C 访问 `1.2.3.4:80`。
2. A 收到包,**只修改目标 IP** 为 B 的虚拟 IP `10.0.0.2`,源 IP 保持 C 不变。
3. 包经 WireGuard 隧道到达 B,B 看到的源地址就是 C。
4. B 处理后回复,目标 IP=C。因为 B 的默认路由指向 A(隧道),回复包也走隧道回到 A。
5. A 在发出回复包前,做 **SNAT/MASQUERADE**,将源 IP 改为自己的公网 IP `1.2.3.4`,客户端 C 收到正常回复。
这样既保持了客户端真实 IP 对 B 可见,又不需要应用层代理或 Proxy Protocol。
---
## 配置步骤
### 1. 前提条件
- **A** 有公网 IPv4(假设为 `1.2.3.4`),已开启 IP 转发:
```bash
echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.conf
sysctl -p
```
- **A、B** 均已安装 WireGuard,且内核模块已加载。
- **B** 能出站连接 A 的某个 UDP 端口(下文用 `51820`)。
### 2. 建立 WireGuard 隧道
#### 2.1 生成密钥(在各自机器上)
```bash
wg genkey | tee privatekey | wg pubkey > publickey
```
#### 2.2 配置 A 的 WireGuard(`/etc/wireguard/wg0.conf`)
```ini
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <A的私钥>
# 开启转发(如果前面未全局开启,此处确保)
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT
[Peer]
# B 的节点
PublicKey = <B的公钥>
AllowedIPs = 10.0.0.2/32
```
启动:
```bash
wg-quick up wg0
systemctl enable wg-quick@wg0
```
#### 2.3 配置 B 的 WireGuard(`/etc/wireguard/wg0.conf`)
```ini
[Interface]
Address = 10.0.0.2/24
PrivateKey = <B的私钥>
# 调整路由,将默认路由指向隧道,同时保留到A公网IP的直连路由
PostUp = ip route add 1.2.3.4/32 via $(ip route show default | awk '/default/ {print $3}') dev $(ip route show default | awk '/default/ {print $5}') 2>/dev/null || true
PostUp = ip route replace default via 10.0.0.1 dev wg0
PreDown = ip route del 1.2.3.4/32 2>/dev/null || true
[Peer]
# A 的节点
PublicKey = <A的公钥>
Endpoint = 1.2.3.4:51820
AllowedIPs = 10.0.0.0/24
PersistentKeepalive = 25
```
**关键解释**:
- `AllowedIPs` 只包含虚拟网络,我们需要手动管理默认路由。
- `PostUp` 中第一步添加一条到 A 公网 IP 的主机路由,确保 WireGuard 的控制流量仍走原物理网关,不会死循环;第二步用隧道替换默认路由。
- 如果你的环境原有默认路由是动态获取,建议使用更稳健的路由策略(如 `ip rule` + 双表),但上述脚本在多数 VPS / 家庭网络下工作良好。
启动 B 的隧道:
```bash
wg-quick up wg0
systemctl enable wg-quick@wg0
```
测试:在 A 上 `ping 10.0.0.2`,在 B 上 `ping 10.0.0.1` 应能通。
### 3. 配置 NAT 与透明端口转发
在 **A 服务器**上操作:
#### 3.1 开启对 B 的出口 NAT(用于 B 访问外网及回复流量)
```bash
iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE
```
(假设 A 的公网接口是 `eth0`,按实际情况修改)
#### 3.2 添加透明转发规则(只做 DNAT,不做 SNAT)
现在要把访问 A 公网 IP 特定端口(如 TCP 80)的流量,重定向到 B 的虚拟 IP,且保留源 IP。
```bash
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j DNAT --to-destination 10.0.0.2:80
```
**注意**:不要在这种流量上添加 `MASQUERADE` 或 `SNAT`,否则源 IP 会被覆盖成 A 的内部 IP,丢失真实客户端 IP。
同时确保 FORWARD 链允许这些包(上面 `PostUp` 中的 FORWARD 规则已放行 wg0 的进出,但如果是通过 `eth0` 进入再转发到 `wg0`,需要额外允许):
```bash
iptables -A FORWARD -i eth0 -o wg0 -p tcp --dport 80 -j ACCEPT
iptables -A FORWARD -i wg0 -o eth0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
```
第一条放行入站请求从 eth0 到 wg0,第二条放行回复包从 wg0 出 eth0(利用 conntrack 状态)。
也可以直接允许所有转发,但生产环境建议细化。
#### 3.3 持久化 iptables 规则(根据系统选用)
```bash
# Debian/Ubuntu 使用 iptables-persistent
apt install iptables-persistent
netfilter-persistent save
```
### 4. 在 B 上确认服务监听
确保 B 上的服务监听在 `0.0.0.0:80` 或 `10.0.0.2:80`,并且能看到真实 IP。
例如使用 Nginx 时,日志中 `$remote_addr` 会直接显示客户端公网 IP。
### 5. 测试与验证
1. 从外网客户端执行 `curl http://1.2.3.4/`(或浏览器访问)。
2. 在 B 上抓包验证:
```bash
tcpdump -i wg0 port 80 -n
```
你会看到源 IP 为客户端真实 IP,目标 IP 为 `10.0.0.2`。
3. 在 A 上抓包验证回复包经过 MASQUERADE:
```bash
tcpdump -i eth0 host <客户端IP> -n
```
源地址已变为 `1.2.3.4`。
---
## 补充说明与常见问题
### 为什么不需要 Proxy Protocol?
因为全程在网络层修改目标地址,源地址原样保留,服务直接解析 IP 层信息,无需应用层辅助。
### 支持 UDP 服务吗?
完全支持。将 `-p tcp` 改为 `-p udp` 即可,conntrack 同样会跟踪 UDP “连接”。
### 如果 B 上服务需要查看访问日志,需要注意什么?
服务可能默认只监听 `127.0.0.1`,需改为 `0.0.0.0` 或 WireGuard 接口 IP;另外某些软件会对非本地子网的请求做限制,注意关闭。
### A 上有多个公网 IP 或接口怎么办?
只需将 DNAT 规则中的 `-i eth0` 调整为你期望的入口接口,或者不限制 `-i`,按目标 IP 匹配。
### 安全提示
- 严格控制 DNAT 的端口范围,只暴露必要服务。
- WireGuard 本身已经加密,但仍建议 A 上配置防火墙仅允许必要的入站流量。
- 若 B 完全依赖 A 作为网关,需留意 A 的单点故障问题。
---
通过以上配置,你的 NAT 内服务器就可以直接感知请求客户端原始 IP,对外提供“透明”服务,同时完全摆脱 Proxy Protocol 的束缚。
1 个帖子 - 1 位参与者