From 72e0adc1bf2b9119dbb8ca8be68ade9f194653bc Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 30 Mar 2025 19:31:55 -0400 Subject: [PATCH] Monitor connection with pings and keep pining Resolves #24 --- main.go | 160 +++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 125 insertions(+), 35 deletions(-) diff --git a/main.go b/main.go index cf3f062..eefaf29 100644 --- a/main.go +++ b/main.go @@ -115,7 +115,12 @@ func ping(tnet *netstack.Net, dst string) error { } func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) { - ticker := time.NewTicker(10 * time.Second) + initialInterval := 10 * time.Second + maxInterval := 60 * time.Second + currentInterval := initialInterval + consecutiveFailures := 0 + + ticker := time.NewTicker(currentInterval) defer ticker.Stop() go func() { @@ -124,8 +129,34 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) case <-ticker.C: err := ping(tnet, serverIP) if err != nil { - logger.Warn("Periodic ping failed: %v", err) + consecutiveFailures++ + logger.Warn("Periodic ping failed (%d consecutive failures): %v", + consecutiveFailures, err) logger.Warn("HINT: Do you have UDP port 51820 (or the port in config.yml) open on your Pangolin server?") + + // Increase interval if we have consistent failures, with a maximum cap + if consecutiveFailures >= 3 && currentInterval < maxInterval { + // Increase by 50% each time, up to the maximum + currentInterval = time.Duration(float64(currentInterval) * 1.5) + if currentInterval > maxInterval { + currentInterval = maxInterval + } + ticker.Reset(currentInterval) + logger.Info("Increased ping check interval to %v due to consecutive failures", + currentInterval) + } + } else { + // On success, if we've backed off, gradually return to normal interval + if currentInterval > initialInterval { + currentInterval = time.Duration(float64(currentInterval) * 0.8) + if currentInterval < initialInterval { + currentInterval = initialInterval + } + ticker.Reset(currentInterval) + logger.Info("Decreased ping check interval to %v after successful ping", + currentInterval) + } + consecutiveFailures = 0 } case <-stopChan: logger.Info("Stopping ping check") @@ -135,34 +166,97 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) }() } +// Function to track connection status and trigger reconnection as needed +func monitorConnectionStatus(tnet *netstack.Net, serverIP string, client *websocket.Client) { + const checkInterval = 30 * time.Second + connectionLost := false + ticker := time.NewTicker(checkInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + // Try a ping to see if connection is alive + err := ping(tnet, serverIP) + + if err != nil && !connectionLost { + // We just lost connection + connectionLost = true + logger.Warn("Connection to server lost. Continuous reconnection attempts will be made.") + + // Notify the user they might need to check their network + logger.Warn("Please check your internet connection and ensure the Pangolin server is online.") + logger.Warn("Newt will continue reconnection attempts automatically when connectivity is restored.") + } else if err == nil && connectionLost { + // Connection has been restored + connectionLost = false + logger.Info("Connection to server restored!") + + // Tell the server we're back + err := client.SendMessage("newt/wg/register", map[string]interface{}{ + "publicKey": fmt.Sprintf("%s", privateKey.PublicKey()), + }) + + if err != nil { + logger.Error("Failed to send registration message after reconnection: %v", err) + } else { + logger.Info("Successfully re-registered with server after reconnection") + } + } + } + } +} + func pingWithRetry(tnet *netstack.Net, dst string) error { const ( - maxAttempts = 15 - retryDelay = 2 * time.Second + initialMaxAttempts = 15 + initialRetryDelay = 2 * time.Second + maxRetryDelay = 60 * time.Second // Cap the maximum delay ) - var lastErr error - for attempt := 1; attempt <= maxAttempts; attempt++ { - logger.Info("Ping attempt %d of %d", attempt, maxAttempts) - - if err := ping(tnet, dst); err != nil { - lastErr = err - logger.Warn("Ping attempt %d failed: %v", attempt, err) - - if attempt < maxAttempts { - time.Sleep(retryDelay) - continue - } - return fmt.Errorf("all ping attempts failed after %d tries, last error: %w", - maxAttempts, lastErr) - } + attempt := 1 + retryDelay := initialRetryDelay + // First try with the initial parameters + logger.Info("Ping attempt %d", attempt) + if err := ping(tnet, dst); err == nil { // Successful ping return nil + } else { + logger.Warn("Ping attempt %d failed: %v", attempt, err) } - // This shouldn't be reached due to the return in the loop, but added for completeness - return fmt.Errorf("unexpected error: all ping attempts failed") + // Start a goroutine that will attempt pings indefinitely with increasing delays + go func() { + attempt = 2 // Continue from attempt 2 + + for { + logger.Info("Ping attempt %d", attempt) + + if err := ping(tnet, dst); err != nil { + logger.Warn("Ping attempt %d failed: %v", attempt, err) + + // Increase delay after certain thresholds but cap it + if attempt%5 == 0 && retryDelay < maxRetryDelay { + retryDelay = time.Duration(float64(retryDelay) * 1.5) + if retryDelay > maxRetryDelay { + retryDelay = maxRetryDelay + } + logger.Info("Increasing ping retry delay to %v", retryDelay) + } + + time.Sleep(retryDelay) + attempt++ + } else { + // Successful ping + logger.Info("Ping succeeded after %d attempts", attempt) + return + } + } + }() + + // Return an error for the first batch of attempts (to maintain compatibility with existing code) + return fmt.Errorf("initial ping attempts failed, continuing in background") } func parseLogLevel(level string) logger.LogLevel { @@ -353,13 +447,8 @@ func main() { if connected { logger.Info("Already connected! But I will send a ping anyway...") - // ping(tnet, wgData.ServerIP) - err = pingWithRetry(tnet, wgData.ServerIP) - if err != nil { - // Handle complete failure after all retries - logger.Warn("Failed to ping %s: %v", wgData.ServerIP, err) - logger.Warn("HINT: Do you have UDP port 51820 (or the port in config.yml) open on your Pangolin server?") - } + // Even if pingWithRetry returns an error, it will continue trying in the background + _ = pingWithRetry(tnet, wgData.ServerIP) // Ignoring initial error as pings will continue return } @@ -414,17 +503,18 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey( } logger.Info("WireGuard device created. Lets ping the server now...") - // Ping to bring the tunnel up on the server side quickly - // ping(tnet, wgData.ServerIP) - err = pingWithRetry(tnet, wgData.ServerIP) - if err != nil { - // Handle complete failure after all retries - logger.Error("Failed to ping %s: %v", wgData.ServerIP, err) - } + // Even if pingWithRetry returns an error, it will continue trying in the background + _ = pingWithRetry(tnet, wgData.ServerIP) + + // Always mark as connected and start the proxy manager regardless of initial ping result + // as the pings will continue in the background if !connected { logger.Info("Starting ping check") startPingCheck(tnet, wgData.ServerIP, pingStopChan) + + // Start connection monitoring in a separate goroutine + go monitorConnectionStatus(tnet, wgData.ServerIP, client) } // Create proxy manager