// Package api 提供 REST API 路由与处理器 // // system.go —— 主机环境体检与一键初始化 // // 设计原则: // 1. 只读检测(GET /system/env)与白名单修复(POST /system/fix)分离 // 2. 修复动作全部走预定义白名单,绝不接受调用方传入的命令串 // 3. 所有动作仅 admin 可执行,均写审计日志 package api import ( "fmt" "log" "os" "os/exec" "runtime" "strings" "github.com/gin-gonic/gin" "vpn-platform/backend/internal/binmgr" "vpn-platform/backend/internal/store" ) // sysctlDropIn / modulesDropIn 持久化路径(重启后仍生效) const ( sysctlDropIn = "/etc/sysctl.d/99-opsx-vpn.conf" modulesDropIn = "/etc/modules-load.d/opsx-vpn.conf" ) // envItem 单项环境检查结果 type envItem struct { Key string `json:"key"` // 唯一标识(修复时回传) Name string `json:"name"` // 展示名 OK bool `json:"ok"` // 是否满足 Detail string `json:"detail"` // 当前状态说明 Fixable bool `json:"fixable"` // 是否支持一键修复 FixHint string `json:"fix_hint"` // 修复动作的用户可读说明 Severity string `json:"severity"` // block / warn / info } // readProc 读取 /proc 下的单值文件 func readProc(path string) string { if raw, err := os.ReadFile(path); err == nil { return strings.TrimSpace(string(raw)) } return "" } // moduleLoaded 判断内核模块是否已加载(读 /proc/modules,不依赖 lsmod) func moduleLoaded(name string) bool { raw, err := os.ReadFile("/proc/modules") if err != nil { return false } for _, line := range strings.Split(string(raw), "\n") { if strings.HasPrefix(line, name+" ") { return true } } return false } // moduleBuiltin 判断模块是否编进内核(/sys/module/x 存在但 /proc/modules 无记录) func moduleBuiltin(name string) bool { if _, err := os.Stat("/sys/module/" + name); err == nil { return !moduleLoaded(name) } return false } // writeDropIn 写入 drop-in 配置(幂等:内容相同则不重写) func writeDropIn(path, content string) error { if cur, err := os.ReadFile(path); err == nil && strings.TrimSpace(string(cur)) == strings.TrimSpace(content) { return nil } if err := os.MkdirAll("/etc/sysctl.d", 0o755); err != nil { return err } if err := os.MkdirAll("/etc/modules-load.d", 0o755); err != nil { return err } return os.WriteFile(path, []byte(content+"\n"), 0o644) } // systemEnv 主机环境体检:返回系统信息 + 逐项检查结果 // // GET /api/system/env func (s *Server) systemEnv(c *gin.Context) { isRoot := os.Geteuid() == 0 items := make([]envItem, 0, 12) // 1) root 权限 —— VPN 平台要管理 iptables/隧道,非 root 无法工作 rootDetail := "以 root 运行,可管理系统网络" if !isRoot { rootDetail = "当前非 root,无法管理 iptables / 隧道;请用 root 或给二进制加 CAP_NET_ADMIN" } items = append(items, envItem{ Key: "root", Name: "运行权限", OK: isRoot, Detail: rootDetail, Fixable: false, Severity: "block", }) // 2) IPv4 转发 —— 客户端拨上来要能出网,必须为 1 fwd := readProc("/proc/sys/net/ipv4/ip_forward") == "1" fwdDetail := "net.ipv4.ip_forward = 1(已开启)" if !fwd { fwdDetail = "net.ipv4.ip_forward = 0,客户端拨入后无法转发流量 / NAT 不生效" } items = append(items, envItem{ Key: "ip_forward", Name: "IPv4 转发", OK: fwd, Detail: fwdDetail, Fixable: true, FixHint: "开启 net.ipv4.ip_forward 并写入 /etc/sysctl.d/ 持久化", Severity: "block", }) // 3) GRE 内核模块(GRE 隧道依赖) greOK := moduleLoaded("gre") || moduleBuiltin("gre") greDetail := "gre 模块已加载" if !greOK { greDetail = "gre 模块未加载,创建 GRE 隧道会失败" } items = append(items, envItem{ Key: "gre_module", Name: "GRE 内核模块", OK: greOK, Detail: greDetail, Fixable: true, FixHint: "modprobe gre 并写入 /etc/modules-load.d/ 持久化", Severity: "warn", }) // 4) 包管理器 pm := binmgr.DetectPackageManager() pmOK := pm != "" pmDetail := "检测到包管理器: " + pm if !pmOK { pmDetail = "未检测到 apt/yum/dnf/apk/pacman,无法自动安装系统包" } items = append(items, envItem{ Key: "package_manager", Name: "包管理器", OK: pmOK, Detail: pmDetail, Fixable: false, Severity: "block", }) // 5) 各协议引擎依赖(复用 binmgr,含 frp/wireguard/openvpn/strongswan) for _, b := range binmgr.CheckAll() { detail := "未安装" if b.Installed { detail = "已就绪" if b.Path != "" { detail += "(" + b.Path + ")" } if b.Version != "" { detail += " " + b.Version } } else if b.InstallCmd != "" { detail = "未安装,可通过包管理器安装" } // l2tp 额外依赖 xl2tpd(strongswan 装了但 xl2tpd 缺失时 L2TP 不可用) items = append(items, envItem{ Key: "bin:" + b.Name, Name: protoDisplayName(b.Name), OK: b.Installed, Detail: detail, Fixable: !b.Installed, FixHint: "一键安装 " + b.Name + " 依赖", Severity: "warn", }) } // 6) xl2tpd(L2TP 专用,binmgr 不单独检测) xOK := binExists("xl2tpd") xDetail := "xl2tpd 已就绪" if !xOK { xDetail = "xl2tpd 未安装,L2TP 协议不可用" } items = append(items, envItem{ Key: "tool:xl2tpd", Name: "L2TP (xl2tpd)", OK: xOK, Detail: xDetail, Fixable: true, FixHint: "安装 xl2tpd", Severity: "info", }) // 汇总 failed := 0 blocking := 0 for _, it := range items { if !it.OK { failed++ if it.Severity == "block" { blocking++ } } } c.JSON(200, gin.H{ "os": readOSName(), "kernel": readProc("/proc/sys/kernel/osrelease"), "arch": runtime.GOARCH, "root": isRoot, "package_manager": pm, "items": items, "failed": failed, "blocking": blocking, "ready": blocking == 0, }) } // systemFix 一键修复(白名单动作) // // POST /api/system/fix // body: {"action": "ip_forward" | "gre_module" | "install:openvpn" | ...} func (s *Server) systemFix(c *gin.Context) { var req struct { Action string `json:"action"` } if err := c.ShouldBindJSON(&req); err != nil || req.Action == "" { c.JSON(400, gin.H{"error": "缺少 action 参数"}) return } if os.Geteuid() != 0 { c.JSON(403, gin.H{"error": "需要 root 权限才能修改系统配置"}) return } action := strings.TrimSpace(req.Action) var output string var err error switch { case action == "ip_forward": output, err = fixIPv4Forwarding() case action == "gre_module": output, err = fixGREModule() case strings.HasPrefix(action, "install:"): // 协议名走白名单(与 installBinary 保持一致) name := strings.TrimPrefix(action, "install:") if !validInstallTarget(name) { c.JSON(400, gin.H{"error": fmt.Sprintf("不支持的安装目标: %s", name)}) return } output, err = binmgr.InstallViaPackageManager(name) case strings.HasPrefix(action, "tool:"): // 仅支持 xl2tpd(L2TP) name := strings.TrimPrefix(action, "tool:") if name != "xl2tpd" { c.JSON(400, gin.H{"error": fmt.Sprintf("不支持的工具: %s", name)}) return } output, err = installPackages("xl2tpd") default: c.JSON(400, gin.H{"error": fmt.Sprintf("不支持的操作: %s", action)}) return } if err != nil { log.Printf("[system] 修复失败 action=%s err=%v", action, err) c.JSON(500, gin.H{"error": err.Error(), "output": output}) return } store.Audit(c.GetString("username"), "system_fix", action, "ok") c.JSON(200, gin.H{"ok": true, "action": action, "output": output}) } // ---- 修复实现 ---- // fixIPv4Forwarding 开启 IPv4 转发并持久化 func fixIPv4Forwarding() (string, error) { var sb strings.Builder out, err := exec.Command("sysctl", "-w", "net.ipv4.ip_forward=1").CombinedOutput() sb.WriteString(string(out)) if err != nil { return sb.String(), fmt.Errorf("sysctl 设置失败: %w", err) } if err := writeDropIn(sysctlDropIn, "# OpsX VPN: VPN 网关需要转发\nnet.ipv4.ip_forward = 1"); err != nil { sb.WriteString("\n⚠️ 持久化写入失败(重启后失效): " + err.Error()) } else { sb.WriteString("\n✅ 已持久化到 " + sysctlDropIn) } // 校验 if readProc("/proc/sys/net/ipv4/ip_forward") != "1" { return sb.String(), fmt.Errorf("设置后校验仍为 0") } sb.WriteString("\n✅ 当前 net.ipv4.ip_forward = 1") return sb.String(), nil } // fixGREModule 加载 gre 模块并持久化 func fixGREModule() (string, error) { var sb strings.Builder out, err := exec.Command("modprobe", "gre").CombinedOutput() sb.WriteString(string(out)) if err != nil { // 模块可能已内建,检查 /sys/module if _, statErr := os.Stat("/sys/module/gre"); statErr != nil { return sb.String(), fmt.Errorf("modprobe gre 失败: %w", err) } sb.WriteString("(模块可能已内建)") } if err := writeDropIn(modulesDropIn, "# OpsX VPN: GRE 隧道依赖\ngre"); err != nil { sb.WriteString("\n⚠️ 持久化写入失败(重启后失效): " + err.Error()) } else { sb.WriteString("\n✅ 已持久化到 " + modulesDropIn) } if !moduleLoaded("gre") && !moduleBuiltin("gre") { return sb.String(), fmt.Errorf("加载后仍检测不到 gre 模块") } sb.WriteString("\n✅ gre 模块已就绪") return sb.String(), nil } // installPackages 用当前包管理器安装指定包 func installPackages(pkgs ...string) (string, error) { pm := binmgr.DetectPackageManager() if pm == "" { return "", fmt.Errorf("未检测到包管理器") } var args []string switch pm { case "apt-get": args = append([]string{"install", "-y"}, pkgs...) case "yum", "dnf": args = append([]string{"install", "-y"}, pkgs...) case "apk": args = append([]string{"add"}, pkgs...) case "pacman": args = append([]string{"-S", "--noconfirm"}, pkgs...) default: return "", fmt.Errorf("不支持的包管理器: %s", pm) } cmd := exec.Command(pm, args...) cmd.Env = append(os.Environ(), "DEBIAN_FRONTEND=noninteractive") out, err := cmd.CombinedOutput() if err != nil { return string(out), fmt.Errorf("%s 安装失败: %w", pm, err) } return string(out), nil } // ---- 小工具 ---- // validInstallTarget 安装目标白名单(与 installBinary 保持一致) func validInstallTarget(name string) bool { switch name { case "frp", "openvpn", "wireguard", "strongswan", "ipsec", "ikev2", "l2tp": return true } return false } // binExists 判断可执行文件是否在 PATH 中 func binExists(name string) bool { _, err := exec.LookPath(name) return err == nil } // readOSName 读取发行版名称 func readOSName() string { raw, err := os.ReadFile("/etc/os-release") if err != nil { return runtime.GOOS } for _, line := range strings.Split(string(raw), "\n") { if strings.HasPrefix(line, "PRETTY_NAME=") { return strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), `"`) } } return runtime.GOOS } // protoDisplayName 协议展示名(与前端保持一致) func protoDisplayName(name string) string { switch name { case "frp": return "FRP 内网穿透" case "wireguard": return "WireGuard" case "openvpn": return "OpenVPN" case "strongswan": return "StrongSwan (IPSec/IKEv2)" } return name }