mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-19 22:17:42 +02:00
152 lines
4.5 KiB
Go
152 lines
4.5 KiB
Go
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))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
sessions, err := w.manager.DB().ListC2Sessions(database.ListC2SessionsFilter{Status: string(SessionDead)})
|
||
if err != nil {
|
||
w.logger.Warn("离线任务查询失败", zap.Error(err))
|
||
return
|
||
}
|
||
for _, session := range sessions {
|
||
w.expireOfflineCommands(session, now)
|
||
}
|
||
}
|
||
|
||
func (w *SessionWatchdog) expireOfflineCommands(session *database.C2Session, now time.Time) {
|
||
tasks, err := w.manager.DB().ListC2Tasks(database.ListC2TasksFilter{SessionID: session.ID, Status: string(TaskSent)})
|
||
if err != nil {
|
||
w.logger.Warn("离线命令查询失败", zap.Error(err))
|
||
return
|
||
}
|
||
for _, task := range tasks {
|
||
if task.TaskType != string(TaskTypeExec) && task.TaskType != string(TaskTypeShell) {
|
||
continue
|
||
}
|
||
if task.SentAt == nil {
|
||
continue
|
||
}
|
||
seconds := 60.0
|
||
if value, ok := task.Payload["timeout_seconds"].(float64); ok && value > 0 {
|
||
seconds = value
|
||
}
|
||
// Invalid huge deadlines remain unexpired rather than wrapping a duration.
|
||
if seconds > float64((1<<63-1)/int64(time.Second)) {
|
||
continue
|
||
}
|
||
if now.Sub(*task.SentAt) <= time.Duration(seconds)*time.Second+w.minGrace {
|
||
continue
|
||
}
|
||
status := string(TaskFailed)
|
||
errText := "会话已离线且任务结果超过执行期限;远端进程是否终止尚未确认"
|
||
if err := w.manager.DB().UpdateC2Task(task.ID, database.C2TaskUpdate{ExpectedStatus: &task.Status, Status: &status, Error: &errText, CompletedAt: &now}); err != nil {
|
||
w.logger.Warn("收尾离线命令失败", zap.Error(err))
|
||
continue
|
||
}
|
||
w.manager.publishEvent("warn", "task", session.ID, task.ID, errText, nil)
|
||
}
|
||
}
|
||
|
||
// 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
|
||
}
|