fix: 网卡更换后 NAT/诊断规则绑定失效网卡导致客户端无法上网

问题: 更换网卡后(如 enp1s0f3 -> enp1s0), nft masquerade 规则仍绑定旧网卡名,
规则永不命中, 客户端连上 VPN 但无法出网; 且诊断面板只检查规则是否存在,
会误报正常。

修改:
1. systemd 单元增加 Wants/After=network-online.target, 开机等待默认路由
   就绪后再启动, 避免启动竞态下检测不到出口网卡而跳过防火墙配置;
2. detectWANInterface 增加 10x1s 重试, 开机初期无默认路由时短暂等待;
3. 诊断面板校验 masquerade 规则绑定的网卡是否真实存在, 不存在时给出
   明确提示(网卡已更换?), 不再误报正常。
This commit is contained in:
dsh
2026-08-17 00:22:19 -04:00
parent 8398067acb
commit e82fb7421d
3 changed files with 54 additions and 15 deletions
+2 -1
View File
@@ -53,7 +53,8 @@ echo ">>> 安装 systemd 服务..."
cat > /etc/systemd/system/lmvpn.service << 'EOF'
[Unit]
Description=LMVPN Server
After=network.target
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
+38 -6
View File
@@ -3,6 +3,7 @@
package vpn
import (
"fmt"
"os"
"os/exec"
"strconv"
@@ -103,10 +104,18 @@ func checkMasquerade() (*bool, string) {
out, err := exec.Command(nftPath, "list", "ruleset").Output()
if err == nil {
has := strings.Contains(string(out), "masquerade")
if has {
return &has, ""
if !has {
return &has, "未检测到 masquerade 规则,客户端无法出网"
}
return &has, "未检测到 masquerade 规则,客户端无法出网"
// masquerade 规则存在,但若绑定到已不存在的网卡名(如更换网卡后),
// 规则永远不会命中,客户端仍然无法上网——需要单独校验。
if iface := masqueradeIface(string(out)); iface != "" {
if err := exec.Command("ip", "link", "show", iface).Run(); err != nil {
return ptrBool(false), fmt.Sprintf(
"masquerade 规则绑定到不存在的网卡 %s(网卡已更换?),请在管理后台保存 VPN 设置或重启服务以重新配置防火墙", iface)
}
}
return &has, ""
}
// nft 存在但执行失败(权限不足等),仍回退 iptables 尝试
}
@@ -141,16 +150,39 @@ func readIP6Forward() *bool {
return &v
}
// masqueradeIface 从 nft ruleset 文本中提取 masquerade 规则绑定的出口网卡名;
// 若规则未绑定网卡(对所有出口生效)则返回空字符串。
func masqueradeIface(ruleset string) string {
for _, line := range strings.Split(ruleset, "\n") {
if !strings.Contains(line, "masquerade") {
continue
}
if i := strings.Index(line, `oifname "`); i >= 0 {
rest := line[i+len(`oifname "`):]
if j := strings.Index(rest, `"`); j > 0 {
return rest[:j]
}
}
}
return ""
}
func checkMasquerade6() (*bool, string) {
nftPath := findExecutable("nft")
if nftPath != "" {
out, err := exec.Command(nftPath, "list", "ruleset").Output()
if err == nil {
s := string(out)
if strings.Contains(s, "ip6 saddr") && strings.Contains(s, "masquerade") {
return ptrBool(true), ""
if !(strings.Contains(s, "ip6 saddr") && strings.Contains(s, "masquerade")) {
return ptrBool(false), "未检测到 IPv6 masquerade 规则,IPv6 客户端无法出网"
}
return ptrBool(false), "未检测到 IPv6 masquerade 规则,IPv6 客户端无法出网"
if iface := masqueradeIface(s); iface != "" {
if err := exec.Command("ip", "link", "show", iface).Run(); err != nil {
return ptrBool(false), fmt.Sprintf(
"masquerade 规则绑定到不存在的网卡 %s(网卡已更换?),请在管理后台保存 VPN 设置或重启服务以重新配置防火墙", iface)
}
}
return ptrBool(true), ""
}
}
+14 -8
View File
@@ -7,6 +7,7 @@ import (
"log"
"os/exec"
"strings"
"time"
)
// configureFirewall dynamically configures NAT masquerade, forward accept rules,
@@ -26,16 +27,21 @@ func configureFirewall(ipNet, ipNet6 string, tunName string) {
configureUFWForward(ipNet, ipNet6)
}
// detectWANInterface 通过默认路由确定出口网卡。
// 开机初期(如 systemd 尚未完成网络在线、DHCP 未取得租约)可能没有默认路由,
// 此时短暂重试,避免在网卡变更/开机竞态下留下绑定旧网卡名的陈旧防火墙规则。
func detectWANInterface() string {
out, err := exec.Command("ip", "route", "show", "default").Output()
if err != nil {
return ""
}
fields := strings.Fields(string(out))
for i, f := range fields {
if f == "dev" && i+1 < len(fields) {
return fields[i+1]
for attempt := 0; attempt < 10; attempt++ {
out, err := exec.Command("ip", "route", "show", "default").Output()
if err == nil {
fields := strings.Fields(string(out))
for i, f := range fields {
if f == "dev" && i+1 < len(fields) {
return fields[i+1]
}
}
}
time.Sleep(time.Second)
}
return ""
}