Working windows service

Former-commit-id: a85f83cc20
This commit is contained in:
Owen
2025-07-23 20:35:00 -07:00
parent 3ad36f95e1
commit 4c58cd6eff
7 changed files with 655 additions and 7 deletions

View File

@@ -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

112
main.go
View File

@@ -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()
}
}

52
olm-service.bat Normal file
View File

@@ -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

85
olm-service.ps1 Normal file
View File

@@ -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
}
}
}

1
olm.exe.REMOVED.git-id Normal file
View File

@@ -0,0 +1 @@
e077d9b8b025c4ca28748090a18728f14f60460c

40
service_unix.go Normal file
View File

@@ -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
}

309
service_windows.go Normal file
View File

@@ -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)
}
}