commit 1967b1a92e6abfbe6ed2bfe99a5da787aa14eb36 Author: db1234719 Date: Sun Feb 15 01:44:38 2026 +0330 added the base which is ai code and this will be tailored (and have been a little) for specifice yejayekhoob.com use case and tested extensively under short period of time diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..18f2a49 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dns-failover diff --git a/bind.go b/bind.go new file mode 100644 index 0000000..073187f --- /dev/null +++ b/bind.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + "os" + "os/exec" +) + +func SwitchBindConfig(source, target string) error { + input, err := os.ReadFile(source) + if err != nil { + return fmt.Errorf("read source config: %w", err) + } + + err = os.WriteFile(target, input, 0o644) + if err != nil { + return fmt.Errorf("write active config: %w", err) + } + + cmd := exec.Command("rndc", "reload") + err = cmd.Run() + if err != nil { + return fmt.Errorf("reload bind: %w", err) + } + + return nil +} diff --git a/dns-failover.service b/dns-failover.service new file mode 100644 index 0000000..2efb97b --- /dev/null +++ b/dns-failover.service @@ -0,0 +1,13 @@ +[Unit] +Description=DNS Failover Watchdog by db123 +After=network.target + +[Service] +ExecStart=/usr/local/bin/dns-failover -ip -v +Restart=always +RestartSec=5 +User=root + +[Install] +WantedBy=multi-user.target + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b4a8dbe --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module dns-failover + +go 1.22 diff --git a/main.go b/main.go new file mode 100644 index 0000000..6f34d33 --- /dev/null +++ b/main.go @@ -0,0 +1,151 @@ +package main + +import ( + "context" + "flag" + "log" + "net" + "net/http" + "os" + "time" +) + +type Config struct { + HealthURL string + StaticIP string + NormalInterval time.Duration + FailInterval time.Duration + Timeout time.Duration + MaxRetries int + BindPrimaryConf string + BindFailoverConf string + BindActiveConf string + Verbose bool +} + +type State int + +const ( + StatePrimary State = iota + StateFailover +) + +func main() { + cfg := parseFlags() + + logger := log.New(os.Stdout, "", log.LstdFlags) + if !cfg.Verbose { + logger.SetOutput(os.Stdout) + logger.SetFlags(0) + } + + currentState := StatePrimary + failureCount := 0 + successCount := 0 + + for { + healthy := checkHealth(cfg) + + if currentState == StatePrimary { + if healthy { + failureCount = 0 + sleep(cfg.NormalInterval) + continue + } + + failureCount++ + if cfg.Verbose { + logger.Println("Health check failed:", failureCount) + } + + if failureCount >= cfg.MaxRetries { + logger.Println("Primary confirmed DOWN. Switching to failover.") + err := SwitchBindConfig(cfg.BindFailoverConf, cfg.BindActiveConf) + if err != nil { + logger.Println("Failed to switch BIND config:", err) + } + currentState = StateFailover + failureCount = 0 + } + + sleep(cfg.FailInterval) + continue + } + + // FAILOVER MODE + if healthy { + successCount++ + if cfg.Verbose { + logger.Println("Primary recovery attempt:", successCount) + } + + if successCount >= cfg.MaxRetries { + logger.Println("Primary confirmed UP. Reverting DNS.") + err := SwitchBindConfig(cfg.BindPrimaryConf, cfg.BindActiveConf) + if err != nil { + logger.Println("Failed to revert BIND config:", err) + } + currentState = StatePrimary + successCount = 0 + } + } else { + successCount = 0 + } + + sleep(cfg.NormalInterval) + } +} + +func parseFlags() *Config { + cfg := &Config{} + + flag.StringVar(&cfg.HealthURL, "url", "/nginx-health", "Health endpoint path") + flag.StringVar(&cfg.StaticIP, "ip", "", "Static IP to check") + flag.DurationVar(&cfg.NormalInterval, "interval", 120*time.Second, "Normal check interval") + flag.DurationVar(&cfg.FailInterval, "fail-interval", 10*time.Second, "Interval during failure detection") + flag.DurationVar(&cfg.Timeout, "timeout", 5*time.Second, "HTTP timeout") + flag.IntVar(&cfg.MaxRetries, "retries", 5, "Consecutive retries before state change") + flag.StringVar(&cfg.BindPrimaryConf, "primary-conf", "/etc/bind/db.primary", "Primary zone config") + flag.StringVar(&cfg.BindFailoverConf, "failover-conf", "/etc/bind/db.failover", "Failover zone config") + flag.StringVar(&cfg.BindActiveConf, "active-conf", "/etc/bind/db.active", "Active zone config path") + flag.BoolVar(&cfg.Verbose, "v", false, "Verbose logging") + + flag.Parse() + + if cfg.StaticIP == "" { + log.Fatal("Static IP must be provided") + } + + return cfg +} + +func checkHealth(cfg *Config) bool { + ctx, cancel := context.WithTimeout(context.Background(), cfg.Timeout) + defer cancel() + + dialer := &net.Dialer{} + transport := &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return dialer.DialContext(ctx, "tcp", net.JoinHostPort(cfg.StaticIP, "80")) + }, + } + + client := &http.Client{ + Transport: transport, + Timeout: cfg.Timeout, + } + + req, _ := http.NewRequestWithContext(ctx, "GET", "http://"+cfg.StaticIP+cfg.HealthURL, nil) + + resp, err := client.Do(req) + if err != nil { + return false + } + defer resp.Body.Close() + + return resp.StatusCode == 200 +} + +func sleep(d time.Duration) { + time.Sleep(d) +}