From 1bd203c5555046b7ee4fbfe2f822eb3d03571ad7 Mon Sep 17 00:00:00 2001 From: SergeiEU <39683682+SergeiEU@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:17:15 +0400 Subject: Initial import --- internal/engine/watchdog.go | 142 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 internal/engine/watchdog.go (limited to 'internal/engine/watchdog.go') diff --git a/internal/engine/watchdog.go b/internal/engine/watchdog.go new file mode 100644 index 0000000..899f81f --- /dev/null +++ b/internal/engine/watchdog.go @@ -0,0 +1,142 @@ +package engine + +import ( + "context" + "io" + "log" + "net/http" + "strings" + "time" + + "vpnem/internal/config" + "vpnem/internal/models" +) + +// WatchdogConfig holds watchdog parameters. +type WatchdogConfig struct { + CheckInterval time.Duration // how often to check sing-box is alive (default 2s) + DeepCheckInterval time.Duration // how often to verify exit IP (default 30s) + ReconnectCooldown time.Duration // min time between reconnect attempts (default 5s) +} + +// DefaultWatchdogConfig returns the default watchdog settings (from vpn.py). +func DefaultWatchdogConfig() WatchdogConfig { + return WatchdogConfig{ + CheckInterval: 2 * time.Second, + DeepCheckInterval: 30 * time.Second, + ReconnectCooldown: 5 * time.Second, + } +} + +// Watchdog monitors sing-box and auto-reconnects on failure. +type Watchdog struct { + engine *Engine + cfg WatchdogConfig + cancel context.CancelFunc + running bool + + // Reconnect parameters (set via StartWatching) + server models.Server + mode config.Mode + ruleSets []models.RuleSet + serverIPs []string +} + +// NewWatchdog creates a new watchdog for the given engine. +func NewWatchdog(engine *Engine, cfg WatchdogConfig) *Watchdog { + return &Watchdog{ + engine: engine, + cfg: cfg, + } +} + +// StartWatching begins monitoring. It stores the connection params for reconnection. +func (w *Watchdog) StartWatching(server models.Server, mode config.Mode, ruleSets []models.RuleSet, serverIPs []string) { + w.StopWatching() + + w.server = server + w.mode = mode + w.ruleSets = ruleSets + w.serverIPs = serverIPs + + ctx, cancel := context.WithCancel(context.Background()) + w.cancel = cancel + w.running = true + + go w.loop(ctx) +} + +// StopWatching stops the watchdog. +func (w *Watchdog) StopWatching() { + if w.cancel != nil { + w.cancel() + } + w.running = false +} + +// IsWatching returns whether the watchdog is active. +func (w *Watchdog) IsWatching() bool { + return w.running +} + +func (w *Watchdog) loop(ctx context.Context) { + ticker := time.NewTicker(w.cfg.CheckInterval) + defer ticker.Stop() + + deepTicker := time.NewTicker(w.cfg.DeepCheckInterval) + defer deepTicker.Stop() + + lastReconnect := time.Time{} + + for { + select { + case <-ctx.Done(): + return + + case <-ticker.C: + if !w.engine.IsRunning() { + if time.Since(lastReconnect) < w.cfg.ReconnectCooldown { + continue + } + log.Println("watchdog: sing-box not running, reconnecting...") + if err := w.engine.Start(w.server, w.mode, w.ruleSets, w.serverIPs); err != nil { + log.Printf("watchdog: reconnect failed: %v", err) + } else { + log.Println("watchdog: reconnected successfully") + } + lastReconnect = time.Now() + } + + case <-deepTicker.C: + if !w.engine.IsRunning() { + continue + } + ip := checkExitIP() + if ip == "" { + log.Println("watchdog: deep check failed (no exit IP), restarting...") + if time.Since(lastReconnect) < w.cfg.ReconnectCooldown { + continue + } + if err := w.engine.Restart(w.server, w.mode, w.ruleSets, w.serverIPs); err != nil { + log.Printf("watchdog: restart failed: %v", err) + } + lastReconnect = time.Now() + } + } + } +} + +func checkExitIP() string { + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Get("http://ifconfig.me/ip") + if err != nil { + return "" + } + defer resp.Body.Close() + + body, err := io.ReadAll(io.LimitReader(resp.Body, 64)) + if err != nil { + return "" + } + return strings.TrimSpace(string(body)) +} -- cgit v1.2.3