package c2 import ( "context" "time" "cyberstrike-ai/internal/database" "go.uber.org/zap" ) // SessionWatchdog 会话心跳看门狗:周期扫描所有 active/sleeping 会话, // 把超过 (sleep * (1 + jitter%) * graceFactor + minGrace) 仍未心跳的标为 dead。 // // 设计要点: // - 单 goroutine + ticker,避免对每个会话开 timer,session 数量大时也线性 OK; // - 阈值随会话自身 sleep/jitter 自适应(sleep=300s 的会话不能用 sleep=5s 的判定); // - 全局最小宽限期 minGrace 避免 sleep 配置错误的会话被误判; // - 不读 implant_uuid,纯按 last_check_in 字段,与 listener 类型解耦。 type SessionWatchdog struct { manager *Manager logger *zap.Logger interval time.Duration // 扫描周期,默认 15s minGrace time.Duration // 最小宽限期,默认 30s gracePct float64 // 心跳超时倍数,默认 3.0(即 3 倍 sleep 周期没心跳算掉线) stopCh chan struct{} } // NewSessionWatchdog 创建看门狗 func NewSessionWatchdog(m *Manager) *SessionWatchdog { return &SessionWatchdog{ manager: m, logger: m.Logger().With(zap.String("component", "c2-watchdog")), interval: 15 * time.Second, minGrace: 30 * time.Second, gracePct: 3.0, stopCh: make(chan struct{}), } } // Run 阻塞执行,直到 ctx.Done() 或 Stop() func (w *SessionWatchdog) Run(ctx context.Context) { t := time.NewTicker(w.interval) defer t.Stop() for { select { case <-ctx.Done(): return case <-w.stopCh: return case <-t.C: w.tick() } } } // Stop 停止 func (w *SessionWatchdog) Stop() { select { case <-w.stopCh: default: close(w.stopCh) } } func (w *SessionWatchdog) tick() { now := time.Now() for _, status := range []string{string(SessionActive), string(SessionSleeping)} { sessions, err := w.manager.DB().ListC2Sessions(database.ListC2SessionsFilter{Status: status}) if err != nil { w.logger.Warn("watchdog 列表查询失败", zap.Error(err)) continue } for _, s := range sessions { if w.isStale(s, now) { if err := w.manager.MarkSessionDead(s.ID); err != nil { w.logger.Warn("标记会话掉线失败", zap.String("session_id", s.ID), zap.Error(err)) } } } } } // isStale 判断会话是否超时 func (w *SessionWatchdog) isStale(s *database.C2Session, now time.Time) bool { // 无心跳记录:以 first_seen_at 兜底 last := s.LastCheckIn if last.IsZero() { last = s.FirstSeenAt } sleep := s.SleepSeconds if sleep <= 0 { // TCP reverse 模式 sleep=0 → 用最小宽限期判定 return now.Sub(last) > w.minGrace*2 } jitter := s.JitterPercent if jitter < 0 { jitter = 0 } if jitter > 100 { jitter = 100 } // 阈值 = sleep * (1 + jitter%) * gracePct,再加 minGrace 兜底 expected := time.Duration(float64(sleep)*(1+float64(jitter)/100.0)*w.gracePct) * time.Second if expected < w.minGrace { expected = w.minGrace } return now.Sub(last) > expected }