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 "" }