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) }