mirror of
https://github.com/fosrl/olm.git
synced 2026-09-22 05:49:10 +02:00
137 lines
4.0 KiB
Go
137 lines
4.0 KiB
Go
package olm
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/fosrl/newt/logger"
|
|
)
|
|
|
|
// WatchdogConfig configures the DNS override watchdog. The watchdog runs as
|
|
// an external process (spawned via SpawnWatchdog) and monitors a parent olm
|
|
// process. When the parent appears to have died without restoring DNS, the
|
|
// watchdog forcibly resets the system DNS configuration.
|
|
type WatchdogConfig struct {
|
|
// ParentPID is the PID of the olm process that installed the DNS
|
|
// override. The watchdog exits when this PID is no longer alive.
|
|
ParentPID int
|
|
|
|
// SocketPath is the path to the olm Unix domain socket (or named pipe
|
|
// on Windows). The watchdog uses it as a secondary liveness signal.
|
|
// May be empty if no socket-based API is enabled.
|
|
SocketPath string
|
|
|
|
// InterfaceName is the name of the WireGuard interface whose DNS
|
|
// override should be reset on parent death.
|
|
InterfaceName string
|
|
|
|
// CheckInterval is how often to poll the parent's liveness.
|
|
// Defaults to 5 seconds when zero.
|
|
CheckInterval time.Duration
|
|
|
|
// FailureThreshold is the number of consecutive failed liveness checks
|
|
// before the watchdog declares the parent dead and resets DNS.
|
|
// Defaults to 3 when zero.
|
|
FailureThreshold int
|
|
}
|
|
|
|
// RunWatchdog runs the watchdog loop in the current process until either
|
|
// (a) the parent dies and DNS is reset, or (b) ctx is cancelled.
|
|
func RunWatchdog(ctx context.Context, cfg WatchdogConfig) error {
|
|
if cfg.ParentPID <= 0 {
|
|
return fmt.Errorf("watchdog: invalid parent PID %d", cfg.ParentPID)
|
|
}
|
|
if cfg.CheckInterval <= 0 {
|
|
cfg.CheckInterval = 5 * time.Second
|
|
}
|
|
if cfg.FailureThreshold <= 0 {
|
|
cfg.FailureThreshold = 3
|
|
}
|
|
|
|
logger.Info("DNS watchdog started: parent=%d interval=%s threshold=%d socket=%q interface=%q",
|
|
cfg.ParentPID, cfg.CheckInterval, cfg.FailureThreshold, cfg.SocketPath, cfg.InterfaceName)
|
|
|
|
ticker := time.NewTicker(cfg.CheckInterval)
|
|
defer ticker.Stop()
|
|
|
|
consecutiveFailures := 0
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
logger.Info("DNS watchdog context cancelled, exiting cleanly")
|
|
return ctx.Err()
|
|
case <-ticker.C:
|
|
}
|
|
|
|
alive := isParentAlive(cfg.ParentPID, cfg.SocketPath)
|
|
if alive {
|
|
if consecutiveFailures > 0 {
|
|
logger.Debug("DNS watchdog: parent recovered after %d failures", consecutiveFailures)
|
|
}
|
|
consecutiveFailures = 0
|
|
continue
|
|
}
|
|
|
|
consecutiveFailures++
|
|
logger.Warn("DNS watchdog: parent liveness check failed (%d/%d)",
|
|
consecutiveFailures, cfg.FailureThreshold)
|
|
|
|
if consecutiveFailures >= cfg.FailureThreshold {
|
|
logger.Warn("DNS watchdog: parent declared dead, forcing DNS reset")
|
|
if err := ForceResetDNS(cfg.InterfaceName); err != nil {
|
|
logger.Error("DNS watchdog: ForceResetDNS failed: %v", err)
|
|
return err
|
|
}
|
|
logger.Info("DNS watchdog: DNS reset complete, exiting")
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// isParentAlive returns true if the parent process appears to be alive. It
|
|
// considers the parent alive if EITHER the PID is still running OR the
|
|
// socket-based health endpoint responds. This dual check avoids false
|
|
// positives where one signal is flaky (e.g., socket blocked but process
|
|
// still recovering).
|
|
func isParentAlive(pid int, socketPath string) bool {
|
|
if pidAlive(pid) {
|
|
return true
|
|
}
|
|
// Process is gone; double-check via socket to avoid races where PID
|
|
// recycling or signal-0 quirks lie to us. Socket should already be
|
|
// gone too.
|
|
if socketPath != "" && socketHealthy(socketPath) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// socketHealthy attempts a fast /health request over the unix socket.
|
|
func socketHealthy(socketPath string) bool {
|
|
if _, err := os.Stat(socketPath); err != nil {
|
|
return false
|
|
}
|
|
|
|
client := &http.Client{
|
|
Timeout: 2 * time.Second,
|
|
Transport: &http.Transport{
|
|
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
|
d := net.Dialer{Timeout: 2 * time.Second}
|
|
return d.DialContext(ctx, "unix", socketPath)
|
|
},
|
|
},
|
|
}
|
|
|
|
resp, err := client.Get("http://localhost/health")
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer resp.Body.Close()
|
|
return resp.StatusCode == http.StatusOK
|
|
}
|