From 7cb1f7e2c268c16a16955546cbac0b98a726b79c Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 9 Dec 2025 17:10:38 -0500 Subject: [PATCH 1/8] Working on ipv6 stuff --- util/util.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/util/util.go b/util/util.go index 66f718b..58221c4 100644 --- a/util/util.go +++ b/util/util.go @@ -33,6 +33,19 @@ func ResolveDomain(domain string) (string, error) { port = "" } + // Check if host is already an IP address (IPv4 or IPv6) + // For IPv6, the host from SplitHostPort will already have brackets stripped + // but if there was no port, we need to handle bracketed IPv6 addresses + cleanHost := strings.TrimPrefix(strings.TrimSuffix(host, "]"), "[") + if ip := net.ParseIP(cleanHost); ip != nil { + // It's already an IP address, no need to resolve + ipAddr := ip.String() + if port != "" { + return net.JoinHostPort(ipAddr, port), nil + } + return ipAddr, nil + } + // Lookup IP addresses ips, err := net.LookupIP(host) if err != nil { From 0fca3457c3ab5c13d8809c2939b59841f2d7048f Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 10 Dec 2025 14:06:55 -0500 Subject: [PATCH 2/8] Rename logs, optional port --- clients.go | 2 +- clients/clients.go | 16 +++++++++------- main.go | 16 +++++++++++++++- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/clients.go b/clients.go index 3f28f4c..94586a2 100644 --- a/clients.go +++ b/clients.go @@ -37,7 +37,7 @@ func setupClients(client *websocket.Client) { } // Create WireGuard service - wgService, err = wgnetstack.NewWireGuardService(interfaceName, mtuInt, host, id, client, dns, useNativeInterface) + wgService, err = wgnetstack.NewWireGuardService(interfaceName, port, mtuInt, host, id, client, dns, useNativeInterface) if err != nil { logger.Fatal("Failed to create WireGuard service: %v", err) } diff --git a/clients/clients.go b/clients/clients.go index ed35567..9b17d07 100644 --- a/clients/clients.go +++ b/clients/clients.go @@ -104,17 +104,19 @@ type WireGuardService struct { wgTesterServer *wgtester.Server } -func NewWireGuardService(interfaceName string, mtu int, host string, newtId string, wsClient *websocket.Client, dns string, useNativeInterface bool) (*WireGuardService, error) { +func NewWireGuardService(interfaceName string, port uint16, mtu int, host string, newtId string, wsClient *websocket.Client, dns string, useNativeInterface bool) (*WireGuardService, error) { key, err := wgtypes.GeneratePrivateKey() if err != nil { return nil, fmt.Errorf("failed to generate private key: %v", err) } - // Find an available port - port, err := util.FindAvailableUDPPort(49152, 65535) - - if err != nil { - return nil, fmt.Errorf("error finding available port: %v", err) + if port == 0 { + // Find an available port + portRandom, err := util.FindAvailableUDPPort(49152, 65535) + if err != nil { + return nil, fmt.Errorf("error finding available port: %v", err) + } + port = uint16(portRandom) } // Create shared UDP socket for both holepunch and WireGuard @@ -522,7 +524,7 @@ func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error { // Create WireGuard device using the shared bind s.device = device.NewDevice(s.tun, s.sharedBind, device.NewLogger( device.LogLevelSilent, - "wireguard: ", + "client-wireguard: ", )) fileUAPI, err := func() (*os.File, error) { diff --git a/main.go b/main.go index 0879a96..2ca0e35 100644 --- a/main.go +++ b/main.go @@ -116,6 +116,7 @@ var ( err error logLevel string interfaceName string + port uint16 disableClients bool updownScript string dockerSocket string @@ -167,6 +168,7 @@ func main() { logLevel = os.Getenv("LOG_LEVEL") updownScript = os.Getenv("UPDOWN_SCRIPT") interfaceName = os.Getenv("INTERFACE") + portStr := os.Getenv("PORT") // Metrics/observability env mirrors metricsEnabledEnv := os.Getenv("NEWT_METRICS_PROMETHEUS_ENABLED") @@ -235,6 +237,9 @@ func main() { if interfaceName == "" { flag.StringVar(&interfaceName, "interface", "newt", "Name of the WireGuard interface") } + if portStr == "" { + flag.StringVar(&portStr, "port", "", "Port for client WireGuard interface") + } if useNativeInterfaceEnv == "" { flag.BoolVar(&useNativeInterface, "native", false, "Use native WireGuard interface") } @@ -297,6 +302,15 @@ func main() { pingTimeout = 5 * time.Second } + if portStr != "" { + portInt, err := strconv.Atoi(portStr) + if err != nil { + logger.Warn("Failed to parse PORT, choosing a random port") + } else { + port = uint16(portInt) + } + } + if dockerEnforceNetworkValidation == "" { flag.StringVar(&dockerEnforceNetworkValidation, "docker-enforce-network-validation", "false", "Enforce validation of container on newt network (true or false)") } @@ -641,7 +655,7 @@ func main() { // Create WireGuard device dev = device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger( util.MapToWireGuardLogLevel(loggerLevel), - "wireguard: ", + "gerbil-wireguard: ", )) host, _, err := net.SplitHostPort(wgData.Endpoint) From 30da7eaa8b7f1a322b1d4c789fc2c1b54efb63a6 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 10 Dec 2025 15:32:49 -0500 Subject: [PATCH 3/8] Kind of working --- clients/permissions/permissions_windows.go | 14 +- main.go | 38 +- service_unix.go | 59 ++ service_windows.go | 740 +++++++++++++++++++++ 4 files changed, 844 insertions(+), 7 deletions(-) create mode 100644 service_unix.go create mode 100644 service_windows.go diff --git a/clients/permissions/permissions_windows.go b/clients/permissions/permissions_windows.go index 729341e..a34b9e5 100644 --- a/clients/permissions/permissions_windows.go +++ b/clients/permissions/permissions_windows.go @@ -6,14 +6,24 @@ import ( "fmt" "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc" ) // CheckNativeInterfacePermissions checks if the process has sufficient // permissions to create a native TUN interface on Windows. -// This requires Administrator privileges. +// This requires Administrator privileges and must be running as a Windows service. func CheckNativeInterfacePermissions() error { + // Check if running as a Windows service + isService, err := svc.IsWindowsService() + if err != nil { + return fmt.Errorf("failed to check if running as Windows service: %v", err) + } + if !isService { + return fmt.Errorf("native TUN interface requires running as a Windows service") + } + var sid *windows.SID - err := windows.AllocateAndInitializeSid( + err = windows.AllocateAndInitializeSid( &windows.SECURITY_NT_AUTHORITY, 2, windows.SECURITY_BUILTIN_DOMAIN_RID, diff --git a/main.go b/main.go index 2ca0e35..d579357 100644 --- a/main.go +++ b/main.go @@ -155,10 +155,27 @@ var ( ) func main() { + // Check if we're running as a Windows service + if isWindowsService() { + runService("NewtWireguardService", false, os.Args[1:]) + return + } + + // Handle service management commands on Windows (install, remove, start, stop, etc.) + if handleServiceCommand() { + return + } + // Prepare context for graceful shutdown and signal handling ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() + // Run the main newt logic + runNewtMain(ctx) +} + +// runNewtMain contains the main newt logic, extracted for service support +func runNewtMain(ctx context.Context) { // 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") @@ -1462,10 +1479,8 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey( } } - // Wait for interrupt signal - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - <-sigCh + // Wait for context cancellation (from signal or service stop) + <-ctx.Done() // Close clients first (including WGTester) closeClients() @@ -1490,7 +1505,20 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey( client.Close() } logger.Info("Exiting...") - os.Exit(0) +} + +// runNewtMainWithArgs is used by the Windows service to run newt with specific arguments +// It sets os.Args and then calls runNewtMain +func runNewtMainWithArgs(ctx context.Context, args []string) { + // Set os.Args to include the program name plus the provided args + // This allows flag parsing to work correctly + os.Args = append([]string{os.Args[0]}, args...) + + // Setup Windows logging if running as a service + setupWindowsEventLog() + + // Run the main newt logic + runNewtMain(ctx) } // validateTLSConfig validates the TLS configuration diff --git a/service_unix.go b/service_unix.go new file mode 100644 index 0000000..de5b907 --- /dev/null +++ b/service_unix.go @@ -0,0 +1,59 @@ +//go:build !windows + +package main + +import ( + "fmt" +) + +// Service management functions are not available on non-Windows platforms +func installService() error { + return fmt.Errorf("service management is only available on Windows") +} + +func removeService() error { + return fmt.Errorf("service management is only available on Windows") +} + +func startService(args []string) error { + _ = args // unused on Unix platforms + return fmt.Errorf("service management is only available on Windows") +} + +func stopService() error { + return fmt.Errorf("service management is only available on Windows") +} + +func getServiceStatus() (string, error) { + return "", fmt.Errorf("service management is only available on Windows") +} + +func debugService(args []string) error { + _ = args // unused on Unix platforms + return fmt.Errorf("debug service is only available on Windows") +} + +func isWindowsService() bool { + return false +} + +func runService(name string, isDebug bool, args []string) { + // No-op on non-Windows platforms +} + +func setupWindowsEventLog() { + // No-op on non-Windows platforms +} + +func watchLogFile(end bool) error { + return fmt.Errorf("watching log file is only available on Windows") +} + +func showServiceConfig() { + fmt.Println("Service configuration is only available on Windows") +} + +// handleServiceCommand returns false on non-Windows platforms +func handleServiceCommand() bool { + return false +} diff --git a/service_windows.go b/service_windows.go new file mode 100644 index 0000000..6324d08 --- /dev/null +++ b/service_windows.go @@ -0,0 +1,740 @@ +//go:build windows + +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "os" + "os/signal" + "path/filepath" + "strings" + "syscall" + "time" + + "github.com/fosrl/newt/logger" + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/debug" + "golang.org/x/sys/windows/svc/eventlog" + "golang.org/x/sys/windows/svc/mgr" +) + +const ( + serviceName = "NewtWireguardService" + serviceDisplayName = "Newt WireGuard Tunnel Service" + serviceDescription = "Newt WireGuard tunnel service for secure network connectivity" +) + +// Global variable to store service arguments +var serviceArgs []string + +// getServiceArgsPath returns the path where service arguments are stored +func getServiceArgsPath() string { + logDir := filepath.Join(os.Getenv("PROGRAMDATA"), "newt") + return filepath.Join(logDir, "service_args.json") +} + +// saveServiceArgs saves the service arguments to a file +func saveServiceArgs(args []string) error { + logDir := filepath.Join(os.Getenv("PROGRAMDATA"), "newt") + err := os.MkdirAll(logDir, 0755) + if err != nil { + return fmt.Errorf("failed to create config directory: %v", err) + } + + argsPath := getServiceArgsPath() + data, err := json.Marshal(args) + if err != nil { + return fmt.Errorf("failed to marshal service args: %v", err) + } + + err = os.WriteFile(argsPath, data, 0644) + if err != nil { + return fmt.Errorf("failed to write service args: %v", err) + } + + return nil +} + +// loadServiceArgs loads the service arguments from a file +func loadServiceArgs() ([]string, error) { + argsPath := getServiceArgsPath() + data, err := os.ReadFile(argsPath) + if err != nil { + if os.IsNotExist(err) { + return []string{}, nil // Return empty args if file doesn't exist + } + return nil, fmt.Errorf("failed to read service args: %v", err) + } + + var args []string + err = json.Unmarshal(data, &args) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal service args: %v", err) + } + + return args, nil +} + +type newtService struct { + elog debug.Log + ctx context.Context + stop context.CancelFunc + args []string +} + +func (s *newtService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) { + const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown + changes <- svc.Status{State: svc.StartPending} + + s.elog.Info(1, fmt.Sprintf("Service Execute called with args: %v", args)) + + // Load saved service arguments + savedArgs, err := loadServiceArgs() + if err != nil { + s.elog.Error(1, fmt.Sprintf("Failed to load service args: %v", err)) + // Continue with empty args if loading fails + savedArgs = []string{} + } + s.elog.Info(1, fmt.Sprintf("Loaded saved service args: %v", savedArgs)) + + // Combine service start args with saved args, giving priority to service start args + // Note: When the service is started via SCM, args[0] is the service name + // When started via s.Start(args...), the args passed are exactly what we provide + finalArgs := []string{} + + // Check if we have args passed directly to Execute (from s.Start()) + if len(args) > 0 { + // The first arg from SCM is the service name, but when we call s.Start(args...), + // the args we pass become args[1:] in Execute. However, if started by SCM without + // args, args[0] will be the service name. + // We need to check if args[0] looks like the service name or a flag + if len(args) == 1 && args[0] == serviceName { + // Only service name, no actual args + s.elog.Info(1, "Only service name in args, checking saved args") + } else if len(args) > 1 && args[0] == serviceName { + // Service name followed by actual args + finalArgs = append(finalArgs, args[1:]...) + s.elog.Info(1, fmt.Sprintf("Using service start parameters (after service name): %v", finalArgs)) + } else { + // Args don't start with service name, use them all + // This happens when args are passed via s.Start(args...) + finalArgs = append(finalArgs, args...) + s.elog.Info(1, fmt.Sprintf("Using service start parameters (direct): %v", finalArgs)) + } + } + + // If no service start parameters, use saved args + if len(finalArgs) == 0 && len(savedArgs) > 0 { + finalArgs = savedArgs + s.elog.Info(1, fmt.Sprintf("Using saved service args: %v", finalArgs)) + } + + s.elog.Info(1, fmt.Sprintf("Final args to use: %v", finalArgs)) + s.args = finalArgs + + // Start the main newt functionality + newtDone := make(chan struct{}) + go func() { + s.runNewt() + close(newtDone) + }() + + changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} + s.elog.Info(1, "Service status set to Running") + + for { + select { + case c := <-r: + switch c.Cmd { + case svc.Interrogate: + changes <- c.CurrentStatus + case svc.Stop, svc.Shutdown: + s.elog.Info(1, "Service stopping") + changes <- svc.Status{State: svc.StopPending} + if s.stop != nil { + s.stop() + } + // Wait for main logic to finish or timeout + select { + case <-newtDone: + s.elog.Info(1, "Main logic finished gracefully") + case <-time.After(10 * time.Second): + s.elog.Info(1, "Timeout waiting for main logic to finish") + } + return false, 0 + default: + s.elog.Error(1, fmt.Sprintf("Unexpected control request #%d", c)) + } + case <-newtDone: + s.elog.Info(1, "Main newt logic completed, stopping service") + changes <- svc.Status{State: svc.StopPending} + return false, 0 + } + } +} + +func (s *newtService) runNewt() { + // Create a context that can be cancelled when the service stops + s.ctx, s.stop = context.WithCancel(context.Background()) + + // Setup logging for service mode + s.elog.Info(1, "Starting Newt main logic") + + // Run the main newt logic and wait for it to complete + done := make(chan struct{}) + go func() { + defer func() { + if r := recover(); r != nil { + s.elog.Error(1, fmt.Sprintf("Panic in newt main: %v", r)) + } + close(done) + }() + + // Call the main newt function with stored arguments + // Use s.ctx as the signal context since the service manages shutdown + runNewtMainWithArgs(s.ctx, s.args) + }() + + // Wait for either context cancellation or main logic completion + select { + case <-s.ctx.Done(): + s.elog.Info(1, "Newt service context cancelled") + case <-done: + s.elog.Info(1, "Newt main logic completed") + } +} + +func runService(name string, isDebug bool, args []string) { + var err error + var elog debug.Log + + if isDebug { + elog = debug.New(name) + fmt.Printf("Starting %s service in debug mode\n", name) + } else { + elog, err = eventlog.Open(name) + if err != nil { + fmt.Printf("Failed to open event log: %v\n", err) + return + } + } + defer elog.Close() + + elog.Info(1, fmt.Sprintf("Starting %s service", name)) + run := svc.Run + if isDebug { + run = debug.Run + } + + service := &newtService{elog: elog, args: args} + err = run(name, service) + if err != nil { + elog.Error(1, fmt.Sprintf("%s service failed: %v", name, err)) + if isDebug { + fmt.Printf("Service failed: %v\n", err) + } + return + } + elog.Info(1, fmt.Sprintf("%s service stopped", name)) + if isDebug { + fmt.Printf("%s service stopped\n", name) + } +} + +func installService() error { + exepath, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable path: %v", err) + } + + m, err := mgr.Connect() + if err != nil { + return fmt.Errorf("failed to connect to service manager: %v", err) + } + defer m.Disconnect() + + s, err := m.OpenService(serviceName) + if err == nil { + s.Close() + return fmt.Errorf("service %s already exists", serviceName) + } + + config := mgr.Config{ + ServiceType: 0x10, // SERVICE_WIN32_OWN_PROCESS + StartType: mgr.StartManual, + ErrorControl: mgr.ErrorNormal, + DisplayName: serviceDisplayName, + Description: serviceDescription, + BinaryPathName: exepath, + } + + s, err = m.CreateService(serviceName, exepath, config) + if err != nil { + return fmt.Errorf("failed to create service: %v", err) + } + defer s.Close() + + err = eventlog.InstallAsEventCreate(serviceName, eventlog.Error|eventlog.Warning|eventlog.Info) + if err != nil { + s.Delete() + return fmt.Errorf("failed to install event log: %v", err) + } + + return nil +} + +func removeService() error { + m, err := mgr.Connect() + if err != nil { + return fmt.Errorf("failed to connect to service manager: %v", err) + } + defer m.Disconnect() + + s, err := m.OpenService(serviceName) + if err != nil { + return fmt.Errorf("service %s is not installed", serviceName) + } + defer s.Close() + + // Stop the service if it's running + status, err := s.Query() + if err != nil { + return fmt.Errorf("failed to query service status: %v", err) + } + + if status.State != svc.Stopped { + _, err = s.Control(svc.Stop) + if err != nil { + return fmt.Errorf("failed to stop service: %v", err) + } + + // Wait for service to stop + timeout := time.Now().Add(30 * time.Second) + for status.State != svc.Stopped { + if timeout.Before(time.Now()) { + return fmt.Errorf("timeout waiting for service to stop") + } + time.Sleep(300 * time.Millisecond) + status, err = s.Query() + if err != nil { + return fmt.Errorf("failed to query service status: %v", err) + } + } + } + + err = s.Delete() + if err != nil { + return fmt.Errorf("failed to delete service: %v", err) + } + + err = eventlog.Remove(serviceName) + if err != nil { + return fmt.Errorf("failed to remove event log: %v", err) + } + + return nil +} + +func startService(args []string) error { + fmt.Printf("Starting service with args: %v\n", args) + + // Always save the service arguments so they can be loaded on service restart + err := saveServiceArgs(args) + if err != nil { + fmt.Printf("Warning: failed to save service args: %v\n", err) + // Continue anyway, args will still be passed directly + } else { + fmt.Printf("Saved service args to: %s\n", getServiceArgsPath()) + } + + m, err := mgr.Connect() + if err != nil { + return fmt.Errorf("failed to connect to service manager: %v", err) + } + defer m.Disconnect() + + s, err := m.OpenService(serviceName) + if err != nil { + return fmt.Errorf("service %s is not installed", serviceName) + } + defer s.Close() + + // Pass arguments directly to the service start call + // Note: These args will appear in Execute() after the service name + err = s.Start(args...) + if err != nil { + return fmt.Errorf("failed to start service: %v", err) + } + + return nil +} + +func stopService() error { + m, err := mgr.Connect() + if err != nil { + return fmt.Errorf("failed to connect to service manager: %v", err) + } + defer m.Disconnect() + + s, err := m.OpenService(serviceName) + if err != nil { + return fmt.Errorf("service %s is not installed", serviceName) + } + defer s.Close() + + status, err := s.Control(svc.Stop) + if err != nil { + return fmt.Errorf("failed to stop service: %v", err) + } + + timeout := time.Now().Add(30 * time.Second) + for status.State != svc.Stopped { + if timeout.Before(time.Now()) { + return fmt.Errorf("timeout waiting for service to stop") + } + time.Sleep(300 * time.Millisecond) + status, err = s.Query() + if err != nil { + return fmt.Errorf("failed to query service status: %v", err) + } + } + + return nil +} + +func debugService(args []string) error { + // Save the service arguments before starting + if len(args) > 0 { + err := saveServiceArgs(args) + if err != nil { + return fmt.Errorf("failed to save service args: %v", err) + } + } + + // Run the service in debug mode (runs in current process) + runService(serviceName, true, args) + return nil +} + +func watchLogFile(end bool) error { + logDir := filepath.Join(os.Getenv("PROGRAMDATA"), "newt", "logs") + logPath := filepath.Join(logDir, "newt.log") + + // Ensure the log directory exists + err := os.MkdirAll(logDir, 0755) + if err != nil { + return fmt.Errorf("failed to create log directory: %v", err) + } + + // Wait for the log file to be created if it doesn't exist + var file *os.File + for i := 0; i < 30; i++ { // Wait up to 15 seconds + file, err = os.Open(logPath) + if err == nil { + break + } + if i == 0 { + fmt.Printf("Waiting for log file to be created...\n") + } + time.Sleep(500 * time.Millisecond) + } + + if err != nil { + return fmt.Errorf("failed to open log file after waiting: %v", err) + } + defer file.Close() + + // Seek to the end of the file to only show new logs + _, err = file.Seek(0, 2) + if err != nil { + return fmt.Errorf("failed to seek to end of file: %v", err) + } + + // Set up signal handling for graceful exit + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) + + // Create a ticker to check for new content + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + + buffer := make([]byte, 4096) + + for { + select { + case <-sigCh: + fmt.Printf("\n\nStopping log watch...\n") + // stop the service if needed + if end { + fmt.Printf("Stopping service...\n") + stopService() + } + fmt.Printf("Log watch stopped.\n") + return nil + case <-ticker.C: + // Read new content + n, err := file.Read(buffer) + if err != nil && err != io.EOF { + // Try to reopen the file in case it was recreated + file.Close() + file, err = os.Open(logPath) + if err != nil { + continue + } + continue + } + + if n > 0 { + // Print the new content + fmt.Print(string(buffer[:n])) + } + } + } +} + +func getServiceStatus() (string, error) { + m, err := mgr.Connect() + if err != nil { + return "", fmt.Errorf("failed to connect to service manager: %v", err) + } + defer m.Disconnect() + + s, err := m.OpenService(serviceName) + if err != nil { + return "Not Installed", nil + } + defer s.Close() + + status, err := s.Query() + if err != nil { + return "", fmt.Errorf("failed to query service status: %v", err) + } + + switch status.State { + case svc.Stopped: + return "Stopped", nil + case svc.StartPending: + return "Starting", nil + case svc.StopPending: + return "Stopping", nil + case svc.Running: + return "Running", nil + case svc.ContinuePending: + return "Continue Pending", nil + case svc.PausePending: + return "Pause Pending", nil + case svc.Paused: + return "Paused", nil + default: + return "Unknown", nil + } +} + +// showServiceConfig displays current saved service configuration +func showServiceConfig() { + configPath := getServiceArgsPath() + fmt.Printf("Service configuration file: %s\n", configPath) + + args, err := loadServiceArgs() + if err != nil { + fmt.Printf("No saved configuration found or error loading: %v\n", err) + return + } + + if len(args) == 0 { + fmt.Println("No saved service arguments found") + } else { + fmt.Printf("Saved service arguments: %v\n", args) + } +} + +func isWindowsService() bool { + isWindowsService, err := svc.IsWindowsService() + return err == nil && isWindowsService +} + +// rotateLogFile handles daily log rotation +func rotateLogFile(logDir string, logFile string) error { + // Get current log file info + info, err := os.Stat(logFile) + if err != nil { + if os.IsNotExist(err) { + return nil // No current log file to rotate + } + return fmt.Errorf("failed to stat log file: %v", err) + } + + // Check if log file is from today + now := time.Now() + fileTime := info.ModTime() + + // If the log file is from today, no rotation needed + if now.Year() == fileTime.Year() && now.YearDay() == fileTime.YearDay() { + return nil + } + + // Create rotated filename with date + rotatedName := fmt.Sprintf("newt-%s.log", fileTime.Format("2006-01-02")) + rotatedPath := filepath.Join(logDir, rotatedName) + + // Rename current log file to dated filename + err = os.Rename(logFile, rotatedPath) + if err != nil { + return fmt.Errorf("failed to rotate log file: %v", err) + } + + // Clean up old log files (keep last 30 days) + cleanupOldLogFiles(logDir, 30) + + return nil +} + +// cleanupOldLogFiles removes log files older than specified days +func cleanupOldLogFiles(logDir string, daysToKeep int) { + cutoff := time.Now().AddDate(0, 0, -daysToKeep) + + files, err := os.ReadDir(logDir) + if err != nil { + return + } + + for _, file := range files { + if !file.IsDir() && strings.HasPrefix(file.Name(), "newt-") && strings.HasSuffix(file.Name(), ".log") { + filePath := filepath.Join(logDir, file.Name()) + info, err := file.Info() + if err != nil { + continue + } + + if info.ModTime().Before(cutoff) { + os.Remove(filePath) + } + } + } +} + +func setupWindowsEventLog() { + // Create log directory if it doesn't exist + logDir := filepath.Join(os.Getenv("PROGRAMDATA"), "newt", "logs") + err := os.MkdirAll(logDir, 0755) + if err != nil { + fmt.Printf("Failed to create log directory: %v\n", err) + return + } + + logFile := filepath.Join(logDir, "newt.log") + + // Rotate log file if needed + err = rotateLogFile(logDir, logFile) + if err != nil { + fmt.Printf("Failed to rotate log file: %v\n", err) + // Continue anyway to create new log file + } + + file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + fmt.Printf("Failed to open log file: %v\n", err) + return + } + + // Set the custom logger output + logger.GetLogger().SetOutput(file) + + log.Printf("Newt service logging initialized - log file: %s", logFile) +} + +// handleServiceCommand checks for service management commands and returns true if handled +func handleServiceCommand() bool { + if len(os.Args) < 2 { + return false + } + + command := os.Args[1] + + switch command { + case "install": + err := installService() + if err != nil { + fmt.Printf("Failed to install service: %v\n", err) + os.Exit(1) + } + fmt.Println("Service installed successfully") + return true + case "remove", "uninstall": + err := removeService() + if err != nil { + fmt.Printf("Failed to remove service: %v\n", err) + os.Exit(1) + } + fmt.Println("Service removed successfully") + return true + case "start": + // Pass the remaining arguments (after "start") to the service + serviceArgs := os.Args[2:] + err := startService(serviceArgs) + if err != nil { + fmt.Printf("Failed to start service: %v\n", err) + os.Exit(1) + } + fmt.Println("Service started successfully") + return true + case "stop": + err := stopService() + if err != nil { + fmt.Printf("Failed to stop service: %v\n", err) + os.Exit(1) + } + fmt.Println("Service stopped successfully") + return true + case "status": + status, err := getServiceStatus() + if err != nil { + fmt.Printf("Failed to get service status: %v\n", err) + os.Exit(1) + } + fmt.Printf("Service status: %s\n", status) + return true + case "debug": + // Pass the remaining arguments (after "debug") to the service + serviceArgs := os.Args[2:] + err := debugService(serviceArgs) + if err != nil { + fmt.Printf("Failed to debug service: %v\n", err) + os.Exit(1) + } + return true + case "logs": + err := watchLogFile(false) + if err != nil { + fmt.Printf("Failed to watch log file: %v\n", err) + os.Exit(1) + } + return true + case "config": + showServiceConfig() + return true + case "service-help": + fmt.Println("Newt WireGuard Tunnel") + fmt.Println("\nWindows Service Management:") + fmt.Println(" install Install the service") + fmt.Println(" remove Remove the service") + fmt.Println(" start [args] Start the service with optional arguments") + fmt.Println(" stop Stop the service") + fmt.Println(" status Show service status") + fmt.Println(" debug [args] Run service in debug mode with optional arguments") + fmt.Println(" logs Tail the service log file") + fmt.Println(" config Show current service configuration") + fmt.Println(" service-help Show this service help") + fmt.Println("\nExamples:") + fmt.Println(" newt start --endpoint https://example.com --id myid --secret mysecret") + fmt.Println(" newt debug --endpoint https://example.com --id myid --secret mysecret") + fmt.Println("\nFor normal console mode, run with standard flags (e.g., newt --endpoint ...)") + return true + } + + return false +} From 9f1f1328f6fd3fca3175c4bed4f8a0292f12496f Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 10 Dec 2025 16:24:22 -0500 Subject: [PATCH 4/8] Update readme --- .gitignore | 1 - Makefile | 18 +-- README.md | 410 +---------------------------------------------------- 3 files changed, 6 insertions(+), 423 deletions(-) diff --git a/.gitignore b/.gitignore index 1a56bfa..ad2355b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -newt .DS_Store bin/ nohup.out diff --git a/Makefile b/Makefile index 54078c4..4dfc1cb 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ -all: build push +all: local docker-build-release: @if [ -z "$(tag)" ]; then \ @@ -9,17 +9,8 @@ docker-build-release: docker buildx build --platform linux/arm/v7,linux/arm64,linux/amd64 -t fosrl/newt:latest -f Dockerfile --push . docker buildx build --platform linux/arm/v7,linux/arm64,linux/amd64 -t fosrl/newt:$(tag) -f Dockerfile --push . -build: - docker build -t fosrl/newt:latest . - -push: - docker push fosrl/newt:latest - -test: - docker run fosrl/newt:latest - local: - CGO_ENABLED=0 go build -o newt + CGO_ENABLED=0 go build -o ./bin/newt go-build-release: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/newt_linux_arm64 @@ -31,7 +22,4 @@ go-build-release: CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/newt_darwin_amd64 CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/newt_windows_amd64.exe CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -o bin/newt_freebsd_amd64 - CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -o bin/newt_freebsd_arm64 - -clean: - rm newt + CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -o bin/newt_freebsd_arm64 \ No newline at end of file diff --git a/README.md b/README.md index 2d06abf..3ac0be7 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,7 @@ Newt is a fully user space [WireGuard](https://www.wireguard.com/) tunnel client Newt is used with Pangolin and Gerbil as part of the larger system. See documentation below: -- [Full Documentation](https://docs.pangolin.net) - -## Preview - -Preview - -_Sample output of a Newt connected to Pangolin and hosting various resource target proxies._ +- [Full Documentation](https://docs.pangolin.net/manage/sites/understanding-sites) ## Key Functions @@ -31,412 +25,14 @@ When Newt receives WireGuard control messages, it will use the information encod When Newt receives WireGuard control messages, it will use the information encoded to create a local low level TCP and UDP proxies attached to the virtual tunnel in order to relay traffic to programmed targets. -## CLI Args - -### Core Configuration - -- `id`: Newt ID generated by Pangolin to identify the client. -- `secret`: A unique secret (not shared and kept private) used to authenticate the client ID with the websocket in order to receive commands. -- `endpoint`: The endpoint where both Gerbil and Pangolin reside in order to connect to the websocket. -- `blueprint-file` (optional): Path to blueprint file to define Pangolin resources and configurations. -- `no-cloud` (optional): Don't fail over to the cloud when using managed nodes in Pangolin Cloud. Default: false -- `log-level` (optional): The log level to use (DEBUG, INFO, WARN, ERROR, FATAL). Default: INFO - -### Docker Integration - -- `docker-socket` (optional): Set the Docker socket to use the container discovery integration -- `docker-enforce-network-validation` (optional): Validate the container target is on the same network as the newt process. Default: false - -### Client Connections - -- `disable-clients` (optional): Disable clients on the WireGuard interface. Default: false (clients enabled) -- `native` (optional): Use native WireGuard interface (requires WireGuard kernel module and Linux, must run as root). Default: false (uses userspace netstack) -- `interface` (optional): Name of the WireGuard interface. Default: newt - -### Metrics & Observability - -- `metrics` (optional): Enable Prometheus /metrics exporter. Default: true -- `otlp` (optional): Enable OTLP exporters (metrics/traces) to OTEL_EXPORTER_OTLP_ENDPOINT. Default: false -- `metrics-admin-addr` (optional): Admin/metrics bind address. Default: 127.0.0.1:2112 -- `metrics-async-bytes` (optional): Enable async bytes counting (background flush; lower hot path overhead). Default: false -- `region` (optional): Optional region resource attribute for telemetry and metrics. - -### Network Configuration - -- `mtu` (optional): MTU for the internal WG interface. Default: 1280 -- `dns` (optional): DNS server to use to resolve the endpoint. Default: 9.9.9.9 -- `ping-interval` (optional): Interval for pinging the server. Default: 3s -- `ping-timeout` (optional): Timeout for each ping. Default: 5s - -### Security & TLS - -- `enforce-hc-cert` (optional): Enforce certificate validation for health checks. Default: false (accepts any cert) -- `tls-client-cert-file` (optional): Path to client certificate file (PEM/DER format) for mTLS. See [mTLS](#mtls) -- `tls-client-key` (optional): Path to client private key file (PEM/DER format) for mTLS -- `tls-client-ca` (optional): Path to CA certificate file for validating remote certificates (can be specified multiple times) -- `tls-client-cert` (optional): Path to client certificate (PKCS12 format) - DEPRECATED: use `--tls-client-cert-file` and `--tls-client-key` instead -- `prefer-endpoint` (optional): Prefer this endpoint for the connection (if set, will override the endpoint from the server) - -### Monitoring & Health - -- `health-file` (optional): Check if connection to WG server (pangolin) is ok. creates a file if ok, removes it if not ok. Can be used with docker healtcheck to restart newt -- `updown` (optional): A script to be called when targets are added or removed. - -## Environment Variables - -All CLI arguments can be set using environment variables as an alternative to command line flags. Environment variables are particularly useful when running Newt in containerized environments. - -### Core Configuration - -- `PANGOLIN_ENDPOINT`: Endpoint of your pangolin server (equivalent to `--endpoint`) -- `NEWT_ID`: Newt ID generated by Pangolin (equivalent to `--id`) -- `NEWT_SECRET`: Newt secret for authentication (equivalent to `--secret`) -- `CONFIG_FILE`: Load the config json from this file instead of in the home folder. -- `BLUEPRINT_FILE`: Path to blueprint file to define Pangolin resources and configurations. (equivalent to `--blueprint-file`) -- `NO_CLOUD`: Don't fail over to the cloud when using managed nodes in Pangolin Cloud. Default: false (equivalent to `--no-cloud`) -- `LOG_LEVEL`: Log level (DEBUG, INFO, WARN, ERROR, FATAL). Default: INFO (equivalent to `--log-level`) - -### Docker Integration - -- `DOCKER_SOCKET`: Path to Docker socket for container discovery (equivalent to `--docker-socket`) -- `DOCKER_ENFORCE_NETWORK_VALIDATION`: Validate container targets are on same network. Default: false (equivalent to `--docker-enforce-network-validation`) - -### Client Connections - -- `DISABLE_CLIENTS`: Disable clients on the WireGuard interface. Default: false (equivalent to `--disable-clients`) -- `USE_NATIVE_INTERFACE`: Use native WireGuard interface (Linux only). Default: false (equivalent to `--native`) -- `INTERFACE`: Name of the WireGuard interface. Default: newt (equivalent to `--interface`) - -### Monitoring & Health - -- `HEALTH_FILE`: Path to health file for connection monitoring (equivalent to `--health-file`) -- `UPDOWN_SCRIPT`: Path to updown script for target add/remove events (equivalent to `--updown`) - -### Metrics & Observability - -- `NEWT_METRICS_PROMETHEUS_ENABLED`: Enable Prometheus /metrics exporter. Default: true (equivalent to `--metrics`) -- `NEWT_METRICS_OTLP_ENABLED`: Enable OTLP exporters (metrics/traces) to OTEL_EXPORTER_OTLP_ENDPOINT. Default: false (equivalent to `--otlp`) -- `NEWT_ADMIN_ADDR`: Admin/metrics bind address. Default: 127.0.0.1:2112 (equivalent to `--metrics-admin-addr`) -- `NEWT_METRICS_ASYNC_BYTES`: Enable async bytes counting (background flush; lower hot path overhead). Default: false (equivalent to `--metrics-async-bytes`) -- `NEWT_REGION`: Optional region resource attribute for telemetry and metrics (equivalent to `--region`) - -### Network Configuration - -- `MTU`: MTU for the internal WG interface. Default: 1280 (equivalent to `--mtu`) -- `DNS`: DNS server to use to resolve the endpoint. Default: 9.9.9.9 (equivalent to `--dns`) -- `PING_INTERVAL`: Interval for pinging the server. Default: 3s (equivalent to `--ping-interval`) -- `PING_TIMEOUT`: Timeout for each ping. Default: 5s (equivalent to `--ping-timeout`) - -### Security & TLS - -- `ENFORCE_HC_CERT`: Enforce certificate validation for health checks. Default: false (equivalent to `--enforce-hc-cert`) -- `TLS_CLIENT_CERT`: Path to client certificate file (PEM/DER format) for mTLS (equivalent to `--tls-client-cert-file`) -- `TLS_CLIENT_KEY`: Path to client private key file (PEM/DER format) for mTLS (equivalent to `--tls-client-key`) -- `TLS_CLIENT_CAS`: Comma-separated list of CA certificate file paths for validating remote certificates (equivalent to multiple `--tls-client-ca` flags) -- `TLS_CLIENT_CERT_PKCS12`: Path to client certificate (PKCS12 format) - DEPRECATED: use `TLS_CLIENT_CERT` and `TLS_CLIENT_KEY` instead - -## Loading secrets from files - -You can use `CONFIG_FILE` to define a location of a config file to store the credentials between runs. - -``` -$ cat ~/.config/newt-client/config.json -{ - "id": "spmzu8rbpzj1qq6", - "secret": "f6v61mjutwme2kkydbw3fjo227zl60a2tsf5psw9r25hgae3", - "endpoint": "https://app.pangolin.net", - "tlsClientCert": "" -} -``` - -This file is also written to when newt first starts up. So you do not need to run every time with --id and secret if you have run it once! - -Default locations: - -- **macOS**: `~/Library/Application Support/newt-client/config.json` -- **Windows**: `%PROGRAMDATA%\newt\newt-client\config.json` -- **Linux/Others**: `~/.config/newt-client/config.json` - -## Examples - -**Note**: When both environment variables and CLI arguments are provided, CLI arguments take precedence. - -- Example: - -```bash -newt \ ---id 31frd0uzbjvp721 \ ---secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6 \ ---endpoint https://example.com -``` - -You can also run it with Docker compose. For example, a service in your `docker-compose.yml` might look like this using environment vars (recommended): - -```yaml -services: - newt: - image: fosrl/newt - container_name: newt - restart: unless-stopped - environment: - - PANGOLIN_ENDPOINT=https://example.com - - NEWT_ID=2ix2t8xk22ubpfy - - NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2 - - HEALTH_FILE=/tmp/healthy -``` - -You can also pass the CLI args to the container: - -```yaml -services: - newt: - image: fosrl/newt - container_name: newt - restart: unless-stopped - command: - - --id 31frd0uzbjvp721 - - --secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6 - - --endpoint https://example.com - - --health-file /tmp/healthy -``` - -## Client Connections - -By default, Newt can accept incoming client connections from other devices, enabling peer-to-peer connectivity through the Newt instance. This behavior can be disabled with the `--disable-clients` flag (or `DISABLE_CLIENTS=true` environment variable). - -### How It Works - -In client acceptance mode, Newt: - -- **Creates a WireGuard service** that can accept incoming connections from other WireGuard clients -- **Starts a connection testing server** (WGTester) that responds to connectivity checks from remote clients -- **Manages peer configurations** dynamically based on Pangolin's instructions -- **Enables bidirectional communication** between the Newt instance and connected clients - -### Use Cases - -- **Site-to-site connectivity**: Connect multiple locations through a central Newt instance -- **Client access to private networks**: Allow remote clients to access resources behind the Newt instance -- **Development environments**: Provide developers secure access to internal services - -### Client Tunneling Modes - -Newt supports two WireGuard tunneling modes: - -#### Userspace Mode (Default) - -By default, Newt uses a fully userspace WireGuard implementation using [netstack](https://github.com/WireGuard/wireguard-go/blob/master/tun/netstack/examples/http_server.go). This mode: - -- **Does not require root privileges** -- **Works on all supported platforms** (Linux, Windows, macOS) -- **Does not require WireGuard kernel module** to be installed -- **Runs entirely in userspace** - no system network interface is created -- **Is containerization-friendly** - works seamlessly in Docker containers - -This is the recommended mode for most deployments, especially containerized environments. - -In this mode, TCP and UDP is proxied out of newt from the remote client using TCP/UDP resources in Pangolin. - -#### Native Mode (Linux only) - -When using the `--native` flag or setting `USE_NATIVE_INTERFACE=true`, Newt uses the native WireGuard kernel module. This mode: - -- **Requires root privileges** to create and manage network interfaces -- **Only works on Linux** with the WireGuard kernel module installed -- **Creates a real network interface** (e.g., `newt0`) on the system -- **May offer better performance** for high-throughput scenarios -- **Requires proper network permissions** and may conflict with existing network configurations - -In this mode it functions like a traditional VPN interface - all data arrives on the interface and you must get it to the destination (or access things locally). - -#### Native Mode Requirements - -To use native mode: - -1. Run on a Linux system -2. Install the WireGuard kernel module -3. Run Newt as root (`sudo`) -4. Ensure the system allows creation of network interfaces - -Docker Compose example (with clients enabled by default): - -```yaml -services: - newt: - image: fosrl/newt - container_name: newt - restart: unless-stopped - environment: - - PANGOLIN_ENDPOINT=https://example.com - - NEWT_ID=2ix2t8xk22ubpfy - - NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2 -``` - -### Technical Details - -When client acceptance is enabled: - -- **WGTester Server**: Runs on `port + 1` (e.g., if WireGuard uses port 51820, WGTester uses 51821) -- **Connection Testing**: Responds to UDP packets with magic header `0xDEADBEEF` for connectivity verification -- **Dynamic Configuration**: Peer configurations are managed remotely through Pangolin -- **Proxy Integration**: Can work with both userspace (netstack) and native WireGuard modes - -**Note**: Client acceptance mode requires coordination with Pangolin for peer management and configuration distribution. - -### Docker Socket Integration - -Newt can integrate with the Docker socket to provide remote inspection of Docker containers. This allows Pangolin to query and retrieve detailed information about containers running on the Newt client, including metadata, network configuration, port mappings, and more. - -**Configuration:** - -You can specify the Docker socket path using the `--docker-socket` CLI argument or by setting the `DOCKER_SOCKET` environment variable. If the Docker socket is not available or accessible, Newt will gracefully disable Docker integration and continue normal operation. - -Supported values include: - -- Local UNIX socket (default): - >You must mount the socket file into the container using a volume, so Newt can access it. - - `unix:///var/run/docker.sock` - -- TCP socket (e.g., via Docker Socket Proxy): - - `tcp://localhost:2375` - -- HTTP/HTTPS endpoints (e.g., remote Docker APIs): - - `http://your-host:2375` - -- SSH connections (experimental, requires SSH setup): - - `ssh://user@host` - - -```yaml -services: - newt: - image: fosrl/newt - container_name: newt - restart: unless-stopped - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - environment: - - PANGOLIN_ENDPOINT=https://example.com - - NEWT_ID=2ix2t8xk22ubpfy - - NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2 - - DOCKER_SOCKET=unix:///var/run/docker.sock -``` ->If you previously used just a path like `/var/run/docker.sock`, it still works — Newt assumes it is a UNIX socket by default. - -#### Hostnames vs IPs - -When the Docker Socket Integration is used, depending on the network which Newt is run with, either the hostname (generally considered the container name) or the IP address of the container will be sent to Pangolin. Here are some of the scenarios where IPs or hostname of the container will be utilised: - -- **Running in Network Mode 'host'**: IP addresses will be used -- **Running in Network Mode 'bridge'**: IP addresses will be used -- **Running in docker-compose without a network specification**: Docker compose creates a network for the compose by default, hostnames will be used -- **Running on docker-compose with defined network**: Hostnames will be used - -### Docker Enforce Network Validation - -When run as a Docker container, Newt can validate that the target being provided is on the same network as the Newt container and only return containers directly accessible by Newt. Validation will be carried out against either the hostname/IP Address and the Port number to ensure the running container is exposing the ports to Newt. - -It is important to note that if the Newt container is run with a network mode of `host` that this feature will not work. Running in `host` mode causes the container to share its resources with the host machine, therefore making it so the specific host container information for Newt cannot be retrieved to be able to carry out network validation. - -**Configuration:** - -Validation is `false` by default. It can be enabled via setting the `--docker-enforce-network-validation` CLI argument or by setting the `DOCKER_ENFORCE_NETWORK_VALIDATION` environment variable. - -If validation is enforced and the Docker socket is available, Newt will **not** add the target as it cannot be verified. A warning will be presented in the Newt logs. - -### Updown - -You can pass in a updown script for Newt to call when it is adding or removing a target: - -`--updown "python3 test.py"` - -It will get called with args when a target is added: -`python3 test.py add tcp localhost:8556` -`python3 test.py remove tcp localhost:8556` - -Returning a string from the script in the format of a target (`ip:dst` so `10.0.0.1:8080`) it will override the target and use this value instead to proxy. - -You can look at updown.py as a reference script to get started! - -### mTLS - -Newt supports mutual TLS (mTLS) authentication if the server is configured to request a client certificate. You can use either a PKCS12 (.p12/.pfx) file or split PEM files for the client cert, private key, and CA. - -#### Option 1: PKCS12 (Legacy) - -> This is the original method and still supported. - -* File must contain: - - * Client private key - * Public certificate - * CA certificate -* Encrypted `.p12` files are **not supported** - -Example: - -```bash -newt \ ---id 31frd0uzbjvp721 \ ---secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6 \ ---endpoint https://example.com \ ---tls-client-cert ./client.p12 -``` - -#### Option 2: Split PEM Files (Preferred) - -You can now provide separate files for: - -* `--tls-client-cert-file`: client certificate (`.crt` or `.pem`) -* `--tls-client-key`: client private key (`.key` or `.pem`) -* `--tls-client-ca`: CA cert to verify the server (can be specified multiple times) - -Example: - -```bash -newt \ ---id 31frd0uzbjvp721 \ ---secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6 \ ---endpoint https://example.com \ ---tls-client-cert-file ./client.crt \ ---tls-client-key ./client.key \ ---tls-client-ca ./ca.crt -``` - - -```yaml -services: - newt: - image: fosrl/newt - container_name: newt - restart: unless-stopped - environment: - - PANGOLIN_ENDPOINT=https://example.com - - NEWT_ID=2ix2t8xk22ubpfy - - NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2 - - TLS_CLIENT_CERT=./client.p12 -``` - ## Build -### Container - -Ensure Docker is installed. - -```bash -make -``` - ### Binary -Make sure to have Go 1.23.1 installed. +Make sure to have Go 1.25 installed. ```bash -make local +make ``` ### Nix Flake From 67d52173794eca88e20fe54d07ac2bad9ac080f8 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 11 Dec 2025 11:57:06 -0500 Subject: [PATCH 5/8] Add iss file --- newt.iss | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 newt.iss diff --git a/newt.iss b/newt.iss new file mode 100644 index 0000000..6174cfb --- /dev/null +++ b/newt.iss @@ -0,0 +1,152 @@ +; Script generated by the Inno Setup Script Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! + +#define MyAppName "newt" +#define MyAppVersion "1.0.0" +#define MyAppPublisher "Fossorial Inc." +#define MyAppURL "https://pangolin.net" +#define MyAppExeName "newt.exe" + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{44A24E4C-B616-476F-ADE7-8D56B930959E} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +;AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={autopf}\{#MyAppName} +UninstallDisplayIcon={app}\{#MyAppExeName} +; "ArchitecturesAllowed=x64compatible" specifies that Setup cannot run +; on anything but x64 and Windows 11 on Arm. +ArchitecturesAllowed=x64compatible +; "ArchitecturesInstallIn64BitMode=x64compatible" requests that the +; install be done in "64-bit mode" on x64 or Windows 11 on Arm, +; meaning it should use the native 64-bit Program Files directory and +; the 64-bit view of the registry. +ArchitecturesInstallIn64BitMode=x64compatible +DefaultGroupName={#MyAppName} +DisableProgramGroupPage=yes +; Uncomment the following line to run in non administrative install mode (install for current user only). +;PrivilegesRequired=lowest +OutputBaseFilename=mysetup +SolidCompression=yes +WizardStyle=modern +; Add this to ensure PATH changes are applied and the system is prompted for a restart if needed +RestartIfNeededByRun=no +ChangesEnvironment=true + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Files] +; The 'DestName' flag ensures that 'newt_windows_amd64.exe' is installed as 'newt.exe' +Source: "C:\Users\Administrator\Downloads\newt_windows_amd64.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}"; Flags: ignoreversion +Source: "C:\Users\Administrator\Downloads\wintun.dll"; DestDir: "{app}"; Flags: ignoreversion +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" + +[Registry] +; Add the application's installation directory to the system PATH environment variable. +; HKLM (HKEY_LOCAL_MACHINE) is used for system-wide changes. +; The 'Path' variable is located under 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'. +; ValueType: expandsz allows for environment variables (like %ProgramFiles%) in the path. +; ValueData: "{olddata};{app}" appends the current application directory to the existing PATH. +; Note: Removal during uninstallation is handled by CurUninstallStepChanged procedure in [Code] section. +; Check: NeedsAddPath ensures this is applied only if the path is not already present. +[Registry] +; Add the application's installation directory to the system PATH. +Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; \ + ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}"; \ + Check: NeedsAddPath(ExpandConstant('{app}')) + +[Code] +function NeedsAddPath(Path: string): boolean; +var + OrigPath: string; +begin + if not RegQueryStringValue(HKEY_LOCAL_MACHINE, + 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', + 'Path', OrigPath) + then begin + // Path variable doesn't exist at all, so we definitely need to add it. + Result := True; + exit; + end; + + // Perform a case-insensitive check to see if the path is already present. + // We add semicolons to prevent partial matches (e.g., matching C:\App in C:\App2). + if Pos(';' + UpperCase(Path) + ';', ';' + UpperCase(OrigPath) + ';') > 0 then + Result := False + else + Result := True; +end; + +procedure RemovePathEntry(PathToRemove: string); +var + OrigPath: string; + NewPath: string; + PathList: TStringList; + I: Integer; +begin + if not RegQueryStringValue(HKEY_LOCAL_MACHINE, + 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', + 'Path', OrigPath) + then begin + // Path variable doesn't exist, nothing to remove + exit; + end; + + // Create a string list to parse the PATH entries + PathList := TStringList.Create; + try + // Split the PATH by semicolons + PathList.Delimiter := ';'; + PathList.StrictDelimiter := True; + PathList.DelimitedText := OrigPath; + + // Find and remove the matching entry (case-insensitive) + for I := PathList.Count - 1 downto 0 do + begin + if CompareText(Trim(PathList[I]), Trim(PathToRemove)) = 0 then + begin + Log('Found and removing PATH entry: ' + PathList[I]); + PathList.Delete(I); + end; + end; + + // Reconstruct the PATH + NewPath := PathList.DelimitedText; + + // Write the new PATH back to the registry + if RegWriteExpandStringValue(HKEY_LOCAL_MACHINE, + 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', + 'Path', NewPath) + then + Log('Successfully removed path entry: ' + PathToRemove) + else + Log('Failed to write modified PATH to registry'); + finally + PathList.Free; + end; +end; + +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +var + AppPath: string; +begin + if CurUninstallStep = usUninstall then + begin + // Get the application installation path + AppPath := ExpandConstant('{app}'); + Log('Removing PATH entry for: ' + AppPath); + + // Remove only our path entry from the system PATH + RemovePathEntry(AppPath); + end; +end; From 73a14f5fa1fddb0adaf3892ce55f92dd787c425a Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 11 Dec 2025 12:21:54 -0500 Subject: [PATCH 6/8] Adjust debug function --- service_windows.go | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/service_windows.go b/service_windows.go index 6324d08..7102cc2 100644 --- a/service_windows.go +++ b/service_windows.go @@ -415,9 +415,14 @@ func debugService(args []string) error { } } - // Run the service in debug mode (runs in current process) - runService(serviceName, true, args) - return nil + // Start the service with the provided arguments + err := startService(args) + if err != nil { + return fmt.Errorf("failed to start service: %v", err) + } + + // Watch the log file + return watchLogFile(true) } func watchLogFile(end bool) error { @@ -699,9 +704,24 @@ func handleServiceCommand() bool { fmt.Printf("Service status: %s\n", status) return true case "debug": + // get the status and if it is Not Installed then install it first + status, err := getServiceStatus() + if err != nil { + fmt.Printf("Failed to get service status: %v\n", err) + os.Exit(1) + } + if status == "Not Installed" { + err := installService() + if err != nil { + fmt.Printf("Failed to install service: %v\n", err) + os.Exit(1) + } + fmt.Println("Service installed successfully, now running in debug mode") + } + // Pass the remaining arguments (after "debug") to the service serviceArgs := os.Args[2:] - err := debugService(serviceArgs) + err = debugService(serviceArgs) if err != nil { fmt.Printf("Failed to debug service: %v\n", err) os.Exit(1) From 2fb4bf09eac7fab1f3d83cffa3ec8191189f65e1 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 11 Dec 2025 12:29:49 -0500 Subject: [PATCH 7/8] Update iss --- newt.iss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/newt.iss b/newt.iss index 6174cfb..8cf0ae7 100644 --- a/newt.iss +++ b/newt.iss @@ -10,7 +10,7 @@ [Setup] ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) -AppId={{44A24E4C-B616-476F-ADE7-8D56B930959E} +AppId={{25A1E3C4-F273-4334-8DF3-47408E83012D} AppName={#MyAppName} AppVersion={#MyAppVersion} ;AppVerName={#MyAppName} {#MyAppVersion} @@ -44,8 +44,8 @@ Name: "english"; MessagesFile: "compiler:Default.isl" [Files] ; The 'DestName' flag ensures that 'newt_windows_amd64.exe' is installed as 'newt.exe' -Source: "C:\Users\Administrator\Downloads\newt_windows_amd64.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}"; Flags: ignoreversion -Source: "C:\Users\Administrator\Downloads\wintun.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "Z:\newt_windows_amd64.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}"; Flags: ignoreversion +Source: "Z:\wintun.dll"; DestDir: "{app}"; Flags: ignoreversion ; NOTE: Don't use "Flags: ignoreversion" on any shared system files [Icons] From cdfcf49d8929d4ba324758cf6caf76dbf9bae95b Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 11 Dec 2025 14:20:52 -0500 Subject: [PATCH 8/8] Fix host header not working in health checks --- healthcheck/healthcheck.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/healthcheck/healthcheck.go b/healthcheck/healthcheck.go index 46bddea..23ca4bd 100644 --- a/healthcheck/healthcheck.go +++ b/healthcheck/healthcheck.go @@ -398,7 +398,12 @@ func (m *Monitor) performHealthCheck(target *Target) { // Add headers for key, value := range target.Config.Headers { - req.Header.Set(key, value) + // Handle Host header specially - it must be set on req.Host, not in headers + if strings.EqualFold(key, "Host") { + req.Host = value + } else { + req.Header.Set(key, value) + } } // Perform request