From 9d80161ab7bc86c4be1ab1a4fc1f5f0671dc0360 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 21 Mar 2025 17:23:10 -0400 Subject: [PATCH 1/3] Increases ping attempts to 15 Might help #7 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 32e1fc1..cf3f062 100644 --- a/main.go +++ b/main.go @@ -137,7 +137,7 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) func pingWithRetry(tnet *netstack.Net, dst string) error { const ( - maxAttempts = 5 + maxAttempts = 15 retryDelay = 2 * time.Second ) From b3e8bf7d12f627f275aaa8cdbb2fbea3d6645ad7 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 30 Mar 2025 10:52:07 -0400 Subject: [PATCH 2/3] Add LOGGER_TIMEZONE env to control the time zone Closes #23 If the name is "" or "UTC", LoadLocation returns UTC. If the name is "Local", LoadLocation returns Local. Otherwise, the name is taken to be a location name corresponding to a file in the IANA Time Zone database, such as "America/New_York". LoadLocation looks for the IANA Time Zone database in the following locations in order: the directory or uncompressed zip file named by the ZONEINFO environment variable on a Unix system, the system standard installation location $GOROOT/lib/time/zoneinfo.zip the time/tzdata package, if it was imported --- logger/logger.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/logger/logger.go b/logger/logger.go index 9ef486d..f033de9 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -53,7 +53,23 @@ func (l *Logger) log(level LogLevel, format string, args ...interface{}) { if level < l.level { return } - timestamp := time.Now().Format("2006/01/02 15:04:05") + + // Get timezone from environment variable or use local timezone + timezone := os.Getenv("LOGGER_TIMEZONE") + var location *time.Location + var err error + + if timezone != "" { + location, err = time.LoadLocation(timezone) + if err != nil { + // If invalid timezone, fall back to local + location = time.Local + } + } else { + location = time.Local + } + + timestamp := time.Now().In(location).Format("2006/01/02 15:04:05") message := fmt.Sprintf(format, args...) l.logger.Printf("%s: %s %s", level.String(), timestamp, message) } From 72e0adc1bf2b9119dbb8ca8be68ade9f194653bc Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 30 Mar 2025 19:31:55 -0400 Subject: [PATCH 3/3] 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