forked from kevin/lmvpn_server
fix: 网卡更换后 NAT/诊断规则绑定失效网卡导致客户端无法上网
问题: 更换网卡后(如 enp1s0f3 -> enp1s0), nft masquerade 规则仍绑定旧网卡名, 规则永不命中, 客户端连上 VPN 但无法出网; 且诊断面板只检查规则是否存在, 会误报正常。 修改: 1. systemd 单元增加 Wants/After=network-online.target, 开机等待默认路由 就绪后再启动, 避免启动竞态下检测不到出口网卡而跳过防火墙配置; 2. detectWANInterface 增加 10x1s 重试, 开机初期无默认路由时短暂等待; 3. 诊断面板校验 masquerade 规则绑定的网卡是否真实存在, 不存在时给出 明确提示(网卡已更换?), 不再误报正常。
This commit is contained in:
+2
-1
@@ -53,7 +53,8 @@ echo ">>> 安装 systemd 服务..."
|
|||||||
cat > /etc/systemd/system/lmvpn.service << 'EOF'
|
cat > /etc/systemd/system/lmvpn.service << 'EOF'
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=LMVPN Server
|
Description=LMVPN Server
|
||||||
After=network.target
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
package vpn
|
package vpn
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -103,10 +104,18 @@ func checkMasquerade() (*bool, string) {
|
|||||||
out, err := exec.Command(nftPath, "list", "ruleset").Output()
|
out, err := exec.Command(nftPath, "list", "ruleset").Output()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
has := strings.Contains(string(out), "masquerade")
|
has := strings.Contains(string(out), "masquerade")
|
||||||
if has {
|
if !has {
|
||||||
return &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 尝试
|
// nft 存在但执行失败(权限不足等),仍回退 iptables 尝试
|
||||||
}
|
}
|
||||||
@@ -141,16 +150,39 @@ func readIP6Forward() *bool {
|
|||||||
return &v
|
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) {
|
func checkMasquerade6() (*bool, string) {
|
||||||
nftPath := findExecutable("nft")
|
nftPath := findExecutable("nft")
|
||||||
if nftPath != "" {
|
if nftPath != "" {
|
||||||
out, err := exec.Command(nftPath, "list", "ruleset").Output()
|
out, err := exec.Command(nftPath, "list", "ruleset").Output()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
s := string(out)
|
s := string(out)
|
||||||
if strings.Contains(s, "ip6 saddr") && strings.Contains(s, "masquerade") {
|
if !(strings.Contains(s, "ip6 saddr") && strings.Contains(s, "masquerade")) {
|
||||||
return ptrBool(true), ""
|
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), ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// configureFirewall dynamically configures NAT masquerade, forward accept rules,
|
// configureFirewall dynamically configures NAT masquerade, forward accept rules,
|
||||||
@@ -26,16 +27,21 @@ func configureFirewall(ipNet, ipNet6 string, tunName string) {
|
|||||||
configureUFWForward(ipNet, ipNet6)
|
configureUFWForward(ipNet, ipNet6)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// detectWANInterface 通过默认路由确定出口网卡。
|
||||||
|
// 开机初期(如 systemd 尚未完成网络在线、DHCP 未取得租约)可能没有默认路由,
|
||||||
|
// 此时短暂重试,避免在网卡变更/开机竞态下留下绑定旧网卡名的陈旧防火墙规则。
|
||||||
func detectWANInterface() string {
|
func detectWANInterface() string {
|
||||||
out, err := exec.Command("ip", "route", "show", "default").Output()
|
for attempt := 0; attempt < 10; attempt++ {
|
||||||
if err != nil {
|
out, err := exec.Command("ip", "route", "show", "default").Output()
|
||||||
return ""
|
if err == nil {
|
||||||
}
|
fields := strings.Fields(string(out))
|
||||||
fields := strings.Fields(string(out))
|
for i, f := range fields {
|
||||||
for i, f := range fields {
|
if f == "dev" && i+1 < len(fields) {
|
||||||
if f == "dev" && i+1 < len(fields) {
|
return fields[i+1]
|
||||||
return fields[i+1]
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user