From 4c58cd6eff9f610863b891982fbf388d946414a8 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 23 Jul 2025 20:35:00 -0700 Subject: [PATCH] Working windows service Former-commit-id: a85f83cc2088b57234f9c241b079a35563b7066d --- README.md | 63 +++++++++ main.go | 112 ++++++++++++++- olm-service.bat | 52 +++++++ olm-service.ps1 | 85 ++++++++++++ olm.exe.REMOVED.git-id | 1 + service_unix.go | 40 ++++++ service_windows.go | 309 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 655 insertions(+), 7 deletions(-) create mode 100644 olm-service.bat create mode 100644 olm-service.ps1 create mode 100644 olm.exe.REMOVED.git-id create mode 100644 service_unix.go create mode 100644 service_windows.go diff --git a/README.md b/README.md index ba7a29a..6809b69 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,69 @@ WantedBy=multi-user.target Make sure to `mv ./olm /usr/local/bin/olm`! +## Windows Service + +On Windows, Olm can be installed and run as a Windows service. This allows it to start automatically at boot and run in the background. + +### Service Management Commands + +```cmd +# Install the service +olm.exe install + +# Start the service +olm.exe start + +# Stop the service +olm.exe stop + +# Check service status +olm.exe status + +# Remove the service +olm.exe remove + +# Run in debug mode (console output) +olm.exe debug + +# Show help +olm.exe help +``` + +**Helper Scripts**: For easier service management, you can use the provided helper scripts: +- `olm-service.bat` - Batch script (requires Administrator privileges) +- `olm-service.ps1` - PowerShell script with better error handling + +Example using the batch script: +```cmd +# Run as Administrator +olm-service.bat install +olm-service.bat start +olm-service.bat status +``` + +### Service Configuration + +When running as a service, Olm will read configuration from environment variables or you can modify the service to include command-line arguments: + +1. Install the service: `olm.exe install` +2. Configure the service with your credentials using Windows Service Manager or by setting system environment variables: + - `PANGOLIN_ENDPOINT=https://example.com` + - `OLM_ID=your_olm_id` + - `OLM_SECRET=your_secret` +3. Start the service: `olm.exe start` + +### Service Logs + +When running as a service, logs are written to: +- Windows Event Log (Application log, source: "OlmWireguardService") +- Log files in: `%PROGRAMDATA%\Olm\logs\olm.log` + +You can view the Windows Event Log using Event Viewer or PowerShell: +```powershell +Get-EventLog -LogName Application -Source "OlmWireguardService" -Newest 10 +``` + ## Build ### Container diff --git a/main.go b/main.go index 6eeb8de..99644d5 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,11 @@ package main import ( + "context" "encoding/json" "flag" "fmt" + "net" "os" "os/signal" "runtime" @@ -24,6 +26,92 @@ import ( ) func main() { + // Check if we're running as a Windows service + if isWindowsService() { + runService("OlmWireguardService", false) + fmt.Println("Service started successfully") + return + } + + // Handle service management commands on Windows + // print the args + for i, arg := range os.Args { + fmt.Printf("Arg %d: %s\n", i, arg) + } + if runtime.GOOS == "windows" && len(os.Args) > 1 { + fmt.Println("Handling Windows service management command:", os.Args[1]) + switch os.Args[1] { + 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 + 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 + case "start": + err := startService() + if err != nil { + fmt.Printf("Failed to start service: %v\n", err) + os.Exit(1) + } + fmt.Println("Service started successfully") + return + 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 + 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 + case "debug": + runService("OlmWireguardService", true) + return + case "help", "--help", "-h": + fmt.Println("Olm WireGuard VPN Client") + fmt.Println("\nWindows Service Management:") + fmt.Println(" install Install the service") + fmt.Println(" remove Remove the service") + fmt.Println(" start Start the service") + fmt.Println(" stop Stop the service") + fmt.Println(" status Show service status") + fmt.Println(" debug Run service in debug mode") + fmt.Println("\nFor console mode, run without arguments or with standard flags.") + return + default: + fmt.Println("Unknown command:", os.Args[1]) + fmt.Println("Use 'olm --help' for usage information.") + return + } + } + + // Run in console mode + runOlmMain(context.Background()) +} + +func runOlmMain(ctx context.Context) { + // Setup Windows event logging if on Windows + if runtime.GOOS == "windows" { + setupWindowsEventLog() + } + var ( endpoint string id string @@ -210,7 +298,7 @@ func main() { var dev *device.Device var wgData WgData var holePunchData HolePunchData - var uapi *os.File + var uapiListener net.Listener var tdev tun.Device sourcePort, err := FindAvailableUDPPort(49152, 65535) @@ -327,7 +415,7 @@ func main() { errs := make(chan error) - uapi, err := uapiListen(interfaceName, fileUAPI) + uapiListener, err = uapiListen(interfaceName, fileUAPI) if err != nil { logger.Error("Failed to listen on uapi socket: %v", err) os.Exit(1) @@ -335,7 +423,7 @@ func main() { go func() { for { - conn, err := uapi.Accept() + conn, err := uapiListener.Accept() if err != nil { errs <- err return @@ -622,10 +710,16 @@ func main() { } defer olm.Close() - // Wait for interrupt signal + // Wait for interrupt signal or context cancellation sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - <-sigCh + + select { + case <-sigCh: + logger.Info("Received interrupt signal") + case <-ctx.Done(): + logger.Info("Context cancelled") + } select { case <-stopHolepunch: @@ -648,6 +742,10 @@ func main() { close(stopPing) } - uapi.Close() - dev.Close() + if uapiListener != nil { + uapiListener.Close() + } + if dev != nil { + dev.Close() + } } diff --git a/olm-service.bat b/olm-service.bat new file mode 100644 index 0000000..e1b748c --- /dev/null +++ b/olm-service.bat @@ -0,0 +1,52 @@ +@echo off +setlocal + +REM Olm Windows Service Management Script +REM This script helps manage the Olm WireGuard service on Windows + +if "%1"=="" goto :help +if "%1"=="help" goto :help +if "%1"=="/?" goto :help +if "%1"=="-h" goto :help +if "%1"=="--help" goto :help + +REM Check if running as administrator +net session >nul 2>&1 +if %errorLevel% neq 0 ( + echo Error: This script must be run as Administrator for service management. + echo Right-click and select "Run as administrator" + pause + exit /b 1 +) + +REM Execute the service command +olm.exe %* +if %errorLevel% neq 0 ( + echo Command failed with error code %errorLevel% + pause + exit /b %errorLevel% +) + +echo. +echo Operation completed successfully. +pause +exit /b 0 + +:help +echo Olm WireGuard Service Management +echo. +echo Usage: %~nx0 [command] +echo. +echo Commands: +echo install Install the Olm service +echo remove Remove the Olm service +echo start Start the Olm service +echo stop Stop the Olm service +echo status Show service status +echo debug Run in debug mode +echo help Show this help +echo. +echo Note: This script must be run as Administrator for service management. +echo Make sure olm.exe is in your PATH or in the same directory. +echo. +pause diff --git a/olm-service.ps1 b/olm-service.ps1 new file mode 100644 index 0000000..9cd8977 --- /dev/null +++ b/olm-service.ps1 @@ -0,0 +1,85 @@ +# Olm Windows Service Management Script +# This PowerShell script helps manage the Olm WireGuard service on Windows + +param( + [Parameter(Position=0)] + [ValidateSet("install", "remove", "uninstall", "start", "stop", "status", "debug", "help")] + [string]$Command = "help" +) + +function Test-Administrator { + $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object Security.Principal.WindowsPrincipal($currentUser) + return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +function Show-Help { + Write-Host "Olm WireGuard Service Management" -ForegroundColor Green + Write-Host "" + Write-Host "Usage: .\olm-service.ps1 [command]" -ForegroundColor Yellow + Write-Host "" + Write-Host "Commands:" -ForegroundColor Yellow + Write-Host " install Install the Olm service" + Write-Host " remove Remove the Olm service" + Write-Host " start Start the Olm service" + Write-Host " stop Stop the Olm service" + Write-Host " status Show service status" + Write-Host " debug Run in debug mode" + Write-Host " help Show this help" + Write-Host "" + Write-Host "Note: This script must be run as Administrator for service management." -ForegroundColor Red + Write-Host "Make sure olm.exe is in your PATH or in the same directory." -ForegroundColor Yellow +} + +function Invoke-OlmCommand { + param([string]$cmd) + + if (-not (Test-Administrator) -and $cmd -ne "status" -and $cmd -ne "help") { + Write-Error "This script must be run as Administrator for service management." + Write-Host "Right-click PowerShell and select 'Run as administrator'" -ForegroundColor Yellow + return $false + } + + try { + $olmPath = Get-Command "olm.exe" -ErrorAction SilentlyContinue + if (-not $olmPath) { + # Try current directory + $olmPath = Join-Path $PSScriptRoot "olm.exe" + if (-not (Test-Path $olmPath)) { + Write-Error "olm.exe not found in PATH or current directory" + return $false + } + } else { + $olmPath = $olmPath.Source + } + + Write-Host "Executing: $olmPath $cmd" -ForegroundColor Cyan + $result = & $olmPath $cmd + + if ($LASTEXITCODE -eq 0) { + Write-Host $result -ForegroundColor Green + Write-Host "Operation completed successfully." -ForegroundColor Green + return $true + } else { + Write-Error "Command failed with exit code: $LASTEXITCODE" + Write-Host $result -ForegroundColor Red + return $false + } + } catch { + Write-Error "Failed to execute olm.exe: $($_.Exception.Message)" + return $false + } +} + +# Main execution +switch ($Command.ToLower()) { + "help" { + Show-Help + } + default { + $success = Invoke-OlmCommand -cmd $Command + if (-not $success) { + exit 1 + } + } +} diff --git a/olm.exe.REMOVED.git-id b/olm.exe.REMOVED.git-id new file mode 100644 index 0000000..b63609a --- /dev/null +++ b/olm.exe.REMOVED.git-id @@ -0,0 +1 @@ +e077d9b8b025c4ca28748090a18728f14f60460c \ No newline at end of file diff --git a/service_unix.go b/service_unix.go new file mode 100644 index 0000000..beeaef1 --- /dev/null +++ b/service_unix.go @@ -0,0 +1,40 @@ +//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() error { + 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 isWindowsService() bool { + return false +} + +func runService(name string, isDebug bool) { + // No-op on non-Windows platforms +} + +func setupWindowsEventLog() { + // No-op on non-Windows platforms +} diff --git a/service_windows.go b/service_windows.go new file mode 100644 index 0000000..ec9bdbf --- /dev/null +++ b/service_windows.go @@ -0,0 +1,309 @@ +//go:build windows + +package main + +import ( + "context" + "fmt" + "log" + "os" + "path/filepath" + "time" + + "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 = "OlmWireguardService" + serviceDisplayName = "Olm WireGuard VPN Service" + serviceDescription = "Olm WireGuard VPN client service for secure network connectivity" +) + +type olmService struct { + elog debug.Log + ctx context.Context + stop context.CancelFunc +} + +func (s *olmService) 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} + + // Start the main olm functionality + go s.runOlm() + + changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} + + 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} + s.stop() + return false, 0 + default: + s.elog.Error(1, fmt.Sprintf("Unexpected control request #%d", c)) + } + } + } +} + +func (s *olmService) runOlm() { + // Create a context that can be cancelled when the service stops + s.ctx, s.stop = context.WithCancel(context.Background()) + + // Run the main olm logic in a separate goroutine + go func() { + defer func() { + if r := recover(); r != nil { + s.elog.Error(1, fmt.Sprintf("Olm panic: %v", r)) + } + }() + + // Call the main olm function + runOlmMain(s.ctx) + }() + + // Wait for context cancellation + <-s.ctx.Done() + s.elog.Info(1, "Olm service context cancelled") +} + +func runService(name string, isDebug bool) { + var err error + var elog debug.Log + + if isDebug { + elog = debug.New(name) + } else { + elog, err = eventlog.Open(name) + if err != nil { + return + } + } + defer elog.Close() + + elog.Info(1, fmt.Sprintf("Starting %s service", name)) + run := svc.Run + if isDebug { + run = debug.Run + } + + service := &olmService{elog: elog} + err = run(name, service) + if err != nil { + elog.Error(1, fmt.Sprintf("%s service failed: %v", name, err)) + return + } + elog.Info(1, fmt.Sprintf("%s service stopped", 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.StartAutomatic, + 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() 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() + + err = s.Start() + 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 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 + } +} + +func isWindowsService() bool { + interactive, err := svc.IsWindowsService() + return err == nil && interactive +} + +func setupWindowsEventLog() { + // Create log directory if it doesn't exist + logDir := filepath.Join(os.Getenv("PROGRAMDATA"), "Olm", "logs") + os.MkdirAll(logDir, 0755) + + logFile := filepath.Join(logDir, "olm.log") + file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err == nil { + log.SetOutput(file) + } +}