Root cause: UFW FORWARD policy DROP overrides lmvpn_nat accept rules. DNS (small UDP) worked but TCP packets were silently dropped. - Add firewall_linux.go: dynamic NAT/forward/UFW config from ApplySettings - Add firewall_darwin.go/firewall_other.go: stubs - Fix anti-spoof bug in switch.go: return dropPacket sentinel instead of nil, so spoofed packets are no longer written to TUN - Simplify install_linux.sh: remove hardcoded subnets and NAT config - Add UFW detection to diag panel
203 lines
5.6 KiB
Go
203 lines
5.6 KiB
Go
//go:build linux
|
||
|
||
package vpn
|
||
|
||
import (
|
||
"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) {
|
||
// 优先 nft:Debian 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, ""
|
||
}
|
||
return &has, "未检测到 masquerade 规则,客户端无法出网"
|
||
}
|
||
// 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
|
||
}
|
||
|
||
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), ""
|
||
}
|
||
return ptrBool(false), "未检测到 IPv6 masquerade 规则,IPv6 客户端无法出网"
|
||
}
|
||
}
|
||
|
||
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, ""
|
||
}
|