// Package gsysctl 提供内核参数(sysctl)读写、持久化、快照备份/回滚、审计日志能力。 // 对应 Python 版 backend/app/services/sysctl.py。 package gsysctl import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "regexp" "sort" "strings" "time" "github.com/user_TGPJp0KLpNsa5TAo/ddhv/core/consts" ) // ErrSysctl 表示 sysctl 操作错误 type ErrSysctl struct{ Msg string } func (e *ErrSysctl) Error() string { return e.Msg } // NewErrSysctl 构造 sysctl 错误 func NewErrSysctl(msg string) error { return &ErrSysctl{Msg: msg} } func newErr(format string, a ...any) error { return &ErrSysctl{Msg: fmt.Sprintf(format, a...)} } // Manager 内核参数管理器 type Manager struct { backupDir string // 快照备份目录 logFile string // 审计日志文件 } // 持久化配置文件(独立文件,不污染系统默认配置) const persistConf = "/etc/sysctl.d/99-gm-tune.conf" // 需要同步的系统配置文件(避免 sysctl --system 重载时被旧值覆盖) var systemConfFiles = []string{ "/etc/sysctl.conf", "/etc/sysctl.d/99-sysctl.conf", "/etc/sysctl.d/99-bbr.conf", } // 快照前缀 const snapshotPrefix = "snapshot_" // NewManager 创建管理器,确保目录存在 func NewManager() *Manager { backup := filepath.Join(consts.APP_DATA_PATH, "backups") logF := filepath.Join(consts.APP_DATA_PATH, "logs", "audit.log") os.MkdirAll(backup, 0o755) os.MkdirAll(filepath.Dir(logF), 0o755) return &Manager{backupDir: backup, logFile: logF} } // run 执行命令并返回 stdout func run(cmd ...string) (string, error) { c := exec.Command(cmd[0], cmd[1:]...) out, err := c.Output() if err != nil { if ee, ok := err.(*exec.ExitError); ok { return "", newErr("命令执行失败: %s => %s", strings.Join(cmd, " "), strings.TrimSpace(string(ee.Stderr))) } return "", newErr("命令执行失败: %s => %v", strings.Join(cmd, " "), err) } return string(out), nil } // GetValue 读取单个内核参数当前值 func GetValue(key string) (string, error) { out, err := run("sysctl", "-n", key) if err != nil { return "", err } return strings.TrimSpace(out), nil } // GetAll 读取全部内核参数 func GetAll() (map[string]string, error) { out, err := run("sysctl", "-a") if err != nil { return nil, err } result := map[string]string{} for _, line := range strings.Split(out, "\n") { line = strings.TrimSpace(line) if line == "" || !strings.Contains(line, "=") { continue } parts := strings.SplitN(line, "=", 2) result[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) } return result, nil } // GetValues 批量读取指定参数 func GetValues(keys []string) (map[string]string, error) { result := map[string]string{} for _, k := range keys { v, err := GetValue(k) if err != nil { v = "" } result[k] = v } return result, nil } // SetValue 设置单个内核参数并持久化 func SetValue(key, value string, persist bool) error { if _, err := run("sysctl", "-w", fmt.Sprintf("%s=%s", key, value)); err != nil { return err } if persist { return persistValue(key, value) } return nil } // SetValues 批量设置,返回每个参数的执行结果 func SetValues(pairs map[string]string, persist bool) map[string]any { results := map[string]any{} for k, v := range pairs { if err := SetValue(k, v, persist); err != nil { results[k] = map[string]any{"ok": false, "error": err.Error()} } else { results[k] = map[string]any{"ok": true, "value": v} } } return results } // syncSystemConf 同步同名参数到系统配置文件(只更新已存在的同名非注释行) func syncSystemConf(key, value string) { re := regexp.MustCompile(fmt.Sprintf(`^\s*%s\s*=`, regexp.QuoteMeta(key))) for _, path := range systemConfFiles { data, err := os.ReadFile(path) if err != nil { continue } lines := strings.Split(string(data), "\n") changed := false for i, line := range lines { trimmed := strings.TrimSpace(line) if re.MatchString(line) && !strings.HasPrefix(trimmed, "#") { lines[i] = fmt.Sprintf("%s = %s", key, value) changed = true } } if changed { os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0o644) } } } // persistValue 将参数写入持久化配置并同步系统配置 func persistValue(key, value string) error { lines := []string{} if data, err := os.ReadFile(persistConf); err == nil { lines = strings.Split(string(data), "\n") } re := regexp.MustCompile(fmt.Sprintf(`^\s*%s\s*=`, regexp.QuoteMeta(key))) newLines := []string{} for _, line := range lines { if !re.MatchString(line) { newLines = append(newLines, line) } } newLines = append(newLines, fmt.Sprintf("%s = %s", key, value)) os.MkdirAll(filepath.Dir(persistConf), 0o755) if err := os.WriteFile(persistConf, []byte(strings.Join(newLines, "\n")+"\n"), 0o644); err != nil { return newErr("写入持久化配置失败: %v", err) } syncSystemConf(key, value) return nil } // RemovePersist 从持久化配置中移除参数 func RemovePersist(key string) error { data, err := os.ReadFile(persistConf) if err != nil { return nil } re := regexp.MustCompile(fmt.Sprintf(`^\s*%s\s*=`, regexp.QuoteMeta(key))) lines := []string{} for _, line := range strings.Split(string(data), "\n") { if !re.MatchString(line) { lines = append(lines, line) } } return os.WriteFile(persistConf, []byte(strings.Join(lines, "\n")), 0o644) } // SnapshotMeta 快照元信息 type SnapshotMeta struct { Name string `json:"name"` CreatedAt string `json:"created_at"` Reason string `json:"reason"` Kind string `json:"kind"` ParamCount int `json:"param_count"` } // CreateSnapshot 创建当前全部内核参数快照 func (m *Manager) CreateSnapshot(reason, kind string) (SnapshotMeta, error) { ts := time.Now().Format("20060102_150405") name := snapshotPrefix + ts dir := filepath.Join(m.backupDir, name) if err := os.MkdirAll(dir, 0o755); err != nil { return SnapshotMeta{}, err } // 全部参数快照 params, err := GetAll() if err != nil { return SnapshotMeta{}, err } keys := make([]string, 0, len(params)) for k := range params { keys = append(keys, k) } sort.Strings(keys) var sb strings.Builder for _, k := range keys { sb.WriteString(fmt.Sprintf("%s = %s\n", k, params[k])) } os.WriteFile(filepath.Join(dir, "sysctl_all.conf"), []byte(sb.String()), 0o644) // 持久化配置副本 if data, err := os.ReadFile(persistConf); err == nil { os.WriteFile(filepath.Join(dir, "persist.conf.bak"), data, 0o644) } // 元信息 meta := SnapshotMeta{ Name: name, CreatedAt: time.Now().Format("2006-01-02 15:04:05"), Reason: reason, Kind: kind, ParamCount: len(params), } mb, _ := json.MarshalIndent(meta, "", " ") os.WriteFile(filepath.Join(dir, "meta.json"), mb, 0o644) return meta, nil } // ListSnapshots 列出所有快照(按时间倒序) func (m *Manager) ListSnapshots() []SnapshotMeta { snapshots := []SnapshotMeta{} entries, err := os.ReadDir(m.backupDir) if err != nil { return snapshots } for _, e := range entries { if !e.IsDir() || !strings.HasPrefix(e.Name(), snapshotPrefix) { continue } meta := SnapshotMeta{Name: e.Name(), Kind: "manual"} if data, err := os.ReadFile(filepath.Join(m.backupDir, e.Name(), "meta.json")); err == nil { json.Unmarshal(data, &meta) } snapshots = append(snapshots, meta) } sort.Slice(snapshots, func(i, j int) bool { return snapshots[i].Name > snapshots[j].Name }) return snapshots } // RestoreSnapshot 回滚到指定快照 func (m *Manager) RestoreSnapshot(name string) (map[string]any, error) { dir := filepath.Join(m.backupDir, name) confPath := filepath.Join(dir, "sysctl_all.conf") if _, err := os.Stat(confPath); err != nil { return nil, newErr("快照不存在: %s", name) } data, err := os.ReadFile(confPath) if err != nil { return nil, err } restored := map[string]string{} failed := map[string]string{} for _, line := range strings.Split(string(data), "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") || !strings.Contains(line, "=") { continue } parts := strings.SplitN(line, "=", 2) k, v := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) if _, err := run("sysctl", "-w", fmt.Sprintf("%s=%s", k, v)); err != nil { failed[k] = err.Error() } else { restored[k] = v } } // 恢复持久化配置 persistBackup := filepath.Join(dir, "persist.conf.bak") if _, err := os.Stat(persistBackup); err == nil { if pb, err := os.ReadFile(persistBackup); err == nil { os.WriteFile(persistConf, pb, 0o644) for k, v := range restored { syncSystemConf(k, v) } } } return map[string]any{ "snapshot": name, "restored_count": len(restored), "failed_count": len(failed), "failed": failed, }, nil } // LogAction 记录操作审计日志 func (m *Manager) LogAction(action string, detail map[string]any) { entry := map[string]any{"time": time.Now().Format("2006-01-02 15:04:05"), "action": action} for k, v := range detail { entry[k] = v } os.MkdirAll(filepath.Dir(m.logFile), 0o755) jb, _ := json.Marshal(entry) f, _ := os.OpenFile(m.logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) if f != nil { f.WriteString(string(jb) + "\n") f.Close() } } // AuditLogs 读取审计日志(最近 n 条,倒序) func (m *Manager) AuditLogs(limit int) []map[string]any { lines := []string{} if data, err := os.ReadFile(m.logFile); err == nil { lines = strings.Split(strings.TrimSpace(string(data)), "\n") } // 倒序(最新在前) result := []map[string]any{} for i := len(lines) - 1; i >= 0 && len(result) < limit; i-- { line := strings.TrimSpace(lines[i]) if line == "" { continue } var entry map[string]any if json.Unmarshal([]byte(line), &entry) == nil { result = append(result, entry) } } return result } // BoolToInt bool 转 int(0/1),供前端逻辑使用 func BoolToInt(b bool) int { if b { return 1 } return 0 }