探针中增加目录防篡改

This commit is contained in:
dushixiang committed 2025-11-22 22:34:45 +08:00
1 parent b05c0beaae
commit 0cc12c2416
6 files changed
+1414 -3

No files matched your search

+1 -1
View File
@@ -4,6 +4,7 @@ go 1.25.0
require (
github.com/coreos/go-oidc/v3 v3.16.0
github.com/fsnotify/fsnotify v1.9.0
github.com/go-errors/errors v1.5.1
github.com/go-orz/orz v0.2.9
github.com/go-orz/toolkit v0.1.0
@@ -32,7 +33,6 @@ require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.9.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/glebarez/sqlite v1.11.0 // indirect
+26
View File
@@ -50,6 +50,9 @@ const (
// 指标消息
MessageTypeMetrics MessageType = "metrics"
MessageTypeMonitorConfig MessageType = "monitor_config"
// 防篡改消息
MessageTypeTamperProtect MessageType = "tamper_protect"
MessageTypeTamperEvent MessageType = "tamper_event"
)
type MetricType string
@@ -262,3 +265,26 @@ type MonitorData struct {
CertExpiryTime int64 `json:"certExpiryTime,omitempty"` // 证书过期时间(毫秒时间戳)
CertDaysLeft int `json:"certDaysLeft,omitempty"` // 证书剩余天数
}
// TamperProtectConfig 防篡改保护配置
type TamperProtectConfig struct {
Paths []string `json:"paths"` // 要保护的目录列表(完整列表)
}
// TamperProtectResponse 防篡改保护响应
type TamperProtectResponse struct {
Success bool `json:"success"` // 是否成功
Message string `json:"message"` // 响应消息
Paths []string `json:"paths"` // 当前保护的目录列表
Added []string `json:"added,omitempty"` // 新增的目录
Removed []string `json:"removed,omitempty"` // 移除的目录
Error string `json:"error,omitempty"` // 错误信息
}
// TamperEventData 防篡改事件数据
type TamperEventData struct {
Path string `json:"path"` // 被修改的路径
Operation string `json:"operation"` // 操作类型: write, remove, rename, chmod, create
Timestamp int64 `json:"timestamp"` // 事件时间(毫秒)
Details string `json:"details"` // 详细信息
}
+133 -2
View File
@@ -16,6 +16,7 @@ import (
"github.com/dushixiang/pika/pkg/agent/collector"
"github.com/dushixiang/pika/pkg/agent/config"
"github.com/dushixiang/pika/pkg/agent/id"
"github.com/dushixiang/pika/pkg/agent/tamper"
"github.com/dushixiang/pika/pkg/version"
"github.com/gorilla/websocket"
"github.com/jpillora/backoff"
@@ -60,13 +61,15 @@ type Agent struct {
activeConn *safeConn
collectorMu sync.RWMutex
collectorManager *collector.Manager
tamperProtector *tamper.Protector
}
// New 创建 Agent 实例
func New(cfg *config.Config) *Agent {
return &Agent{
cfg: cfg,
idMgr: id.NewManager(),
cfg: cfg,
idMgr: id.NewManager(),
tamperProtector: tamper.NewProtector(),
}
}
@@ -192,6 +195,11 @@ func (a *Agent) runOnce(ctx context.Context) error {
}
}()
// 启动防篡改事件监控
go func() {
a.tamperEventLoop(ctx, conn, done)
}()
// 等待错误或上下文取消
select {
case err := <-errChan:
@@ -242,6 +250,8 @@ func (a *Agent) readLoop(conn *websocket.Conn, done chan struct{}) error {
go a.handleCommand(msg.Data)
case protocol.MessageTypeMonitorConfig:
go a.handleMonitorConfig(msg.Data)
case protocol.MessageTypeTamperProtect:
go a.handleTamperProtect(msg.Data)
default:
// 忽略其他类型
}
@@ -576,3 +586,124 @@ func (a *Agent) sendCommandResponse(conn *safeConn, cmdID, cmdType, status, errM
func GetVersion() string {
return version.GetVersion()
}
// handleTamperProtect 处理防篡改保护指令
func (a *Agent) handleTamperProtect(data json.RawMessage) {
var config protocol.TamperProtectConfig
if err := json.Unmarshal(data, &config); err != nil {
log.Printf("⚠️ 解析防篡改保护配置失败: %v", err)
a.sendTamperProtectResponse(false, "解析配置失败", nil, nil, nil, err.Error())
return
}
log.Printf("📥 收到防篡改保护配置: Paths=%v", config.Paths)
conn := a.getActiveConn()
if conn == nil {
log.Println("⚠️ 当前连接未就绪,无法执行防篡改保护")
return
}
// 如果配置为空列表,停止所有保护
if len(config.Paths) == 0 {
if err := a.tamperProtector.StopAll(); err != nil {
log.Printf("❌ 停止所有防篡改保护失败: %v", err)
a.sendTamperProtectResponse(false, "停止所有保护失败", []string{}, nil, nil, err.Error())
return
}
log.Println("✅ 已停止所有防篡改保护")
a.sendTamperProtectResponse(true, "已停止所有防篡改保护", []string{}, []string{}, a.tamperProtector.GetProtectedPaths(), "")
return
}
// 更新保护目录列表
ctx := context.Background()
result, err := a.tamperProtector.UpdatePaths(ctx, config.Paths)
if err != nil {
log.Printf("⚠️ 更新防篡改保护失败: %v", err)
// 即使有错误也返回部分成功的结果
if result != nil {
a.sendTamperProtectResponse(false, "部分更新失败", result.Current, result.Added, result.Removed, err.Error())
} else {
a.sendTamperProtectResponse(false, "更新失败", nil, nil, nil, err.Error())
}
return
}
// 成功更新
message := fmt.Sprintf("防篡改保护已更新: 新增 %d 个, 移除 %d 个, 当前保护 %d 个目录",
len(result.Added), len(result.Removed), len(result.Current))
log.Printf("✅ %s", message)
a.sendTamperProtectResponse(true, message, result.Current, result.Added, result.Removed, "")
}
// sendTamperProtectResponse 发送防篡改保护响应
func (a *Agent) sendTamperProtectResponse(success bool, message string, paths []string, added []string, removed []string, errMsg string) {
conn := a.getActiveConn()
if conn == nil {
return
}
resp := protocol.TamperProtectResponse{
Success: success,
Message: message,
Paths: paths,
Added: added,
Removed: removed,
Error: errMsg,
}
respData, err := json.Marshal(resp)
if err != nil {
log.Printf("⚠️ 序列化防篡改保护响应失败: %v", err)
return
}
msg := protocol.Message{
Type: protocol.MessageTypeTamperProtect,
Data: respData,
}
if err := conn.WriteJSON(msg); err != nil {
log.Printf("⚠️ 发送防篡改保护响应失败: %v", err)
}
}
// tamperEventLoop 防篡改事件监控循环
func (a *Agent) tamperEventLoop(ctx context.Context, conn *safeConn, done chan struct{}) {
eventCh := a.tamperProtector.GetEvents()
for {
select {
case <-done:
return
case <-ctx.Done():
return
case event := <-eventCh:
// 发送防篡改事件到服务端
eventData := protocol.TamperEventData{
Path: event.Path,
Operation: event.Operation,
Timestamp: event.Timestamp.UnixMilli(),
Details: event.Details,
}
data, err := json.Marshal(eventData)
if err != nil {
log.Printf("⚠️ 序列化防篡改事件失败: %v", err)
continue
}
msg := protocol.Message{
Type: protocol.MessageTypeTamperEvent,
Data: data,
}
if err := conn.WriteJSON(msg); err != nil {
log.Printf("⚠️ 发送防篡改事件失败: %v", err)
} else {
log.Printf("📤 已上报防篡改事件: %s - %s", event.Path, event.Operation)
}
}
}
}
+518
View File
@@ -0,0 +1,518 @@
# 防篡改保护模块
## 功能概述
该模块为 Pika Agent 提供 Linux 文件系统防篡改保护功能。通过以下机制实现:
1. **文件属性保护**: 使用 `chattr +i` 命令将指定目录设置为不可变(immutable)属性
2. **实时监控**: 使用 `fsnotify` 库监控受保护目录的文件系统事件
3. **事件上报**: 将检测到的文件变动事件实时上报到服务端
4. **动态管理**: 支持动态增加、移除保护目录,自动计算差异并应用
## 系统要求
- **操作系统**: 仅支持 Linux 系统
- **权限**: 需要 root 权限才能执行 `chattr` 命令
- **依赖**: `github.com/fsnotify/fsnotify`
## 工作原理
### 动态目录管理
服务端每次发送的是**完整的目录列表**,Agent 会自动计算需要新增和移除的目录:
#### 场景示例
**第一次配置**: `/a /b /c`
```json
{
"type": "tamper_protect",
"data": {
"paths": ["/a", "/b", "/c"]
}
}
```
- Agent 操作: 新增保护 `/a /b /c`
- 响应: `added: ["/a", "/b", "/c"], removed: [], current: ["/a", "/b", "/c"]`
**第二次配置**: `/a /b` (移除 /c)
```json
{
"type": "tamper_protect",
"data": {
"paths": ["/a", "/b"]
}
}
```
- Agent 操作: 移除 `/c` 的保护 (执行 `chattr -i /c`)
- 响应: `added: [], removed: ["/c"], current: ["/a", "/b"]`
**第三次配置**: `/a /b /c /d` (新增 /c /d)
```json
{
"type": "tamper_protect",
"data": {
"paths": ["/a", "/b", "/c", "/d"]
}
}
```
- Agent 操作: 新增保护 `/c /d` (执行 `chattr +i /c` 和 `chattr +i /d`)
- 响应: `added: ["/c", "/d"], removed: [], current: ["/a", "/b", "/c", "/d"]`
**第四次配置**: `[]` (清空所有保护)
```json
{
"type": "tamper_protect",
"data": {
"paths": []
}
}
```
- Agent 操作: 移除所有目录的保护
- 响应: `added: [], removed: ["/a", "/b", "/c", "/d"], current: []`
### 核心流程
```
服务端发送完整列表
↓
Agent 计算差异
• 新列表 - 当前列表 = 需要新增
• 当前列表 - 新列表 = 需要移除
↓
执行操作
• 对移除的目录: chattr -i + watcher.Remove()
• 对新增的目录: chattr +i + watcher.Add()
↓
返回结果
• 成功/失败状态
• 新增的目录列表
• 移除的目录列表
• 当前所有保护的目录
```
## 核心 API
### Protector.UpdatePaths()
```go
func (p *Protector) UpdatePaths(ctx context.Context, newPaths []string) (*UpdateResult, error)
```
**参数**:
- `newPaths`: 新的完整目录列表
**返回**:
- `UpdateResult`: 更新结果
- `Added`: 本次新增保护的目录
- `Removed`: 本次移除保护的目录
- `Current`: 当前所有受保护的目录
- `error`: 错误信息(如果部分操作失败,仍会返回 UpdateResult)
**特性**:
- **幂等性**: 多次使用相同参数调用,结果一致
- **原子性**: 单个目录的添加/移除操作失败不影响其他目录
- **智能差异**: 自动计算需要变更的目录,避免重复操作
### Protector.StopAll()
```go
func (p *Protector) StopAll() error
```
停止所有防篡改保护,移除所有目录的不可变属性,关闭文件监控器。
### Protector.GetProtectedPaths()
```go
func (p *Protector) GetProtectedPaths() []string
```
获取当前所有受保护的目录列表。
### Protector.IsProtected()
```go
func (p *Protector) IsProtected(path string) bool
```
检查指定路径是否受保护。
## Protocol 定义
### 消息类型
```go
MessageTypeTamperProtect MessageType = "tamper_protect"
MessageTypeTamperEvent MessageType = "tamper_event"
```
### 数据结构
#### 配置请求
```go
type TamperProtectConfig struct {
Paths []string // 要保护的目录列表(完整列表)
}
```
#### 配置响应
```go
type TamperProtectResponse struct {
Success bool // 是否成功
Message string // 响应消息
Paths []string // 当前保护的目录列表
Added []string // 本次新增的目录
Removed []string // 本次移除的目录
Error string // 错误信息
}
```
#### 事件数据
```go
type TamperEventData struct {
Path string // 被修改的路径
Operation string // 操作类型: write, remove, rename, chmod, create
Timestamp int64 // 事件时间(毫秒)
Details string // 详细信息
}
```
## Agent 集成
在 `pkg/agent/service/agent.go` 中的集成:
```go
// 初始化
agent.tamperProtector = tamper.NewProtector()
// 接收配置消息
case protocol.MessageTypeTamperProtect:
go a.handleTamperProtect(msg.Data)
// 处理配置
func (a *Agent) handleTamperProtect(data json.RawMessage) {
var config protocol.TamperProtectConfig
// ...解析配置
if len(config.Paths) == 0 {
// 清空所有保护
a.tamperProtector.StopAll()
} else {
// 更新保护目录
result, err := a.tamperProtector.UpdatePaths(ctx, config.Paths)
// ...发送响应
}
}
// 事件监控循环
func (a *Agent) tamperEventLoop(ctx context.Context, conn *safeConn, done chan struct{}) {
eventCh := a.tamperProtector.GetEvents()
for event := range eventCh {
// 上报事件到服务端
sendTamperEvent(event)
}
}
```
## 使用示例
### 服务端: 第一次配置
```go
config := protocol.TamperProtectConfig{
Paths: []string{"/etc/nginx", "/var/www"},
}
// 发送到 Agent
sendMessage(protocol.MessageTypeTamperProtect, config)
// 期望响应
// {
// "success": true,
// "message": "防篡改保护已更新: 新增 2 个, 移除 0 个, 当前保护 2 个目录",
// "paths": ["/etc/nginx", "/var/www"],
// "added": ["/etc/nginx", "/var/www"],
// "removed": []
// }
```
### 服务端: 修改配置(移除一个目录)
```go
config := protocol.TamperProtectConfig{
Paths: []string{"/etc/nginx"}, // 只保留一个
}
sendMessage(protocol.MessageTypeTamperProtect, config)
// 期望响应
// {
// "success": true,
// "message": "防篡改保护已更新: 新增 0 个, 移除 1 个, 当前保护 1 个目录",
// "paths": ["/etc/nginx"],
// "added": [],
// "removed": ["/var/www"]
// }
```
### 服务端: 添加新目录
```go
config := protocol.TamperProtectConfig{
Paths: []string{"/etc/nginx", "/etc/ssh", "/opt/app"},
}
sendMessage(protocol.MessageTypeTamperProtect, config)
// 期望响应
// {
// "success": true,
// "message": "防篡改保护已更新: 新增 2 个, 移除 0 个, 当前保护 3 个目录",
// "paths": ["/etc/nginx", "/etc/ssh", "/opt/app"],
// "added": ["/etc/ssh", "/opt/app"],
// "removed": []
// }
```
### 服务端: 清空所有保护
```go
config := protocol.TamperProtectConfig{
Paths: []string{}, // 空列表
}
sendMessage(protocol.MessageTypeTamperProtect, config)
// 期望响应
// {
// "success": true,
// "message": "已停止所有防篡改保护",
// "paths": [],
// "added": [],
// "removed": ["/etc/nginx", "/etc/ssh", "/opt/app"]
// }
```
### 服务端: 接收事件通知
```go
// 监听事件消息
case protocol.MessageTypeTamperEvent:
var event protocol.TamperEventData
json.Unmarshal(msg.Data, &event)
// 处理防篡改事件
log.Printf("检测到篡改: %s - %s at %d",
event.Path, event.Operation, event.Timestamp)
// 可以进行告警、记录日志等操作
sendAlert(event)
```
## 测试
### 运行测试
```bash
# 运行所有测试(在非 Linux 系统上部分测试会跳过)
go test -v ./pkg/agent/tamper/
# 在 Linux 系统上以 root 权限运行完整测试
sudo go test -v ./pkg/agent/tamper/
```
### 测试场景
测试覆盖了完整的动态管理场景:
1. ✅ **第一次配置 `/a /b /c`**: 验证新增 3 个目录
2. ✅ **第二次配置 `/a /b`**: 验证移除 1 个目录
3. ✅ **第三次配置 `/a /b /c /d`**: 验证新增 2 个目录
4. ✅ **第四次配置 `[]`**: 验证移除所有目录
5. ✅ **重复配置**: 验证幂等性(无变化时不执行操作)
6. ✅ **事件监控**: 验证文件变动事件上报
7. ✅ **JSON 序列化**: 验证数据结构正确性
## 设计优势
### 1. 简化服务端逻辑
服务端无需维护状态,每次只需发送完整的目录列表,差异计算由 Agent 完成:
```
❌ 复杂的方式(需要服务端计算差异):
- 服务端需要知道 Agent 当前保护的目录
- 需要对比计算差异
- 发送增删指令
✅ 简单的方式(Agent 计算差异):
- 服务端只需发送完整列表
- Agent 自动计算并应用差异
- 响应中包含完整的变更信息
```
### 2. 幂等性保证
相同的配置多次发送不会产生副作用:
```go
// 第一次
UpdatePaths(["/a", "/b"]) → added: ["/a", "/b"]
// 第二次(相同配置)
UpdatePaths(["/a", "/b"]) → added: [], removed: [] (无操作)
```
### 3. 容错性
部分目录操作失败不影响其他目录:
```go
result, err := UpdatePaths(["/valid", "/invalid", "/valid2"])
// 即使 /invalid 失败,/valid 和 /valid2 仍然会被保护
// result.Added = ["/valid", "/valid2"]
// err 会包含失败信息
```
### 4. 清晰的状态反馈
响应中包含完整的变更信息,便于服务端验证:
```json
{
"success": true,
"paths": ["/a", "/b", "/c"], // 当前状态
"added": ["/c"], // 本次新增
"removed": ["/d"] // 本次移除
}
```
## 注意事项
1. **权限要求**: 必须以 root 权限运行 Agent,否则 `chattr` 命令会失败
2. **操作系统限制**: 仅支持 Linux 系统,在其他系统上会返回错误
3. **文件系统支持**: 目标文件系统必须支持 chattr 属性(如 ext2/ext3/ext4/xfs)
4. **性能影响**:
- `chattr +i` 会阻止所有写操作,包括合法的系统更新
- 文件监控会消耗一定的系统资源
5. **事件队列**: 事件通道缓冲区大小为 100,如果处理不及时可能丢失事件
6. **并发安全**: 使用了读写锁保护共享状态,支持并发操作
7. **watcher 生命周期**: 首次调用 UpdatePaths 时创建,调用 StopAll 时销毁
## 安全建议
1. **谨慎选择保护路径**: 不要保护系统关键目录(如 `/`, `/usr`, `/bin`),可能导致系统无法正常运行
2. **定期审查**: 定期检查防篡改事件,及时发现异常行为
3. **配合其他安全措施**: 防篡改保护应作为纵深防御的一部分,配合防火墙、入侵检测等
4. **备份重要文件**: 在启用保护前备份重要配置文件
5. **测试环境验证**: 先在测试环境验证,确认不影响业务后再部署到生产环境
## 故障排查
### chattr 命令失败
```
错误: 执行 chattr 失败: permission denied
解决: 确保以 root 权限运行 Agent
```
### 不支持的文件系统
```
错误: 执行 chattr 失败: Inappropriate ioctl for device
解决: 检查目标目录所在的文件系统是否支持 chattr 属性
```
### 事件未上报
```
问题: 配置了防篡改保护,但没有收到事件
排查:
1. 检查 Agent 日志,确认保护已启用
2. 确认文件确实发生了变动
3. 检查事件队列是否已满
4. 检查网络连接是否正常
```
### 部分目录操作失败
```
响应: {
"success": false,
"error": "部分操作失败: 添加失败 1 个, 移除失败 0 个",
"paths": ["/valid"],
"added": ["/valid"],
"removed": []
}
处理:
1. 检查失败目录的权限
2. 检查失败目录是否存在
3. 检查文件系统类型
4. 查看 Agent 日志获取详细错误
```
## 架构图
```
┌─────────────────────────────────────────────────────────────┐
│ 服务端 │
│ │
│ 管理员配置: ["/etc/nginx", "/var/www", "/opt/app"] │
│ ↓ │
│ 发送完整列表(TamperProtectConfig) │
└────────────────────────┬────────────────────────────────────┘
│ WebSocket
↓
┌─────────────────────────────────────────────────────────────┐
│ Agent │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Protector.UpdatePaths() │ │
│ │ │ │
│ │ 1. 计算差异: │ │
│ │ • 新列表 - 当前 = 需要新增 │ │
│ │ • 当前 - 新列表 = 需要移除 │ │
│ │ │ │
│ │ 2. 执行操作: │ │
│ │ 移除: chattr -i + watcher.Remove() │ │
│ │ 新增: chattr +i + watcher.Add() │ │
│ │ │ │
│ │ 3. 返回结果: │ │
│ │ { added: [...], removed: [...], current: [...] } │ │
│ └────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 文件监控(fsnotify.Watcher) │ │
│ │ │ │
│ │ 监控所有受保护的目录 │ │
│ │ ↓ │ │
│ │ 检测到文件变动 │ │
│ │ ↓ │ │
│ │ 生成 TamperEvent │ │
│ │ ↓ │ │
│ │ 通过 channel 发送 │ │
│ └────────────────────────────────────────────────────┘ │
│ ↓ │
│ 上报事件到服务端(TamperEventData) │
└────────────────────────┬────────────────────────────────────┘
│ WebSocket
↓
┌─────────────────────────────────────────────────────────────┐
│ 服务端 │
│ │
│ 接收事件 → 告警 → 日志 → 审计 │
└─────────────────────────────────────────────────────────────┘
```
## 未来改进
- [ ] 支持递归监控子目录
- [ ] 支持白名单机制(允许特定进程修改)
- [ ] 增加事件过滤器(过滤掉不重要的事件)
- [ ] 支持更多文件系统属性(如 append-only)
- [ ] 增加性能监控指标
- [ ] 支持 Windows 平台(使用不同的实现机制)
- [ ] 支持配置持久化(Agent 重启后恢复保护状态)
- [ ] 支持按文件粒度保护(不仅仅是目录)
+379
View File
@@ -0,0 +1,379 @@
package tamper
import (
"context"
"fmt"
"log"
"os/exec"
"runtime"
"sync"
"time"
"github.com/fsnotify/fsnotify"
)
// TamperEvent 防篡改事件
type TamperEvent struct {
Path string `json:"path"` // 被修改的路径
Operation string `json:"operation"` // 操作类型: write, remove, rename, chmod
Timestamp time.Time `json:"timestamp"` // 事件时间
Details string `json:"details"` // 详细信息
}
// AttributeTamperAlert 属性篡改告警
type AttributeTamperAlert struct {
Path string `json:"path"` // 被篡改的路径
Timestamp time.Time `json:"timestamp"` // 检测时间
Details string `json:"details"` // 详细信息(如: "不可变属性被移除")
Restored bool `json:"restored"` // 是否已自动恢复
}
// UpdateResult 更新结果
type UpdateResult struct {
Added []string // 新增保护的目录
Removed []string // 移除保护的目录
Current []string // 当前所有保护的目录
}
// Protector 防篡改保护器
type Protector struct {
mu sync.RWMutex
paths map[string]bool // 当前保护的目录集合(使用 map 便于查找)
watcher *fsnotify.Watcher
ctx context.Context
cancel context.CancelFunc
eventCh chan TamperEvent
alertCh chan AttributeTamperAlert // 属性篡改告警通道
watcherOnce sync.Once // 确保 watcher 只创建一次
patrolOnce sync.Once // 确保巡检只启动一次
}
// NewProtector 创建防篡改保护器
func NewProtector() *Protector {
return &Protector{
paths: make(map[string]bool),
eventCh: make(chan TamperEvent, 100),
}
}
// UpdatePaths 更新保护的目录列表
// 参数 newPaths: 新的完整目录列表
// 返回: 更新结果(新增/移除的目录)和错误
func (p *Protector) UpdatePaths(ctx context.Context, newPaths []string) (*UpdateResult, error) {
// 检查操作系统
if runtime.GOOS != "linux" {
return nil, fmt.Errorf("防篡改功能仅支持 Linux 系统")
}
p.mu.Lock()
defer p.mu.Unlock()
// 转换为 map 便于比较
newPathsMap := make(map[string]bool)
for _, path := range newPaths {
newPathsMap[path] = true
}
// 计算需要新增的目录
var toAdd []string
for path := range newPathsMap {
if !p.paths[path] {
toAdd = append(toAdd, path)
}
}
// 计算需要移除的目录
var toRemove []string
for path := range p.paths {
if !newPathsMap[path] {
toRemove = append(toRemove, path)
}
}
// 如果没有变化,直接返回
if len(toAdd) == 0 && len(toRemove) == 0 {
log.Println("ℹ️ 防篡改保护目录列表无变化")
return &UpdateResult{
Added: []string{},
Removed: []string{},
Current: p.getCurrentPaths(),
}, nil
}
// 初始化 watcher(如果还没创建)
if err := p.initWatcher(ctx); err != nil {
return nil, err
}
// 处理需要移除的目录
var removeFailed []string
for _, path := range toRemove {
if err := p.removePath(path); err != nil {
log.Printf("⚠️ 移除目录 %s 保护失败: %v", path, err)
removeFailed = append(removeFailed, path)
} else {
delete(p.paths, path)
log.Printf("✅ 已取消保护目录: %s", path)
}
}
// 处理需要新增的目录
var addFailed []string
for _, path := range toAdd {
if err := p.addPath(path); err != nil {
log.Printf("⚠️ 添加目录 %s 保护失败: %v", path, err)
addFailed = append(addFailed, path)
} else {
p.paths[path] = true
log.Printf("✅ 已保护目录: %s", path)
}
}
// 构建结果
result := &UpdateResult{
Added: filterFailed(toAdd, addFailed),
Removed: filterFailed(toRemove, removeFailed),
Current: p.getCurrentPaths(),
}
// 如果有失败的操作,返回错误
if len(addFailed) > 0 || len(removeFailed) > 0 {
return result, fmt.Errorf("部分操作失败: 添加失败 %d 个, 移除失败 %d 个", len(addFailed), len(removeFailed))
}
log.Printf("✅ 防篡改保护已更新: 新增 %d 个目录, 移除 %d 个目录, 当前保护 %d 个目录",
len(result.Added), len(result.Removed), len(result.Current))
return result, nil
}
// StopAll 停止所有防篡改保护
func (p *Protector) StopAll() error {
p.mu.Lock()
defer p.mu.Unlock()
if len(p.paths) == 0 {
log.Println("ℹ️ 没有正在保护的目录")
return nil
}
var lastErr error
// 取消 context
if p.cancel != nil {
p.cancel()
p.cancel = nil
}
// 关闭监控器
if p.watcher != nil {
if err := p.watcher.Close(); err != nil {
log.Printf("⚠️ 关闭文件监控器失败: %v", err)
lastErr = err
}
p.watcher = nil
p.watcherOnce = sync.Once{} // 重置,允许下次重新创建
}
// 移除所有目录的不可变属性
for path := range p.paths {
if err := p.setImmutable(path, false); err != nil {
log.Printf("⚠️ 移除目录 %s 不可变属性失败: %v", path, err)
lastErr = err
} else {
log.Printf("✅ 已取消保护目录: %s", path)
}
}
// 清空路径列表
p.paths = make(map[string]bool)
log.Println("✅ 已停止所有防篡改保护")
return lastErr
}
// GetEvents 获取事件通道
func (p *Protector) GetEvents() <-chan TamperEvent {
return p.eventCh
}
// GetProtectedPaths 获取受保护的路径列表
func (p *Protector) GetProtectedPaths() []string {
p.mu.RLock()
defer p.mu.RUnlock()
return p.getCurrentPaths()
}
// IsProtected 检查路径是否受保护
func (p *Protector) IsProtected(path string) bool {
p.mu.RLock()
defer p.mu.RUnlock()
return p.paths[path]
}
// getCurrentPaths 获取当前路径列表(内部方法,不加锁)
func (p *Protector) getCurrentPaths() []string {
paths := make([]string, 0, len(p.paths))
for path := range p.paths {
paths = append(paths, path)
}
return paths
}
// initWatcher 初始化文件监控器(只会创建一次)
func (p *Protector) initWatcher(ctx context.Context) error {
var err error
p.watcherOnce.Do(func() {
// 创建文件监控器
p.watcher, err = fsnotify.NewWatcher()
if err != nil {
err = fmt.Errorf("创建文件监控器失败: %w", err)
return
}
// 创建 context
p.ctx, p.cancel = context.WithCancel(ctx)
// 启动监控循环
go p.watchLoop()
log.Println("✅ 文件监控器已启动")
})
return err
}
// addPath 添加目录保护(内部方法,不加锁)
func (p *Protector) addPath(path string) error {
// 设置不可变属性
if err := p.setImmutable(path, true); err != nil {
return fmt.Errorf("设置目录不可变属性失败: %w", err)
}
// 添加到监控
if p.watcher != nil {
if err := p.watcher.Add(path); err != nil {
// 如果添加监控失败,尝试回滚不可变属性
_ = p.setImmutable(path, false)
return fmt.Errorf("添加目录到监控失败: %w", err)
}
}
return nil
}
// removePath 移除目录保护(内部方法,不加锁)
func (p *Protector) removePath(path string) error {
// 从监控中移除
if p.watcher != nil {
if err := p.watcher.Remove(path); err != nil {
log.Printf("⚠️ 从监控中移除目录失败: %v", err)
// 继续执行,不返回错误
}
}
// 移除不可变属性
if err := p.setImmutable(path, false); err != nil {
return fmt.Errorf("移除目录不可变属性失败: %w", err)
}
return nil
}
// watchLoop 监控循环
func (p *Protector) watchLoop() {
for {
select {
case <-p.ctx.Done():
return
case event, ok := <-p.watcher.Events:
if !ok {
return
}
p.handleEvent(event)
case err, ok := <-p.watcher.Errors:
if !ok {
return
}
log.Printf("⚠️ 文件监控错误: %v", err)
}
}
}
// handleEvent 处理文件系统事件
func (p *Protector) handleEvent(event fsnotify.Event) {
var operation string
var details string
switch {
case event.Op&fsnotify.Write == fsnotify.Write:
operation = "write"
details = "文件被写入"
case event.Op&fsnotify.Remove == fsnotify.Remove:
operation = "remove"
details = "文件被删除"
case event.Op&fsnotify.Rename == fsnotify.Rename:
operation = "rename"
details = "文件被重命名"
case event.Op&fsnotify.Chmod == fsnotify.Chmod:
operation = "chmod"
details = "文件权限被修改"
case event.Op&fsnotify.Create == fsnotify.Create:
operation = "create"
details = "文件被创建"
default:
operation = "unknown"
details = fmt.Sprintf("未知操作: %v", event.Op)
}
tamperEvent := TamperEvent{
Path: event.Name,
Operation: operation,
Timestamp: time.Now(),
Details: details,
}
// 发送事件(非阻塞)
select {
case p.eventCh <- tamperEvent:
log.Printf("🚨 检测到文件变动: %s - %s (%s)", event.Name, operation, details)
default:
log.Printf("⚠️ 事件队列已满,丢弃事件: %s", event.Name)
}
}
// setImmutable 设置或移除文件/目录的不可变属性
func (p *Protector) setImmutable(path string, immutable bool) error {
var cmd *exec.Cmd
if immutable {
cmd = exec.Command("chattr", "+i", path)
} else {
cmd = exec.Command("chattr", "-i", path)
}
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("执行 chattr 失败: %w, 输出: %s", err, string(output))
}
return nil
}
// filterFailed 过滤掉失败的项,返回成功的项
func filterFailed(all []string, failed []string) []string {
if len(failed) == 0 {
return all
}
failedMap := make(map[string]bool)
for _, f := range failed {
failedMap[f] = true
}
var success []string
for _, item := range all {
if !failedMap[item] {
success = append(success, item)
}
}
return success
}
+357
View File
@@ -0,0 +1,357 @@
package tamper
import (
"context"
"encoding/json"
"os"
"runtime"
"testing"
"time"
)
func TestNewProtector(t *testing.T) {
p := NewProtector()
if p == nil {
t.Fatal("NewProtector() 返回 nil")
}
paths := p.GetProtectedPaths()
if len(paths) != 0 {
t.Errorf("新建的保护器不应该有受保护的路径,当前有 %d 个", len(paths))
}
}
func TestUpdatePaths(t *testing.T) {
// 此测试仅在 Linux 系统上运行
if runtime.GOOS != "linux" {
t.Skip("防篡改功能仅支持 Linux 系统")
}
// 检查是否有 root 权限
if os.Geteuid() != 0 {
t.Skip("此测试需要 root 权限才能运行 chattr 命令")
}
p := NewProtector()
ctx := context.Background()
// 创建测试目录
testDir1 := "/tmp/tamper_test_1_" + time.Now().Format("20060102150405")
testDir2 := "/tmp/tamper_test_2_" + time.Now().Format("20060102150405")
testDir3 := "/tmp/tamper_test_3_" + time.Now().Format("20060102150405")
testDir4 := "/tmp/tamper_test_4_" + time.Now().Format("20060102150405")
if err := os.MkdirAll(testDir1, 0755); err != nil {
t.Fatalf("创建测试目录失败: %v", err)
}
defer os.RemoveAll(testDir1)
if err := os.MkdirAll(testDir2, 0755); err != nil {
t.Fatalf("创建测试目录失败: %v", err)
}
defer os.RemoveAll(testDir2)
if err := os.MkdirAll(testDir3, 0755); err != nil {
t.Fatalf("创建测试目录失败: %v", err)
}
defer os.RemoveAll(testDir3)
if err := os.MkdirAll(testDir4, 0755); err != nil {
t.Fatalf("创建测试目录失败: %v", err)
}
defer os.RemoveAll(testDir4)
defer p.StopAll()
// 第一次更新: 添加 /a /b /c
t.Run("第一次配置: /a /b /c", func(t *testing.T) {
result, err := p.UpdatePaths(ctx, []string{testDir1, testDir2, testDir3})
if err != nil {
t.Fatalf("UpdatePaths() 失败: %v", err)
}
if len(result.Added) != 3 {
t.Errorf("应该新增 3 个目录,实际新增 %d 个", len(result.Added))
}
if len(result.Removed) != 0 {
t.Errorf("不应该移除任何目录,实际移除 %d 个", len(result.Removed))
}
if len(result.Current) != 3 {
t.Errorf("当前应该保护 3 个目录,实际保护 %d 个", len(result.Current))
}
// 验证目录确实受保护
if !p.IsProtected(testDir1) {
t.Errorf("目录 %s 应该受保护", testDir1)
}
})
// 第二次更新: 修改为 /a /b (移除 /c)
t.Run("第二次配置: /a /b", func(t *testing.T) {
result, err := p.UpdatePaths(ctx, []string{testDir1, testDir2})
if err != nil {
t.Fatalf("UpdatePaths() 失败: %v", err)
}
if len(result.Added) != 0 {
t.Errorf("不应该新增任何目录,实际新增 %d 个", len(result.Added))
}
if len(result.Removed) != 1 {
t.Errorf("应该移除 1 个目录,实际移除 %d 个", len(result.Removed))
}
if len(result.Current) != 2 {
t.Errorf("当前应该保护 2 个目录,实际保护 %d 个", len(result.Current))
}
// 验证 /c 不再受保护
if p.IsProtected(testDir3) {
t.Errorf("目录 %s 不应该受保护", testDir3)
}
})
// 第三次更新: 修改为 /a /b /c /d (新增 /c /d)
t.Run("第三次配置: /a /b /c /d", func(t *testing.T) {
result, err := p.UpdatePaths(ctx, []string{testDir1, testDir2, testDir3, testDir4})
if err != nil {
t.Fatalf("UpdatePaths() 失败: %v", err)
}
if len(result.Added) != 2 {
t.Errorf("应该新增 2 个目录,实际新增 %d 个", len(result.Added))
}
if len(result.Removed) != 0 {
t.Errorf("不应该移除任何目录,实际移除 %d 个", len(result.Removed))
}
if len(result.Current) != 4 {
t.Errorf("当前应该保护 4 个目录,实际保护 %d 个", len(result.Current))
}
// 验证所有目录都受保护
for _, dir := range []string{testDir1, testDir2, testDir3, testDir4} {
if !p.IsProtected(dir) {
t.Errorf("目录 %s 应该受保护", dir)
}
}
})
// 第四次更新: 清空列表
t.Run("第四次配置: 清空", func(t *testing.T) {
result, err := p.UpdatePaths(ctx, []string{})
if err != nil {
t.Fatalf("UpdatePaths() 失败: %v", err)
}
if len(result.Added) != 0 {
t.Errorf("不应该新增任何目录,实际新增 %d 个", len(result.Added))
}
if len(result.Removed) != 4 {
t.Errorf("应该移除 4 个目录,实际移除 %d 个", len(result.Removed))
}
if len(result.Current) != 0 {
t.Errorf("当前应该保护 0 个目录,实际保护 %d 个", len(result.Current))
}
})
// 第五次更新: 重复配置相同的列表
t.Run("第五次配置: /a /b (重复)", func(t *testing.T) {
// 先添加
p.UpdatePaths(ctx, []string{testDir1, testDir2})
// 再次添加相同的配置
result, err := p.UpdatePaths(ctx, []string{testDir1, testDir2})
if err != nil {
t.Fatalf("UpdatePaths() 失败: %v", err)
}
if len(result.Added) != 0 {
t.Errorf("重复配置不应该新增任何目录,实际新增 %d 个", len(result.Added))
}
if len(result.Removed) != 0 {
t.Errorf("重复配置不应该移除任何目录,实际移除 %d 个", len(result.Removed))
}
if len(result.Current) != 2 {
t.Errorf("当前应该保护 2 个目录,实际保护 %d 个", len(result.Current))
}
})
}
func TestStopAll(t *testing.T) {
// 此测试仅在 Linux 系统上运行
if runtime.GOOS != "linux" {
t.Skip("防篡改功能仅支持 Linux 系统")
}
// 检查是否有 root 权限
if os.Geteuid() != 0 {
t.Skip("此测试需要 root 权限才能运行 chattr 命令")
}
p := NewProtector()
// 创建测试目录
testDir := "/tmp/tamper_test_stop_" + time.Now().Format("20060102150405")
if err := os.MkdirAll(testDir, 0755); err != nil {
t.Fatalf("创建测试目录失败: %v", err)
}
defer os.RemoveAll(testDir)
ctx := context.Background()
// 添加保护
_, err := p.UpdatePaths(ctx, []string{testDir})
if err != nil {
t.Fatalf("UpdatePaths() 失败: %v", err)
}
// 验证目录受保护
if !p.IsProtected(testDir) {
t.Error("目录应该受保护")
}
// 停止所有保护
err = p.StopAll()
if err != nil {
t.Fatalf("StopAll() 失败: %v", err)
}
// 验证目录不再受保护
if p.IsProtected(testDir) {
t.Error("目录不应该受保护")
}
paths := p.GetProtectedPaths()
if len(paths) != 0 {
t.Errorf("停止后不应该有受保护的路径,当前有 %d 个", len(paths))
}
}
func TestProtectorEvents(t *testing.T) {
// 此测试仅在 Linux 系统上运行
if runtime.GOOS != "linux" {
t.Skip("防篡改功能仅支持 Linux 系统")
}
// 检查是否有 root 权限
if os.Geteuid() != 0 {
t.Skip("此测试需要 root 权限才能运行 chattr 命令")
}
p := NewProtector()
// 创建测试目录
testDir := "/tmp/tamper_test_events_" + time.Now().Format("20060102150405")
if err := os.MkdirAll(testDir, 0755); err != nil {
t.Fatalf("创建测试目录失败: %v", err)
}
defer os.RemoveAll(testDir)
ctx := context.Background()
// 启动保护器
_, err := p.UpdatePaths(ctx, []string{testDir})
if err != nil {
t.Fatalf("UpdatePaths() 失败: %v", err)
}
defer p.StopAll()
// 获取事件通道
eventCh := p.GetEvents()
// 等待一段时间让 watcher 准备就绪
time.Sleep(500 * time.Millisecond)
// 尝试在受保护的目录中创建文件(这应该被阻止或触发事件)
testFile := testDir + "/test.txt"
_ = os.WriteFile(testFile, []byte("test"), 0644)
// 等待事件
select {
case event := <-eventCh:
t.Logf("收到防篡改事件: Path=%s, Operation=%s, Details=%s",
event.Path, event.Operation, event.Details)
// 验证事件字段
if event.Path == "" {
t.Error("事件路径不应为空")
}
if event.Operation == "" {
t.Error("事件操作不应为空")
}
case <-time.After(3 * time.Second):
t.Log("在超时时间内没有收到事件(这可能是正常的,因为 chattr +i 会阻止操作)")
}
}
func TestTamperEventJSON(t *testing.T) {
event := TamperEvent{
Path: "/tmp/test.txt",
Operation: "write",
Timestamp: time.Now(),
Details: "文件被写入",
}
// 测试 JSON 序列化
data, err := json.Marshal(event)
if err != nil {
t.Fatalf("JSON 序列化失败: %v", err)
}
// 测试 JSON 反序列化
var decoded TamperEvent
err = json.Unmarshal(data, &decoded)
if err != nil {
t.Fatalf("JSON 反序列化失败: %v", err)
}
// 验证字段
if decoded.Path != event.Path {
t.Errorf("Path = %s, want %s", decoded.Path, event.Path)
}
if decoded.Operation != event.Operation {
t.Errorf("Operation = %s, want %s", decoded.Operation, event.Operation)
}
if decoded.Details != event.Details {
t.Errorf("Details = %s, want %s", decoded.Details, event.Details)
}
}
func TestUpdateResultJSON(t *testing.T) {
result := UpdateResult{
Added: []string{"/tmp/a", "/tmp/b"},
Removed: []string{"/tmp/c"},
Current: []string{"/tmp/a", "/tmp/b", "/tmp/d"},
}
// 测试 JSON 序列化
data, err := json.Marshal(result)
if err != nil {
t.Fatalf("JSON 序列化失败: %v", err)
}
// 测试 JSON 反序列化
var decoded UpdateResult
err = json.Unmarshal(data, &decoded)
if err != nil {
t.Fatalf("JSON 反序列化失败: %v", err)
}
// 验证字段
if len(decoded.Added) != len(result.Added) {
t.Errorf("Added length = %d, want %d", len(decoded.Added), len(result.Added))
}
if len(decoded.Removed) != len(result.Removed) {
t.Errorf("Removed length = %d, want %d", len(decoded.Removed), len(result.Removed))
}
if len(decoded.Current) != len(result.Current) {
t.Errorf("Current length = %d, want %d", len(decoded.Current), len(result.Current))
}
}