From e82fb7421d5d09fde6130fa604e27ac3b45ab46e Mon Sep 17 00:00:00 2001 From: dsh Date: Mon, 17 Aug 2026 00:22:19 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BD=91=E5=8D=A1=E6=9B=B4=E6=8D=A2?= =?UTF-8?q?=E5=90=8E=20NAT/=E8=AF=8A=E6=96=AD=E8=A7=84=E5=88=99=E7=BB=91?= =?UTF-8?q?=E5=AE=9A=E5=A4=B1=E6=95=88=E7=BD=91=E5=8D=A1=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF=E6=97=A0=E6=B3=95=E4=B8=8A=E7=BD=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题: 更换网卡后(如 enp1s0f3 -> enp1s0), nft masquerade 规则仍绑定旧网卡名, 规则永不命中, 客户端连上 VPN 但无法出网; 且诊断面板只检查规则是否存在, 会误报正常。 修改: 1. systemd 单元增加 Wants/After=network-online.target, 开机等待默认路由 就绪后再启动, 避免启动竞态下检测不到出口网卡而跳过防火墙配置; 2. detectWANInterface 增加 10x1s 重试, 开机初期无默认路由时短暂等待; 3. 诊断面板校验 masquerade 规则绑定的网卡是否真实存在, 不存在时给出 明确提示(网卡已更换?), 不再误报正常。 --- install_linux.sh | 3 ++- internal/vpn/diag_linux.go | 44 +++++++++++++++++++++++++++++----- internal/vpn/firewall_linux.go | 22 ++++++++++------- 3 files changed, 54 insertions(+), 15 deletions(-) diff --git a/install_linux.sh b/install_linux.sh index 131e5f4..e6e8c60 100755 --- a/install_linux.sh +++ b/install_linux.sh @@ -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 diff --git a/internal/vpn/diag_linux.go b/internal/vpn/diag_linux.go index b619a64..5f9b4a7 100644 --- a/internal/vpn/diag_linux.go +++ b/internal/vpn/diag_linux.go @@ -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), "" } } diff --git a/internal/vpn/firewall_linux.go b/internal/vpn/firewall_linux.go index 31c31a3..85f746d 100644 --- a/internal/vpn/firewall_linux.go +++ b/internal/vpn/firewall_linux.go @@ -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 "" }