Files
HomeAgent/internal/system/network.go
root bc9ef15eb0 三层回退恢复机制(L0写前留档/L1恢复梯子/L2离线回滚)+ guard 父守护
- L0: files 插件写受保护系统路径(/etc 等)前自动留档,AbstractBeforeWrite 到 data/file_baseline
- L1: failback 受限 worker 执行恢复梯子 probe→还原DNS/proxy→还原LLM配置+ReloadFromConfig→probe,N轮有界
- L2: tracker changeset 持久化原文 blob,guard 离线 RollbackFromDisk 回滚 agentfs;SystemSnapshot 支撑
- guard 父守护: 心跳 IPC(PING/ACK unix socket, 文件心跳回退)、失败计数、退出码协议(42/43/44)、最后手段
- 发行版路径适配: system.protected_paths/network_paths 可注入,默认面向主流 Linux
- Windows 兼容: guard.go/failback.go 加 //go:build linux, guard_windows.go 提供 no-op 桩
- 修复: guard.yaml last_resort 键冲突、changeset Content 不落盘导致离线回滚丢原文

Build 全绿, vet 干净, system/recovery/ipc/tracker 单元测试全过
2026-08-05 16:00:08 +08:00

240 lines
6.9 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.

// Package system 提供 L0/L1 层的系统级网络配置基线:捕获与还原本机的
// DNSresolv.conf/ hosts / 代理环境,供 failback 在救援源恢复前还原被主 agent
// 改坏的网络配置。
package system
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
// NetworkBaseline 一张系统网络配置快照:捕获时点 + DNS/hosts/代理环境原文。
type NetworkBaseline struct {
CapturedAt string `json:"captured_at"`
ResolvConf string `json:"resolv_conf,omitempty"` // /etc/resolv.conf 原文
Hosts string `json:"hosts,omitempty"` // /etc/hosts 原文
ProxyEnv map[string]string `json:"proxy_env,omitempty"` // http_proxy/https_proxy/no_proxy 等
}
const baselineFileName = "network_baseline.json"
// relevantPath 本机网络相关文件,与代理环境变量一同纳入基线。
// 默认值面向主流 Linux 发行版systemd-resolved 等也写 /etc/resolv.conf
// 若目标发行版布局不同(如 resolv.conf 在 /run/systemd/resolve/),由
// SetNetworkPaths 在启动时注入,避免硬编码失效。
var relevantPath = []string{
"/etc/resolv.conf",
"/etc/hosts",
"/etc/environment",
}
// SetNetworkPaths 覆盖纳入网络基线的文件路径列表(发行版适配入口)。
// resolv.conf / hosts 用于还原与"被改动"判定;其余文件仅捕获(诊断用)。
// 传 nil/空则恢复 Linux 默认。
func SetNetworkPaths(paths []string) {
if len(paths) == 0 {
relevantPath = []string{"/etc/resolv.conf", "/etc/hosts", "/etc/environment"}
return
}
ns := make([]string, 0, len(paths))
for _, p := range paths {
if p = strings.TrimSpace(p); p != "" {
ns = append(ns, p)
}
}
relevantPath = ns
}
// NetworkPaths 返回当前纳入基线的网络文件路径(副本)。
func NetworkPaths() []string {
out := make([]string, len(relevantPath))
copy(out, relevantPath)
return out
}
// envProxyKeys 纳入基线的代理相关环境变量(大小写归一,捕获时同时保存小写与大写形式)。
var envProxyKeys = []string{
"http_proxy", "https_proxy", "no_proxy",
"HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY",
"all_proxy", "ALL_PROXY",
}
// fileKey 映射路径 → 基线字段。已知可还原字段resolv_conf / hosts。
func fileKey(p string) string {
switch {
case strings.Contains(p, "resolv"):
return "resolv"
case strings.Contains(p, "hosts"):
return "hosts"
}
return ""
}
// CaptureNetwork 捕获当前系统网络配置成基线对象(不落盘)。
func CaptureNetwork() (*NetworkBaseline, error) {
b := &NetworkBaseline{
CapturedAt: time.Now().Format(time.RFC3339),
ProxyEnv: make(map[string]string),
}
for _, p := range relevantPath {
if data, err := os.ReadFile(p); err == nil {
switch fileKey(p) {
case "resolv":
b.ResolvConf = string(data)
case "hosts":
b.Hosts = string(data)
default:
if b.ProxyEnv == nil {
b.ProxyEnv = make(map[string]string)
}
b.ProxyEnv["file:"+p] = string(data)
}
}
}
for _, k := range envProxyKeys {
if v, ok := os.LookupEnv(k); ok {
b.ProxyEnv[k] = v
}
}
return b, nil
}
// SaveNetworkBaseline 把网络基线写入 <dataDir>/network_baseline.json。
func SaveNetworkBaseline(dataDir string, b *NetworkBaseline) error {
path := NetworkBaselinePath(dataDir)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
data, err := json.MarshalIndent(b, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0600)
}
// LoadNetworkBaseline 从 <dataDir>/network_baseline.json 读回网络基线。
func LoadNetworkBaseline(dataDir string) (*NetworkBaseline, error) {
path := NetworkBaselinePath(dataDir)
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var b NetworkBaseline
if err := json.Unmarshal(data, &b); err != nil {
return nil, err
}
return &b, nil
}
// NetworkBaselinePath 返回网络基线文件路径。
func NetworkBaselinePath(dataDir string) string {
return filepath.Join(dataDir, "recovery", baselineFileName)
}
// ApplyProxyEnv 把基线中的代理环境变量写回当前进程环境guard 在拉起 worker 前调用,
// 使 worker 继承干净代理)。
func (b *NetworkBaseline) ApplyProxyEnv() {
for _, k := range envProxyKeys {
if v, ok := b.ProxyEnv[k]; ok {
os.Setenv(k, v)
}
}
}
// ExtraProxyKeys 返回基线中额外捕获的键(如 ENVIRONMENT排序后供诊断/报告使用。
func (b *NetworkBaseline) ExtraProxyKeys() []string {
var keys []string
for k := range b.ProxyEnv {
found := false
for _, p := range envProxyKeys {
if k == p {
found = true
break
}
}
if !found {
keys = append(keys, k)
}
}
sort.Strings(keys)
return keys
}
// restoreTargets 返回可还原的网络文件(键→基线内容)。由注入路径派生,
// 适配不同发行版的 resolv.conf/hosts 位置。
func (b *NetworkBaseline) restoreTargets() map[string]string {
m := map[string]string{}
for _, p := range relevantPath {
switch fileKey(p) {
case "resolv":
if b.ResolvConf != "" {
m[p] = b.ResolvConf
}
case "hosts":
if b.Hosts != "" {
m[p] = b.Hosts
}
}
}
return m
}
// RestoreFiles 把基线中的 resolv.conf / hosts 写回原路径。仅当该路径被主 agent
// 改动过(当前内容与基线不同)时才覆盖,返回改动项列表。
func (b *NetworkBaseline) RestoreFiles() ([]string, error) {
var changed []string
for path, want := range b.restoreTargets() {
cur, err := os.ReadFile(path)
if err != nil || string(cur) == want {
continue
}
if err := os.WriteFile(path, []byte(want), 0644); err != nil {
return changed, fmt.Errorf("restore %s: %w", path, err)
}
changed = append(changed, path)
}
return changed, nil
}
// MismatchedFiles 返回与基线不一致的系统网络文件guard 判定"网络是否被破坏")。
// 返回名字列表;无则空。忽略读取失败的文件。
func (b *NetworkBaseline) MismatchedFiles() []string {
var out []string
for path, want := range b.restoreTargets() {
if cur, err := os.ReadFile(path); err != nil {
out = append(out, path+"(unreadable)")
} else if string(cur) != want {
out = append(out, path+"(modified)")
}
}
if len(out) == 0 {
return nil
}
sort.Strings(out)
return out
}
// IsNetworkMismatch 判断主机网络配置与基线是否不一致resolv.conf/hosts 中任一改动)。
func (b *NetworkBaseline) IsNetworkMismatch() bool {
return len(b.MismatchedFiles()) > 0
}
// Summary 生成基线摘要(用于 failback 报告)。
func (b *NetworkBaseline) Summary() string {
parts := []string{fmt.Sprintf("captured=%s", b.CapturedAt)}
if b.ResolvConf != "" {
parts = append(parts, fmt.Sprintf("resolv=%dbytes", len(b.ResolvConf)))
}
if b.Hosts != "" {
parts = append(parts, fmt.Sprintf("hosts=%dbytes", len(b.Hosts)))
}
if n := len(b.ProxyEnv); n > 0 {
parts = append(parts, fmt.Sprintf("proxy_vars=%d", n))
}
return strings.Join(parts, " ")
}