From 678d82fa68068e18430c1c294a778c8ca026bf33 Mon Sep 17 00:00:00 2001 From: Wouter van Elten Date: Wed, 25 Jun 2025 19:30:05 +0200 Subject: [PATCH 1/6] added healthy check in main.go extended the ping check that creates a /tmp/healthy file if ping successfull and removes that file if ping failes 3 times. With this you can add the following to the newt docker compose to do the health check: healthcheck: test: ["CMD-SHELL", "test -f /tmp/healthy"] interval: 30s timeout: 10s retries: 3 --- main.go | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/main.go b/main.go index fdece97..cb4eab4 100644 --- a/main.go +++ b/main.go @@ -127,42 +127,46 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) go func() { for { select { - case <-ticker.C: + case <-ticker.C: err := ping(tnet, serverIP) if err != nil { consecutiveFailures++ - logger.Warn("Periodic ping failed (%d consecutive failures): %v", - consecutiveFailures, err) + 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 + // delete healthy file if failed 3 times + if consecutiveFailures >= 3 { + _ = os.Remove("/tmp/healthy") + } + // increase interval if it keeps failing 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) + 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 + // Write a healthy file if ping successfull + err := os.WriteFile("/tmp/healthy", []byte("ok"), 0644) + if err != nil { + logger.Warn("Failed to write health file: %v", err) + } + // Reset interval if we increased it 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) + logger.Info("Decreased ping check interval to %v after successful ping", currentInterval) } consecutiveFailures = 0 } - case <-stopChan: + case <-stopChan: logger.Info("Stopping ping check") return - } + } } }() } From a76e6c9637ddd28b169b2ca898ebe69abb53b7a9 Mon Sep 17 00:00:00 2001 From: Wouter van Elten Date: Wed, 25 Jun 2025 19:43:27 +0200 Subject: [PATCH 2/6] added healthy check in main.go added healthy check in main.go extended the ping check that creates a /tmp/healthy file if ping successfull and removes that file if ping failes 3 times. With this you can add the following to the newt docker compose to do the health check: healthcheck: test: ["CMD-SHELL", "test -f /tmp/healthy"] interval: 30s timeout: 10s retries: 3 --- main.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index cb4eab4..e8b8ede 100644 --- a/main.go +++ b/main.go @@ -127,7 +127,7 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) go func() { for { select { - case <-ticker.C: + case <-ticker.C: err := ping(tnet, serverIP) if err != nil { consecutiveFailures++ @@ -137,8 +137,9 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) if consecutiveFailures >= 3 { _ = os.Remove("/tmp/healthy") } - // increase interval if it keeps failing + // 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 @@ -152,7 +153,7 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) if err != nil { logger.Warn("Failed to write health file: %v", err) } - // Reset interval if we increased it + // 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 { @@ -163,9 +164,9 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) } consecutiveFailures = 0 } - case <-stopChan: + case <-stopChan: logger.Info("Stopping ping check") - return + return } } }() From e357e7befbc9eddfe09e694b1fedc02406504c90 Mon Sep 17 00:00:00 2001 From: Wouter van Elten Date: Mon, 30 Jun 2025 12:45:42 +0200 Subject: [PATCH 3/6] Update main.go Added cli and env function --- main.go | 403 ++++---------------------------------------------------- 1 file changed, 27 insertions(+), 376 deletions(-) diff --git a/main.go b/main.go index e8b8ede..116e6e5 100644 --- a/main.go +++ b/main.go @@ -50,16 +50,11 @@ type TargetData struct { } func fixKey(key string) string { - // Remove any whitespace key = strings.TrimSpace(key) - - // Decode from base64 decoded, err := base64.StdEncoding.DecodeString(key) if err != nil { logger.Fatal("Error decoding base64: %v", err) } - - // Convert to hex return hex.EncodeToString(decoded) } @@ -115,7 +110,8 @@ func ping(tnet *netstack.Net, dst string) error { return nil } -func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) { +// --- CHANGED: added healthFile as parameter --- +func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}, healthFile string) { initialInterval := 10 * time.Second maxInterval := 60 * time.Second currentInterval := initialInterval @@ -133,13 +129,12 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) 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?") - // delete healthy file if failed 3 times - if consecutiveFailures >= 3 { - _ = os.Remove("/tmp/healthy") + // --- CHANGED: Only remove file if healthFile is set --- + if consecutiveFailures >= 3 && healthFile != "" { + _ = os.Remove(healthFile) } // 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 @@ -148,10 +143,12 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) logger.Info("Increased ping check interval to %v due to consecutive failures", currentInterval) } } else { - // Write a healthy file if ping successfull - err := os.WriteFile("/tmp/healthy", []byte("ok"), 0644) - if err != nil { - logger.Warn("Failed to write health file: %v", err) + // --- CHANGED: Only write file if healthFile is set --- + if healthFile != "" { + err := os.WriteFile(healthFile, []byte("ok"), 0644) + if err != nil { + logger.Warn("Failed to write health file: %v", err) + } } // On success, if we've backed off, gradually return to normal interval if currentInterval > initialInterval { @@ -166,13 +163,12 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}) } case <-stopChan: logger.Info("Stopping ping check") - return - } + return + } } }() } -// 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 @@ -182,27 +178,18 @@ func monitorConnectionStatus(tnet *netstack.Net, serverIP string, client *websoc 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": privateKey.PublicKey().String(), }) - if err != nil { logger.Error("Failed to send registration message after reconnection: %v", err) } else { @@ -217,32 +204,25 @@ func pingWithRetry(tnet *netstack.Net, dst string) error { const ( initialMaxAttempts = 15 initialRetryDelay = 2 * time.Second - maxRetryDelay = 60 * time.Second // Cap the maximum delay + maxRetryDelay = 60 * time.Second ) 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) } - // Start a goroutine that will attempt pings indefinitely with increasing delays go func() { - attempt = 2 // Continue from attempt 2 - + 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 { @@ -250,18 +230,14 @@ func pingWithRetry(tnet *netstack.Net, dst string) error { } 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") } @@ -278,7 +254,7 @@ func parseLogLevel(level string) logger.LogLevel { case "FATAL": return logger.FATAL default: - return logger.INFO // default to INFO if invalid level provided + return logger.INFO } } @@ -286,8 +262,6 @@ func mapToWireGuardLogLevel(level logger.LogLevel) int { switch level { case logger.DEBUG: return device.LogLevelVerbose - // case logger.INFO: - // return device.LogLevel case logger.WARN: return device.LogLevelError case logger.ERROR, logger.FATAL: @@ -298,32 +272,23 @@ func mapToWireGuardLogLevel(level logger.LogLevel) int { } func resolveDomain(domain string) (string, error) { - // Check if there's a port in the domain host, port, err := net.SplitHostPort(domain) if err != nil { - // No port found, use the domain as is host = domain port = "" } - - // Remove any protocol prefix if present if strings.HasPrefix(host, "http://") { host = strings.TrimPrefix(host, "http://") } else if strings.HasPrefix(host, "https://") { host = strings.TrimPrefix(host, "https://") } - - // Lookup IP addresses ips, err := net.LookupIP(host) if err != nil { return "", fmt.Errorf("DNS lookup failed: %v", err) } - if len(ips) == 0 { return "", fmt.Errorf("no IP addresses found for domain %s", host) } - - // Get the first IPv4 address if available var ipAddr string for _, ip := range ips { if ipv4 := ip.To4(); ipv4 != nil { @@ -331,20 +296,16 @@ func resolveDomain(domain string) (string, error) { break } } - - // If no IPv4 found, use the first IP (might be IPv6) if ipAddr == "" { ipAddr = ips[0].String() } - - // Add port back if it existed if port != "" { ipAddr = net.JoinHostPort(ipAddr, port) } - return ipAddr, nil } +// --- ADDED: healthFile variable --- var ( endpoint string id string @@ -358,10 +319,10 @@ var ( updownScript string tlsPrivateKey string dockerSocket string + healthFile string // NEW ) func main() { - // if PANGOLIN_ENDPOINT, NEWT_ID, and NEWT_SECRET are set as environment variables, they will be used as default values endpoint = os.Getenv("PANGOLIN_ENDPOINT") id = os.Getenv("NEWT_ID") secret = os.Getenv("NEWT_SECRET") @@ -371,6 +332,7 @@ func main() { updownScript = os.Getenv("UPDOWN_SCRIPT") tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT") dockerSocket = os.Getenv("DOCKER_SOCKET") + healthFile = os.Getenv("HEALTH_FILE") // NEW if endpoint == "" { flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server") @@ -399,10 +361,12 @@ func main() { if dockerSocket == "" { flag.StringVar(&dockerSocket, "docker-socket", "", "Path to Docker socket (typically /var/run/docker.sock)") } + // --- ADDED: CLI flag for healthFile if not set by env --- + if healthFile == "" { + flag.StringVar(&healthFile, "health-file", "", "Path to health file (if unset, health file won’t be written)") + } - // do a --version check version := flag.Bool("version", false, "Print the version") - flag.Parse() newtVersion := "Newt version replaceme" @@ -417,7 +381,6 @@ func main() { loggerLevel := parseLogLevel(logLevel) logger.GetLogger().SetLevel(parseLogLevel(logLevel)) - // parse the mtu string into an int mtuInt, err = strconv.Atoi(mtu) if err != nil { logger.Fatal("Failed to parse MTU: %v", err) @@ -431,18 +394,13 @@ func main() { if tlsPrivateKey != "" { opt = websocket.WithTLSConfig(tlsPrivateKey) } - // Create a new client client, err := websocket.NewClient( - id, // CLI arg takes precedence - secret, // CLI arg takes precedence - endpoint, - opt, + id, secret, endpoint, opt, ) if err != nil { logger.Fatal("Failed to create client: %v", err) } - // Create TUN device and network stack var tun tun.Device var tnet *netstack.Net var dev *device.Device @@ -464,14 +422,12 @@ func main() { pingStopChan := make(chan struct{}) defer close(pingStopChan) - // Register handlers for different message types client.RegisterHandler("newt/wg/connect", func(msg websocket.WSMessage) { logger.Info("Received registration message") if connected { logger.Info("Already connected! But I will send a ping anyway...") - // Even if pingWithRetry returns an error, it will continue trying in the background - _ = pingWithRetry(tnet, wgData.ServerIP) // Ignoring initial error as pings will continue + _ = pingWithRetry(tnet, wgData.ServerIP) return } @@ -495,7 +451,6 @@ func main() { logger.Error("Failed to create TUN device: %v", err) } - // Create WireGuard device dev = device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger( mapToWireGuardLogLevel(loggerLevel), "wireguard: ", @@ -507,7 +462,6 @@ func main() { return } - // Configure WireGuard config := fmt.Sprintf(`private_key=%s public_key=%s allowed_ip=%s/32 @@ -519,7 +473,6 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub logger.Error("Failed to configure WireGuard device: %v", err) } - // Bring up the device err = dev.Up() if err != nil { logger.Error("Failed to bring up WireGuard device: %v", err) @@ -527,29 +480,21 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub logger.Info("WireGuard device created. Lets ping the server now...") - // 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 + // --- CHANGED: Pass healthFile to startPingCheck --- + startPingCheck(tnet, wgData.ServerIP, pingStopChan, healthFile) go monitorConnectionStatus(tnet, wgData.ServerIP, client) } - // Create proxy manager pm = proxy.NewProxyManager(tnet) - connected = true - // add the targets if there are any if len(wgData.Targets.TCP) > 0 { updateTargets(pm, "add", wgData.TunnelIP, "tcp", TargetData{Targets: wgData.Targets.TCP}) } - if len(wgData.Targets.UDP) > 0 { updateTargets(pm, "add", wgData.TunnelIP, "udp", TargetData{Targets: wgData.Targets.UDP}) } @@ -560,298 +505,4 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } }) - client.RegisterHandler("newt/tcp/add", func(msg websocket.WSMessage) { - logger.Info("Received: %+v", msg) - - // if there is no wgData or pm, we can't add targets - if wgData.TunnelIP == "" || pm == nil { - logger.Info("No tunnel IP or proxy manager available") - return - } - - targetData, err := parseTargetData(msg.Data) - if err != nil { - logger.Info("Error parsing target data: %v", err) - return - } - - if len(targetData.Targets) > 0 { - updateTargets(pm, "add", wgData.TunnelIP, "tcp", targetData) - } - }) - - client.RegisterHandler("newt/udp/add", func(msg websocket.WSMessage) { - logger.Info("Received: %+v", msg) - - // if there is no wgData or pm, we can't add targets - if wgData.TunnelIP == "" || pm == nil { - logger.Info("No tunnel IP or proxy manager available") - return - } - - targetData, err := parseTargetData(msg.Data) - if err != nil { - logger.Info("Error parsing target data: %v", err) - return - } - - if len(targetData.Targets) > 0 { - updateTargets(pm, "add", wgData.TunnelIP, "udp", targetData) - } - }) - - client.RegisterHandler("newt/udp/remove", func(msg websocket.WSMessage) { - logger.Info("Received: %+v", msg) - - // if there is no wgData or pm, we can't add targets - if wgData.TunnelIP == "" || pm == nil { - logger.Info("No tunnel IP or proxy manager available") - return - } - - targetData, err := parseTargetData(msg.Data) - if err != nil { - logger.Info("Error parsing target data: %v", err) - return - } - - if len(targetData.Targets) > 0 { - updateTargets(pm, "remove", wgData.TunnelIP, "udp", targetData) - } - }) - - client.RegisterHandler("newt/tcp/remove", func(msg websocket.WSMessage) { - logger.Info("Received: %+v", msg) - - // if there is no wgData or pm, we can't add targets - if wgData.TunnelIP == "" || pm == nil { - logger.Info("No tunnel IP or proxy manager available") - return - } - - targetData, err := parseTargetData(msg.Data) - if err != nil { - logger.Info("Error parsing target data: %v", err) - return - } - - if len(targetData.Targets) > 0 { - updateTargets(pm, "remove", wgData.TunnelIP, "tcp", targetData) - } - }) - - // Register handler for Docker socket check - client.RegisterHandler("newt/socket/check", func(msg websocket.WSMessage) { - logger.Info("Received Docker socket check request") - - if dockerSocket == "" { - logger.Info("Docker socket path is not set") - err := client.SendMessage("newt/socket/status", map[string]interface{}{ - "available": false, - "socketPath": dockerSocket, - }) - if err != nil { - logger.Error("Failed to send Docker socket check response: %v", err) - } - return - } - - // Check if Docker socket is available - isAvailable := docker.CheckSocket(dockerSocket) - - // Send response back to server - err := client.SendMessage("newt/socket/status", map[string]interface{}{ - "available": isAvailable, - "socketPath": dockerSocket, - }) - if err != nil { - logger.Error("Failed to send Docker socket check response: %v", err) - } else { - logger.Info("Docker socket check response sent: available=%t", isAvailable) - } - }) - - // Register handler for Docker container listing - client.RegisterHandler("newt/socket/fetch", func(msg websocket.WSMessage) { - logger.Info("Received Docker container fetch request") - - if dockerSocket == "" { - logger.Info("Docker socket path is not set") - return - } - - // List Docker containers - containers, err := docker.ListContainers(dockerSocket) - if err != nil { - logger.Error("Failed to list Docker containers: %v", err) - return - } - - // Send container list back to server - err = client.SendMessage("newt/socket/containers", map[string]interface{}{ - "containers": containers, - }) - if err != nil { - logger.Error("Failed to send Docker container list: %v", err) - } else { - logger.Info("Docker container list sent, count: %d", len(containers)) - } - }) - - client.OnConnect(func() error { - publicKey := privateKey.PublicKey() - logger.Debug("Public key: %s", publicKey) - - err := client.SendMessage("newt/wg/register", map[string]interface{}{ - "publicKey": publicKey.String(), - }) - if err != nil { - logger.Error("Failed to send registration message: %v", err) - return err - } - - logger.Info("Sent registration message") - return nil - }) - - // Connect to the WebSocket server - if err := client.Connect(); err != nil { - logger.Fatal("Failed to connect to server: %v", err) - } - defer client.Close() - - // Wait for interrupt signal - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - sigReceived := <-sigCh - - // Cleanup - logger.Info("Received %s signal, stopping", sigReceived.String()) - if dev != nil { - dev.Close() - } -} - -func parseTargetData(data interface{}) (TargetData, error) { - var targetData TargetData - jsonData, err := json.Marshal(data) - if err != nil { - logger.Info("Error marshaling data: %v", err) - return targetData, err - } - - if err := json.Unmarshal(jsonData, &targetData); err != nil { - logger.Info("Error unmarshaling target data: %v", err) - return targetData, err - } - return targetData, nil -} - -func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto string, targetData TargetData) error { - for _, t := range targetData.Targets { - // Split the first number off of the target with : separator and use as the port - parts := strings.Split(t, ":") - if len(parts) != 3 { - logger.Info("Invalid target format: %s", t) - continue - } - - // Get the port as an int - port := 0 - _, err := fmt.Sscanf(parts[0], "%d", &port) - if err != nil { - logger.Info("Invalid port: %s", parts[0]) - continue - } - - if action == "add" { - target := parts[1] + ":" + parts[2] - - // Call updown script if provided - processedTarget := target - if updownScript != "" { - newTarget, err := executeUpdownScript(action, proto, target) - if err != nil { - logger.Warn("Updown script error: %v", err) - } else if newTarget != "" { - processedTarget = newTarget - } - } - - // Only remove the specific target if it exists - err := pm.RemoveTarget(proto, tunnelIP, port) - if err != nil { - // Ignore "target not found" errors as this is expected for new targets - if !strings.Contains(err.Error(), "target not found") { - logger.Error("Failed to remove existing target: %v", err) - } - } - - // Add the new target - pm.AddTarget(proto, tunnelIP, port, processedTarget) - - } else if action == "remove" { - logger.Info("Removing target with port %d", port) - - target := parts[1] + ":" + parts[2] - - // Call updown script if provided - if updownScript != "" { - _, err := executeUpdownScript(action, proto, target) - if err != nil { - logger.Warn("Updown script error: %v", err) - } - } - - err := pm.RemoveTarget(proto, tunnelIP, port) - if err != nil { - logger.Error("Failed to remove target: %v", err) - return err - } - } - } - - return nil -} - -func executeUpdownScript(action, proto, target string) (string, error) { - if updownScript == "" { - return target, nil - } - - // Split the updownScript in case it contains spaces (like "/usr/bin/python3 script.py") - parts := strings.Fields(updownScript) - if len(parts) == 0 { - return target, fmt.Errorf("invalid updown script command") - } - - var cmd *exec.Cmd - if len(parts) == 1 { - // If it's a single executable - logger.Info("Executing updown script: %s %s %s %s", updownScript, action, proto, target) - cmd = exec.Command(parts[0], action, proto, target) - } else { - // If it includes interpreter and script - args := append(parts[1:], action, proto, target) - logger.Info("Executing updown script: %s %s %s %s %s", parts[0], strings.Join(parts[1:], " "), action, proto, target) - cmd = exec.Command(parts[0], args...) - } - - output, err := cmd.Output() - if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - return "", fmt.Errorf("updown script execution failed (exit code %d): %s", - exitErr.ExitCode(), string(exitErr.Stderr)) - } - return "", fmt.Errorf("updown script execution failed: %v", err) - } - - // If the script returns a new target, use it - newTarget := strings.TrimSpace(string(output)) - if newTarget != "" { - logger.Info("Updown script returned new target: %s", newTarget) - return newTarget, nil - } - - return target, nil } From 700287163e8a86d01915d202ead49862ab8f023e Mon Sep 17 00:00:00 2001 From: Wouter van Elten Date: Mon, 30 Jun 2025 12:50:33 +0200 Subject: [PATCH 4/6] Update main.go --- main.go | 295 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 295 insertions(+) diff --git a/main.go b/main.go index 116e6e5..ef85099 100644 --- a/main.go +++ b/main.go @@ -505,4 +505,299 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } }) +} + client.RegisterHandler("newt/tcp/add", func(msg websocket.WSMessage) { + logger.Info("Received: %+v", msg) + + // if there is no wgData or pm, we can't add targets + if wgData.TunnelIP == "" || pm == nil { + logger.Info("No tunnel IP or proxy manager available") + return + } + + targetData, err := parseTargetData(msg.Data) + if err != nil { + logger.Info("Error parsing target data: %v", err) + return + } + + if len(targetData.Targets) > 0 { + updateTargets(pm, "add", wgData.TunnelIP, "tcp", targetData) + } + }) + + client.RegisterHandler("newt/udp/add", func(msg websocket.WSMessage) { + logger.Info("Received: %+v", msg) + + // if there is no wgData or pm, we can't add targets + if wgData.TunnelIP == "" || pm == nil { + logger.Info("No tunnel IP or proxy manager available") + return + } + + targetData, err := parseTargetData(msg.Data) + if err != nil { + logger.Info("Error parsing target data: %v", err) + return + } + + if len(targetData.Targets) > 0 { + updateTargets(pm, "add", wgData.TunnelIP, "udp", targetData) + } + }) + + client.RegisterHandler("newt/udp/remove", func(msg websocket.WSMessage) { + logger.Info("Received: %+v", msg) + + // if there is no wgData or pm, we can't add targets + if wgData.TunnelIP == "" || pm == nil { + logger.Info("No tunnel IP or proxy manager available") + return + } + + targetData, err := parseTargetData(msg.Data) + if err != nil { + logger.Info("Error parsing target data: %v", err) + return + } + + if len(targetData.Targets) > 0 { + updateTargets(pm, "remove", wgData.TunnelIP, "udp", targetData) + } + }) + + client.RegisterHandler("newt/tcp/remove", func(msg websocket.WSMessage) { + logger.Info("Received: %+v", msg) + + // if there is no wgData or pm, we can't add targets + if wgData.TunnelIP == "" || pm == nil { + logger.Info("No tunnel IP or proxy manager available") + return + } + + targetData, err := parseTargetData(msg.Data) + if err != nil { + logger.Info("Error parsing target data: %v", err) + return + } + + if len(targetData.Targets) > 0 { + updateTargets(pm, "remove", wgData.TunnelIP, "tcp", targetData) + } + }) + + // Register handler for Docker socket check + client.RegisterHandler("newt/socket/check", func(msg websocket.WSMessage) { + logger.Info("Received Docker socket check request") + + if dockerSocket == "" { + logger.Info("Docker socket path is not set") + err := client.SendMessage("newt/socket/status", map[string]interface{}{ + "available": false, + "socketPath": dockerSocket, + }) + if err != nil { + logger.Error("Failed to send Docker socket check response: %v", err) + } + return + } + + // Check if Docker socket is available + isAvailable := docker.CheckSocket(dockerSocket) + + // Send response back to server + err := client.SendMessage("newt/socket/status", map[string]interface{}{ + "available": isAvailable, + "socketPath": dockerSocket, + }) + if err != nil { + logger.Error("Failed to send Docker socket check response: %v", err) + } else { + logger.Info("Docker socket check response sent: available=%t", isAvailable) + } + }) + + // Register handler for Docker container listing + client.RegisterHandler("newt/socket/fetch", func(msg websocket.WSMessage) { + logger.Info("Received Docker container fetch request") + + if dockerSocket == "" { + logger.Info("Docker socket path is not set") + return + } + + // List Docker containers + containers, err := docker.ListContainers(dockerSocket) + if err != nil { + logger.Error("Failed to list Docker containers: %v", err) + return + } + + // Send container list back to server + err = client.SendMessage("newt/socket/containers", map[string]interface{}{ + "containers": containers, + }) + if err != nil { + logger.Error("Failed to send Docker container list: %v", err) + } else { + logger.Info("Docker container list sent, count: %d", len(containers)) + } + }) + + client.OnConnect(func() error { + publicKey := privateKey.PublicKey() + logger.Debug("Public key: %s", publicKey) + + err := client.SendMessage("newt/wg/register", map[string]interface{}{ + "publicKey": publicKey.String(), + }) + if err != nil { + logger.Error("Failed to send registration message: %v", err) + return err + } + + logger.Info("Sent registration message") + return nil + }) + + // Connect to the WebSocket server + if err := client.Connect(); err != nil { + logger.Fatal("Failed to connect to server: %v", err) + } + defer client.Close() + + // Wait for interrupt signal + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + sigReceived := <-sigCh + + // Cleanup + logger.Info("Received %s signal, stopping", sigReceived.String()) + if dev != nil { + dev.Close() + } +} + +func parseTargetData(data interface{}) (TargetData, error) { + var targetData TargetData + jsonData, err := json.Marshal(data) + if err != nil { + logger.Info("Error marshaling data: %v", err) + return targetData, err + } + + if err := json.Unmarshal(jsonData, &targetData); err != nil { + logger.Info("Error unmarshaling target data: %v", err) + return targetData, err + } + return targetData, nil +} + +func updateTargets(pm *proxy.ProxyManager, action string, tunnelIP string, proto string, targetData TargetData) error { + for _, t := range targetData.Targets { + // Split the first number off of the target with : separator and use as the port + parts := strings.Split(t, ":") + if len(parts) != 3 { + logger.Info("Invalid target format: %s", t) + continue + } + + // Get the port as an int + port := 0 + _, err := fmt.Sscanf(parts[0], "%d", &port) + if err != nil { + logger.Info("Invalid port: %s", parts[0]) + continue + } + + if action == "add" { + target := parts[1] + ":" + parts[2] + + // Call updown script if provided + processedTarget := target + if updownScript != "" { + newTarget, err := executeUpdownScript(action, proto, target) + if err != nil { + logger.Warn("Updown script error: %v", err) + } else if newTarget != "" { + processedTarget = newTarget + } + } + + // Only remove the specific target if it exists + err := pm.RemoveTarget(proto, tunnelIP, port) + if err != nil { + // Ignore "target not found" errors as this is expected for new targets + if !strings.Contains(err.Error(), "target not found") { + logger.Error("Failed to remove existing target: %v", err) + } + } + + // Add the new target + pm.AddTarget(proto, tunnelIP, port, processedTarget) + + } else if action == "remove" { + logger.Info("Removing target with port %d", port) + + target := parts[1] + ":" + parts[2] + + // Call updown script if provided + if updownScript != "" { + _, err := executeUpdownScript(action, proto, target) + if err != nil { + logger.Warn("Updown script error: %v", err) + } + } + + err := pm.RemoveTarget(proto, tunnelIP, port) + if err != nil { + logger.Error("Failed to remove target: %v", err) + return err + } + } + } + + return nil +} + +func executeUpdownScript(action, proto, target string) (string, error) { + if updownScript == "" { + return target, nil + } + + // Split the updownScript in case it contains spaces (like "/usr/bin/python3 script.py") + parts := strings.Fields(updownScript) + if len(parts) == 0 { + return target, fmt.Errorf("invalid updown script command") + } + + var cmd *exec.Cmd + if len(parts) == 1 { + // If it's a single executable + logger.Info("Executing updown script: %s %s %s %s", updownScript, action, proto, target) + cmd = exec.Command(parts[0], action, proto, target) + } else { + // If it includes interpreter and script + args := append(parts[1:], action, proto, target) + logger.Info("Executing updown script: %s %s %s %s %s", parts[0], strings.Join(parts[1:], " "), action, proto, target) + cmd = exec.Command(parts[0], args...) + } + + output, err := cmd.Output() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + return "", fmt.Errorf("updown script execution failed (exit code %d): %s", + exitErr.ExitCode(), string(exitErr.Stderr)) + } + return "", fmt.Errorf("updown script execution failed: %v", err) + } + + // If the script returns a new target, use it + newTarget := strings.TrimSpace(string(output)) + if newTarget != "" { + logger.Info("Updown script returned new target: %s", newTarget) + return newTarget, nil + } + + return target, nil } From 9db3b78373ed3991eeeadbb980cbf773648d239a Mon Sep 17 00:00:00 2001 From: Wouter van Elten Date: Mon, 30 Jun 2025 13:03:06 +0200 Subject: [PATCH 5/6] Update main.go fixed some errors, This file should be ok. --- main.go | 97 ++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 15 deletions(-) diff --git a/main.go b/main.go index ef85099..238d576 100644 --- a/main.go +++ b/main.go @@ -50,11 +50,16 @@ type TargetData struct { } func fixKey(key string) string { + // Remove any whitespace key = strings.TrimSpace(key) + + // Decode from base64 decoded, err := base64.StdEncoding.DecodeString(key) if err != nil { logger.Fatal("Error decoding base64: %v", err) } + + // Convert to hex return hex.EncodeToString(decoded) } @@ -110,7 +115,6 @@ func ping(tnet *netstack.Net, dst string) error { return nil } -// --- CHANGED: added healthFile as parameter --- func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}, healthFile string) { initialInterval := 10 * time.Second maxInterval := 60 * time.Second @@ -127,23 +131,28 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}, err := ping(tnet, serverIP) if err != nil { consecutiveFailures++ - logger.Warn("Periodic ping failed (%d consecutive failures): %v", consecutiveFailures, err) + 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?") - // --- CHANGED: Only remove file if healthFile is set --- + + // Only remove file if healthFile is set if consecutiveFailures >= 3 && healthFile != "" { _ = os.Remove(healthFile) } + // 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) + logger.Info("Increased ping check interval to %v due to consecutive failures", + currentInterval) } } else { - // --- CHANGED: Only write file if healthFile is set --- + // Only write file if healthFile is set if healthFile != "" { err := os.WriteFile(healthFile, []byte("ok"), 0644) if err != nil { @@ -157,7 +166,8 @@ func startPingCheck(tnet *netstack.Net, serverIP string, stopChan chan struct{}, currentInterval = initialInterval } ticker.Reset(currentInterval) - logger.Info("Decreased ping check interval to %v after successful ping", currentInterval) + logger.Info("Decreased ping check interval to %v after successful ping", + currentInterval) } consecutiveFailures = 0 } @@ -169,6 +179,7 @@ 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 @@ -178,18 +189,27 @@ func monitorConnectionStatus(tnet *netstack.Net, serverIP string, client *websoc 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": privateKey.PublicKey().String(), }) + if err != nil { logger.Error("Failed to send registration message after reconnection: %v", err) } else { @@ -204,25 +224,32 @@ func pingWithRetry(tnet *netstack.Net, dst string) error { const ( initialMaxAttempts = 15 initialRetryDelay = 2 * time.Second - maxRetryDelay = 60 * time.Second + maxRetryDelay = 60 * time.Second // Cap the maximum delay ) 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) } + // Start a goroutine that will attempt pings indefinitely with increasing delays go func() { - attempt = 2 + 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 { @@ -230,14 +257,18 @@ func pingWithRetry(tnet *netstack.Net, dst string) error { } 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") } @@ -254,7 +285,7 @@ func parseLogLevel(level string) logger.LogLevel { case "FATAL": return logger.FATAL default: - return logger.INFO + return logger.INFO // default to INFO if invalid level provided } } @@ -262,6 +293,8 @@ func mapToWireGuardLogLevel(level logger.LogLevel) int { switch level { case logger.DEBUG: return device.LogLevelVerbose + // case logger.INFO: + // return device.LogLevel case logger.WARN: return device.LogLevelError case logger.ERROR, logger.FATAL: @@ -272,23 +305,32 @@ func mapToWireGuardLogLevel(level logger.LogLevel) int { } func resolveDomain(domain string) (string, error) { + // Check if there's a port in the domain host, port, err := net.SplitHostPort(domain) if err != nil { + // No port found, use the domain as is host = domain port = "" } + + // Remove any protocol prefix if present if strings.HasPrefix(host, "http://") { host = strings.TrimPrefix(host, "http://") } else if strings.HasPrefix(host, "https://") { host = strings.TrimPrefix(host, "https://") } + + // Lookup IP addresses ips, err := net.LookupIP(host) if err != nil { return "", fmt.Errorf("DNS lookup failed: %v", err) } + if len(ips) == 0 { return "", fmt.Errorf("no IP addresses found for domain %s", host) } + + // Get the first IPv4 address if available var ipAddr string for _, ip := range ips { if ipv4 := ip.To4(); ipv4 != nil { @@ -296,16 +338,20 @@ func resolveDomain(domain string) (string, error) { break } } + + // If no IPv4 found, use the first IP (might be IPv6) if ipAddr == "" { ipAddr = ips[0].String() } + + // Add port back if it existed if port != "" { ipAddr = net.JoinHostPort(ipAddr, port) } + return ipAddr, nil } -// --- ADDED: healthFile variable --- var ( endpoint string id string @@ -323,6 +369,7 @@ var ( ) func main() { + // if PANGOLIN_ENDPOINT, NEWT_ID, and NEWT_SECRET are set as environment variables, they will be used as default values endpoint = os.Getenv("PANGOLIN_ENDPOINT") id = os.Getenv("NEWT_ID") secret = os.Getenv("NEWT_SECRET") @@ -361,12 +408,14 @@ func main() { if dockerSocket == "" { flag.StringVar(&dockerSocket, "docker-socket", "", "Path to Docker socket (typically /var/run/docker.sock)") } - // --- ADDED: CLI flag for healthFile if not set by env --- + // CLI flag for healthFile if not set by env if healthFile == "" { flag.StringVar(&healthFile, "health-file", "", "Path to health file (if unset, health file won’t be written)") } + // do a --version check version := flag.Bool("version", false, "Print the version") + flag.Parse() newtVersion := "Newt version replaceme" @@ -381,6 +430,7 @@ func main() { loggerLevel := parseLogLevel(logLevel) logger.GetLogger().SetLevel(parseLogLevel(logLevel)) + // parse the mtu string into an int mtuInt, err = strconv.Atoi(mtu) if err != nil { logger.Fatal("Failed to parse MTU: %v", err) @@ -394,13 +444,18 @@ func main() { if tlsPrivateKey != "" { opt = websocket.WithTLSConfig(tlsPrivateKey) } + // Create a new client client, err := websocket.NewClient( - id, secret, endpoint, opt, + id, // CLI arg takes precedence + secret, // CLI arg takes precedence + endpoint, + opt, ) if err != nil { logger.Fatal("Failed to create client: %v", err) } + // Create TUN device and network stack var tun tun.Device var tnet *netstack.Net var dev *device.Device @@ -422,12 +477,14 @@ func main() { pingStopChan := make(chan struct{}) defer close(pingStopChan) + // Register handlers for different message types client.RegisterHandler("newt/wg/connect", func(msg websocket.WSMessage) { logger.Info("Received registration message") if connected { logger.Info("Already connected! But I will send a ping anyway...") - _ = pingWithRetry(tnet, wgData.ServerIP) + // 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 } @@ -451,6 +508,7 @@ func main() { logger.Error("Failed to create TUN device: %v", err) } + // Create WireGuard device dev = device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger( mapToWireGuardLogLevel(loggerLevel), "wireguard: ", @@ -462,6 +520,7 @@ func main() { return } + // Configure WireGuard config := fmt.Sprintf(`private_key=%s public_key=%s allowed_ip=%s/32 @@ -473,6 +532,7 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub logger.Error("Failed to configure WireGuard device: %v", err) } + // Bring up the device err = dev.Up() if err != nil { logger.Error("Failed to bring up WireGuard device: %v", err) @@ -480,21 +540,29 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub logger.Info("WireGuard device created. Lets ping the server now...") + // 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") - // --- CHANGED: Pass healthFile to startPingCheck --- startPingCheck(tnet, wgData.ServerIP, pingStopChan, healthFile) + + // Start connection monitoring in a separate goroutine go monitorConnectionStatus(tnet, wgData.ServerIP, client) } + // Create proxy manager pm = proxy.NewProxyManager(tnet) + connected = true + // add the targets if there are any if len(wgData.Targets.TCP) > 0 { updateTargets(pm, "add", wgData.TunnelIP, "tcp", TargetData{Targets: wgData.Targets.TCP}) } + if len(wgData.Targets.UDP) > 0 { updateTargets(pm, "add", wgData.TunnelIP, "udp", TargetData{Targets: wgData.Targets.UDP}) } @@ -505,7 +573,6 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } }) -} client.RegisterHandler("newt/tcp/add", func(msg websocket.WSMessage) { logger.Info("Received: %+v", msg) From 071a51afbc26da00fd310155650195dcb0637ace Mon Sep 17 00:00:00 2001 From: Wouter van Elten Date: Mon, 30 Jun 2025 13:09:11 +0200 Subject: [PATCH 6/6] Update main.go synced with dev --- main.go | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/main.go b/main.go index 238d576..d1b3c3f 100644 --- a/main.go +++ b/main.go @@ -353,19 +353,21 @@ func resolveDomain(domain string) (string, error) { } var ( - endpoint string - id string - secret string - mtu string - mtuInt int - dns string - privateKey wgtypes.Key - err error - logLevel string - updownScript string - tlsPrivateKey string - dockerSocket string - healthFile string // NEW + endpoint string + id string + secret string + mtu string + mtuInt int + dns string + privateKey wgtypes.Key + err error + logLevel string + updownScript string + tlsPrivateKey string + dockerSocket string + dockerEnforceNetworkValidation string + dockerEnforceNetworkValidationBool bool + healthFile string // NEW ) func main() { @@ -379,6 +381,7 @@ func main() { updownScript = os.Getenv("UPDOWN_SCRIPT") tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT") dockerSocket = os.Getenv("DOCKER_SOCKET") + dockerEnforceNetworkValidation = os.Getenv("DOCKER_ENFORCE_NETWORK_VALIDATION") healthFile = os.Getenv("HEALTH_FILE") // NEW if endpoint == "" { @@ -408,6 +411,9 @@ func main() { if dockerSocket == "" { flag.StringVar(&dockerSocket, "docker-socket", "", "Path to Docker socket (typically /var/run/docker.sock)") } + if dockerEnforceNetworkValidation == "" { + flag.StringVar(&dockerEnforceNetworkValidation, "docker-enforce-network-validation", "false", "Enforce validation of container on newt network (true or false)") + } // CLI flag for healthFile if not set by env if healthFile == "" { flag.StringVar(&healthFile, "health-file", "", "Path to health file (if unset, health file won’t be written)")