Files
pika/pkg/agent/sshmonitor/monitor.go
T
2026-01-03 11:05:23 +08:00

286 lines
6.1 KiB
Go

package sshmonitor
import (
"context"
"fmt"
"log/slog"
"os"
"runtime"
"sync"
"time"
"github.com/dushixiang/pika/internal/protocol"
"github.com/fsnotify/fsnotify"
)
const (
DefaultLogFile = "/var/log/pika/ssh_login.log"
DefaultLogDir = "/var/log/pika"
)
// Monitor SSH登录监控器
type Monitor struct {
mu sync.RWMutex
enabled bool
logFile string
watcher *fsnotify.Watcher
ctx context.Context
cancel context.CancelFunc
eventCh chan SSHLoginEvent
parser *Parser
hookManager *HookManager
watcherOnce sync.Once
lastOffset int64 // 记录上次读取的位置,避免重复读取
}
// NewMonitor 创建监控器
func NewMonitor() *Monitor {
return &Monitor{
enabled: false,
logFile: DefaultLogFile,
eventCh: make(chan SSHLoginEvent, 100),
parser: NewParser(),
hookManager: NewHookManager(),
}
}
// Start 启动监控(根据配置启用/禁用)
func (m *Monitor) Start(ctx context.Context, config protocol.SSHLoginConfig) error {
// 检查操作系统
if runtime.GOOS != "linux" {
return fmt.Errorf("SSH登录监控仅支持 Linux 系统")
}
m.mu.Lock()
defer m.mu.Unlock()
// 如果已启动,先停止
if m.enabled {
m.stopInternal()
}
if !config.Enabled {
slog.Info("SSH登录监控已禁用")
return nil
}
// 确保日志目录存在
if err := os.MkdirAll(DefaultLogDir, 0755); err != nil {
return fmt.Errorf("创建日志目录失败: %w", err)
}
// 确保日志文件存在
if _, err := os.Stat(m.logFile); os.IsNotExist(err) {
f, err := os.Create(m.logFile)
if err != nil {
return fmt.Errorf("创建日志文件失败: %w", err)
}
f.Close()
os.Chmod(m.logFile, 0644)
}
// 安装 PAM Hook
if err := m.hookManager.Install(); err != nil {
if os.IsPermission(err) {
slog.Warn("权限不足,无法自动安装 PAM Hook", "error", err)
slog.Info("如需启用 SSH 登录监控,请手动执行安装")
// 不返回错误,继续监控(假设已手动安装)
} else {
slog.Warn("安装 PAM Hook 失败", "error", err)
return err
}
}
// 初始化 watcher
if err := m.initWatcher(ctx); err != nil {
return err
}
// 读取现有日志(避免丢失 Agent 重启期间的登录)
if err := m.readExistingLogs(); err != nil {
slog.Warn("读取现有日志失败", "error", err)
}
m.enabled = true
slog.Info("SSH登录监控已启动", "logFile", m.logFile)
return nil
}
// Stop 停止监控
func (m *Monitor) Stop() error {
m.mu.Lock()
defer m.mu.Unlock()
return m.stopInternal()
}
// stopInternal 内部停止方法(不加锁)
func (m *Monitor) stopInternal() error {
if !m.enabled {
return nil
}
// 取消 context
if m.cancel != nil {
m.cancel()
m.cancel = nil
}
// 关闭 watcher
if m.watcher != nil {
m.watcher.Close()
m.watcher = nil
m.watcherOnce = sync.Once{}
}
// 卸载 PAM Hook
if err := m.hookManager.Uninstall(); err != nil {
slog.Warn("卸载 PAM Hook 失败", "error", err)
}
m.enabled = false
slog.Info("SSH登录监控已停止")
return nil
}
// GetEvents 获取事件通道
func (m *Monitor) GetEvents() <-chan SSHLoginEvent {
return m.eventCh
}
// initWatcher 初始化文件监控器
func (m *Monitor) initWatcher(ctx context.Context) error {
var err error
m.watcherOnce.Do(func() {
m.watcher, err = fsnotify.NewWatcher()
if err != nil {
err = fmt.Errorf("创建文件监控器失败: %w", err)
return
}
// 添加日志文件到监控
if err = m.watcher.Add(m.logFile); err != nil {
err = fmt.Errorf("添加文件监控失败: %w", err)
return
}
// 创建 context
m.ctx, m.cancel = context.WithCancel(ctx)
// 启动监控循环
go m.watchLoop()
slog.Info("文件监控器已启动", "file", m.logFile)
})
return err
}
// watchLoop 监控循环
func (m *Monitor) watchLoop() {
for {
select {
case <-m.ctx.Done():
return
case event, ok := <-m.watcher.Events:
if !ok {
return
}
// 检测文件被删除或重命名(logrotate)
if event.Op&fsnotify.Remove == fsnotify.Remove ||
event.Op&fsnotify.Rename == fsnotify.Rename {
slog.Warn("日志文件被删除或轮转,尝试重新监控")
// 等待新文件创建
time.Sleep(1 * time.Second)
// 重新添加监控
if err := m.watcher.Add(m.logFile); err != nil {
slog.Error("重新添加文件监控失败", "error", err)
} else {
m.lastOffset = 0 // 重置偏移量
}
}
// 只处理写入事件
if event.Op&fsnotify.Write == fsnotify.Write {
m.handleFileWrite()
}
case err, ok := <-m.watcher.Errors:
if !ok {
return
}
slog.Warn("文件监控错误", "error", err)
}
}
}
// handleFileWrite 处理文件写入事件
func (m *Monitor) handleFileWrite() {
// 打开文件,从上次读取位置继续读
f, err := os.Open(m.logFile)
if err != nil {
slog.Warn("打开日志文件失败", "error", err)
return
}
defer f.Close()
// 移动到上次读取位置
if _, err := f.Seek(m.lastOffset, 0); err != nil {
slog.Warn("移动文件位置失败", "error", err)
return
}
// 解析新增的日志行
events, newOffset, err := m.parser.ParseFromReader(f, m.lastOffset)
if err != nil {
slog.Warn("解析日志失败", "error", err)
return
}
// 更新偏移量
m.lastOffset = newOffset
// 发送事件
for _, event := range events {
select {
case m.eventCh <- event:
slog.Info("检测到SSH登录", "user", event.Username, "ip", event.IP, "status", event.Status)
default:
slog.Warn("事件队列已满,丢弃事件")
}
}
}
// readExistingLogs 读取现有日志(Agent重启时)
func (m *Monitor) readExistingLogs() error {
f, err := os.Open(m.logFile)
if err != nil {
return err
}
defer f.Close()
// 从文件开头读取所有日志
events, newOffset, err := m.parser.ParseFromReader(f, 0)
if err != nil {
return err
}
m.lastOffset = newOffset
// 只上报最近的登录(例如最近100条),避免重复上报历史数据
startIdx := len(events) - 100
if startIdx < 0 {
startIdx = 0
}
for i := startIdx; i < len(events); i++ {
select {
case m.eventCh <- events[i]:
default:
break
}
}
return nil
}