Files
lmvpn_server/internal/vpn/diag_linux.go
T
dsh e82fb7421d fix: 网卡更换后 NAT/诊断规则绑定失效网卡导致客户端无法上网
问题: 更换网卡后(如 enp1s0f3 -> enp1s0), nft masquerade 规则仍绑定旧网卡名,
规则永不命中, 客户端连上 VPN 但无法出网; 且诊断面板只检查规则是否存在,
会误报正常。

修改:
1. systemd 单元增加 Wants/After=network-online.target, 开机等待默认路由
   就绪后再启动, 避免启动竞态下检测不到出口网卡而跳过防火墙配置;
2. detectWANInterface 增加 10x1s 重试, 开机初期无默认路由时短暂等待;
3. 诊断面板校验 masquerade 规则绑定的网卡是否真实存在, 不存在时给出
   明确提示(网卡已更换?), 不再误报正常。
2026-08-17 00:22:19 -04:00

235 lines
7.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//go:build linux
package vpn
import (
"fmt"
"os"
"os/exec"
"strconv"
"strings"
)
func fillPlatformDiag(r *DiagResult) {
r.HasCapNetAdmin = ptrBool(checkCapNetAdmin())
if r.HasCapNetAdmin == nil || !*r.HasCapNetAdmin {
r.CapNetAdminNote = "CAP_NET_ADMIN 未授权,TUN 操作需 root 或显式授予该能力"
}
v := readIPForward()
r.IPForward = v
if v == nil || !*v {
r.IPForwardNote = "未开启,执行: sysctl -w net.ipv4.ip_forward=1"
}
m, note := checkMasquerade()
r.Masquerade = m
r.MasqueradeNote = note
v6 := readIP6Forward()
r.IP6Forward = v6
if v6 == nil || !*v6 {
r.IP6ForwardNote = "未开启,执行: sysctl -w net.ipv6.conf.all.forwarding=1"
}
m6, note6 := checkMasquerade6()
r.Masquerade6 = m6
r.Masquerade6Note = note6
ufwActive := checkUFWActive()
r.UFWActive = &ufwActive
if ufwActive {
ufwOk, ufwNote := checkUFWForward()
if !ufwOk {
r.UFWForwardNote = ufwNote
}
}
}
func ptrBool(b bool) *bool { return &b }
func checkCapNetAdmin() bool {
data, err := os.ReadFile("/proc/self/status")
if err != nil {
return false
}
for _, line := range strings.Split(string(data), "\n") {
if !strings.HasPrefix(line, "CapEff:") {
continue
}
fields := strings.Fields(line)
if len(fields) < 2 {
return false
}
val, err := strconv.ParseUint(fields[1], 16, 64)
if err != nil {
return false
}
return val&(1<<12) != 0
}
return false
}
func readIPForward() *bool {
data, err := os.ReadFile("/proc/sys/net/ipv4/ip_forward")
if err != nil {
return nil
}
v := strings.TrimSpace(string(data)) == "1"
return &v
}
// findExecutable 在常见路径中查找可执行文件,弥补 systemd 服务 PATH 不含 /usr/sbin、/sbin 的问题
func findExecutable(names ...string) string {
for _, name := range names {
if p, err := exec.LookPath(name); err == nil {
return p
}
for _, dir := range []string{"/usr/sbin", "/sbin", "/usr/bin", "/bin"} {
full := dir + "/" + name
if fi, err := os.Stat(full); err == nil && !fi.IsDir() {
return full
}
}
}
return ""
}
// checkMasquerade 检测 NAT masquerade 规则,优先 nft(原生、无兼容问题),回退 iptables
// 返回 (结果, 说明);结果为 nil 表示无法判定
func checkMasquerade() (*bool, string) {
// 优先 nftDebian 12+ 的 iptables 是 nft 包装器,操作原生 nft nat 表会 "incompatible"
nftPath := findExecutable("nft")
if nftPath != "" {
out, err := exec.Command(nftPath, "list", "ruleset").Output()
if err == nil {
has := strings.Contains(string(out), "masquerade")
if !has {
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 尝试
}
// 回退 iptables(老系统或 nft 不可用时)
iptPath := findExecutable("iptables")
if iptPath != "" {
out, err := exec.Command(iptPath, "-t", "nat", "-L", "POSTROUTING", "-n").Output()
if err != nil {
// iptables-nft 与原生 nft 表不兼容时,若 nft 也读不到则判定为无法检测
if nftPath != "" {
return nil, "iptables 与原生 nft 表不兼容且 nft 不可执行,无法检测 MASQUERADE"
}
return nil, "iptables 不可执行(权限不足?),无法检测 MASQUERADE"
}
has := strings.Contains(string(out), "MASQUERADE")
if has {
return &has, ""
}
return &has, "未检测到 MASQUERADE 规则,客户端无法出网"
}
return nil, "iptables 与 nft 均未安装,无法检测 NAT 规则。Debian/Ubuntu 安装: apt install nftables"
}
func readIP6Forward() *bool {
data, err := os.ReadFile("/proc/sys/net/ipv6/conf/all/forwarding")
if err != nil {
return nil
}
v := strings.TrimSpace(string(data)) == "1"
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(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), ""
}
}
ip6tPath := findExecutable("ip6tables")
if ip6tPath != "" {
out, err := exec.Command(ip6tPath, "-t", "nat", "-L", "POSTROUTING", "-n").Output()
if err != nil {
if nftPath != "" {
return nil, "ip6tables 与原生 nft 表不兼容且 nft 不可执行,无法检测 IPv6 MASQUERADE"
}
return nil, "ip6tables 不可执行(权限不足?),无法检测 IPv6 MASQUERADE"
}
has := strings.Contains(string(out), "MASQUERADE")
if has {
return &has, ""
}
return &has, "未检测到 IPv6 MASQUERADE 规则,IPv6 客户端无法出网"
}
return nil, "ip6tables 与 nft 均未安装,无法检测 IPv6 NAT 规则"
}
// checkUFWForward checks if VPN forward rules exist in UFW's user-forward chains.
func checkUFWForward() (bool, string) {
nftPath := findExecutable("nft")
if nftPath == "" {
return true, ""
}
// Check IPv4
out, err := exec.Command(nftPath, "list", "chain", "ip", "filter", "ufw-user-forward").Output()
if err == nil {
s := string(out)
if !strings.Contains(s, "jump lmvpn-fwd") && !strings.Contains(s, "192.168.") {
return false, "UFW 已启用但 IPv4 转发规则未配置,客户端可能无法上网"
}
}
// Check IPv6
out6, err := exec.Command(nftPath, "list", "chain", "ip6", "filter", "ufw6-user-forward").Output()
if err == nil {
s := string(out6)
if !strings.Contains(s, "jump lmvpn6-fwd") && !strings.Contains(s, "fd00:") {
return false, "UFW 已启用但 IPv6 转发规则未配置,IPv6 客户端可能无法上网"
}
}
return true, ""
}