Functions/GenXdev.Windows.WireGuard/EnsureWireGuard.ps1

################################################################################
<#
.SYNOPSIS
Ensures WireGuard VPN service is installed and running via Docker container.
 
.DESCRIPTION
This function sets up and manages the WireGuard VPN service using Docker
Desktop. It automatically ensures Docker Desktop is running, pulls the latest
WireGuard Docker image, creates persistent storage volumes, and manages the
container lifecycle including health monitoring and restart capabilities.
 
WireGuard is a simple, fast, and modern VPN that utilizes state-of-the-art
cryptography. It offers superior performance and simplicity compared to
traditional VPN solutions like OpenVPN, with minimal configuration overhead
and excellent cross-platform support.
 
.PARAMETER ContainerName
The name for the Docker container. Default: "wireguard"
 
.PARAMETER VolumeName
The name for the Docker volume for persistent storage of configuration files
and client certificates. Default: "wireguard_data"
 
.PARAMETER ServicePort
The UDP port number for the WireGuard service to listen on. Must be between
1-65535. Default: 51820
 
.PARAMETER HealthCheckTimeout
Maximum time in seconds to wait for service health check before timing out.
Must be between 10-300 seconds. Default: 60
 
.PARAMETER HealthCheckInterval
Interval in seconds between health check attempts during startup validation.
Must be between 1-10 seconds. Default: 3
 
.PARAMETER ImageName
Custom Docker image name to use instead of the default. If not specified,
uses the official "linuxserver/wireguard" image from Docker Hub.
 
.PARAMETER PUID
User ID for file permissions inside the container. Should match your host
system user ID for proper file access. Default: "1000"
 
.PARAMETER PGID
Group ID for file permissions inside the container. Should match your host
system group ID for proper file access. Default: "1000"
 
.PARAMETER TimeZone
Timezone identifier to use for container logging and timestamps. Uses standard
timezone database format. Default: "Etc/UTC"
 
.PARAMETER Force
Forces complete rebuilding of Docker container and removes all existing data.
This will stop and remove existing containers and volumes, pull the latest
WireGuard image, and create a fresh container with clean configuration.
 
.EXAMPLE
EnsureWireGuard
 
.EXAMPLE
EnsureWireGuard -ContainerName "my_wireguard" -ServicePort 51821
 
.EXAMPLE
EnsureWireGuard -VolumeName "custom_vpn_data" -HealthCheckTimeout 120
 
.EXAMPLE
EnsureWireGuard -PUID 1001 -PGID 1001 -TimeZone "America/New_York"
 
.EXAMPLE
EnsureWireGuard -Force
 
.NOTES
To generate client configurations after setup:
- Run: docker exec -it wireguard /app/show-peer 1
 
For Android 10 and above:
- Install the official WireGuard app from Google Play Store
- Scan the QR code or import the config file to connect
 
For more information, see: https://www.wireguard.com/
#>

###############################################################################
function EnsureWireGuard {

    [CmdletBinding()]
    [OutputType([System.Boolean])]

    param(
        #######################################################################
        [Parameter(
            Position = 0,
            Mandatory = $false,
            HelpMessage = "The name for the Docker container"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $ContainerName = "wireguard",
        #######################################################################
        [Parameter(
            Position = 1,
            Mandatory = $false,
            HelpMessage = ("The name for the Docker volume for persistent " +
                          "storage")
        )]
        [ValidateNotNullOrEmpty()]
        [string] $VolumeName = "wireguard_data",
        #######################################################################
        [Parameter(
            Position = 2,
            Mandatory = $false,
            HelpMessage = "The port number for the WireGuard service"
        )]
        [ValidateRange(1, 65535)]
        [int] $ServicePort = 51820,
        #######################################################################
        [Parameter(
            Position = 3,
            Mandatory = $false,
            HelpMessage = ("Maximum time in seconds to wait for service " +
                          "health check")
        )]
        [ValidateRange(10, 300)]
        [int] $HealthCheckTimeout = 60,
        #######################################################################
        [Parameter(
            Position = 4,
            Mandatory = $false,
            HelpMessage = "Interval in seconds between health check attempts"
        )]
        [ValidateRange(1, 10)]
        [int] $HealthCheckInterval = 3,
        #######################################################################
        [Parameter(
            Position = 5,
            Mandatory = $false,
            HelpMessage = "Custom Docker image name to use"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $ImageName = "linuxserver/wireguard",
        #######################################################################
        [Parameter(
            Position = 6,
            Mandatory = $false,
            HelpMessage = "User ID for permissions in the container"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $PUID = "1000",
        #######################################################################
        [Parameter(
            Position = 7,
            Mandatory = $false,
            HelpMessage = "Group ID for permissions in the container"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $PGID = "1000",
        #######################################################################
        [Parameter(
            Position = 8,
            Mandatory = $false,
            HelpMessage = "Timezone to use for the container"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $TimeZone = "Etc/UTC",
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ("Force rebuild of Docker container and remove " +
                          "existing data")
        )]
        [Alias("ForceRebuild")]
        [switch] $Force
        #######################################################################
    )    begin {

        # set script-scoped variables from parameters for container management
        $script:containerName = $ContainerName

        # set docker image name for pulling and container creation
        $script:imageName = $ImageName

        # set script-scoped variables for docker volume and networking
        $script:volumeName = $VolumeName

        # set script-scoped variables for service configuration
        $script:servicePort = $ServicePort

        # set script-scoped variables for health monitoring timeouts
        $script:healthCheckTimeout = $HealthCheckTimeout

        # set script-scoped variable for health check retry intervals
        $script:healthCheckInterval = $HealthCheckInterval

        # set script-scoped variables for container environment settings
        $script:puid = $PUID

        $script:pgid = $PGID

        $script:timezone = $TimeZone

        # store original location for cleanup at the end of the function
        $script:originalLocation = `
            (Microsoft.PowerShell.Management\Get-Location).Path

        #######################################################################
        <#
        .SYNOPSIS
        Tests if Docker is available and responsive to commands.
 
        .DESCRIPTION
        Verifies Docker Desktop is running by attempting to query the Docker
        version. Returns true if Docker responds successfully, false otherwise.
        #>

        function Test-DockerAvailability {

            try {

                # attempt to get docker version to verify docker is running
                $null = docker version --format "{{.Server.Version}}" 2>$null

                return $LASTEXITCODE -eq 0
            }
            catch {

                return $false
            }
        }
        #######################################################################
        <#
        .SYNOPSIS
        Tests if a Docker image exists locally.
 
        .DESCRIPTION
        Queries Docker for the existence of a specific image by name. Returns
        true if the image is found locally, false if not found or on error.
 
        .PARAMETER ImageName
        The Docker image name to search for in the local image repository.
        #>

        function Test-DockerImage {

            param([string]$ImageName)

            try {

                # query docker for existing images matching the specified name
                $images = docker images $ImageName --format "{{.Repository}}" `
                    2>$null

                return -not [string]::IsNullOrWhiteSpace($images)
            }
            catch {

                return $false
            }
        }
        #######################################################################
        <#
        .SYNOPSIS
        Tests if a Docker container exists (running or stopped).
 
        .DESCRIPTION
        Searches for a Docker container by exact name match, including both
        running and stopped containers. Returns true if found, false otherwise.
 
        .PARAMETER ContainerName
        The exact container name to search for in Docker container list.
        #>

        function Test-DockerContainer {

            param([string]$ContainerName)

            try {

                # search for containers with exact name match including stopped ones
                $containers = docker ps -a --filter "name=^${ContainerName}$" `
                    --format "{{.ID}}" 2>$null

                return -not [string]::IsNullOrWhiteSpace($containers)
            }
            catch {

                return $false
            }
        }
        #######################################################################
        <#
        .SYNOPSIS
        Tests if a Docker container is currently running.
 
        .DESCRIPTION
        Checks specifically for running containers with the given name. Returns
        true if the container exists and is actively running, false otherwise.
 
        .PARAMETER ContainerName
        The exact container name to check for running status.
        #>

        function Test-DockerContainerRunning {

            param([string]$ContainerName)

            try {

                # search for running containers with exact name match
                $containers = docker ps --filter "name=^${ContainerName}$" `
                    --format "{{.ID}}" 2>$null

                return -not [string]::IsNullOrWhiteSpace($containers)
            }
            catch {

                return $false
            }
        }
        #######################################################################
        <#
        .SYNOPSIS
        Safely removes a Docker container with proper error handling.
 
        .DESCRIPTION
        Stops and removes a Docker container if it exists. Uses ShouldProcess
        for confirmation and provides verbose logging. Handles errors gracefully
        without throwing exceptions.
 
        .PARAMETER ContainerName
        The name of the container to stop and remove from Docker.
        #>

        function Remove-DockerContainer {

            [CmdletBinding(SupportsShouldProcess)]
            param([string]$ContainerName)

            try {

                # check if container exists before attempting removal
                if (Test-DockerContainer $ContainerName) {

                    if ($PSCmdlet.ShouldProcess($ContainerName,
                        "Stop and remove Docker container")) {

                        # output verbose information about container removal
                        Microsoft.PowerShell.Utility\Write-Verbose `
                            "Stopping and removing container: $ContainerName"

                        # stop the container gracefully before removal
                        $null = docker stop $ContainerName 2>$null

                        # remove the container completely from docker
                        $null = docker rm $ContainerName 2>$null
                    }
                }
            }
            catch {

                # warn about container removal failures without throwing
                Microsoft.PowerShell.Utility\Write-Warning `
                    "Failed to remove container ${ContainerName}: $_"
            }
        }
        #######################################################################
        <#
        .SYNOPSIS
        Safely removes a Docker volume with proper error handling.
 
        .DESCRIPTION
        Removes a Docker volume if it exists. Uses ShouldProcess for
        confirmation and provides verbose logging. Handles errors gracefully
        without throwing exceptions.
 
        .PARAMETER VolumeName
        The name of the Docker volume to remove from the system.
        #>

        function Remove-DockerVolume {

            [CmdletBinding(SupportsShouldProcess)]
            param([string]$VolumeName)

            try {

                if ($PSCmdlet.ShouldProcess($VolumeName,
                    "Remove Docker volume")) {

                    # output verbose information about volume removal
                    Microsoft.PowerShell.Utility\Write-Verbose `
                        "Removing Docker volume: $VolumeName"

                    # remove the docker volume and discard output
                    $null = docker volume rm $VolumeName 2>$null
                }
            }
            catch {

                # warn about volume removal failures without throwing
                Microsoft.PowerShell.Utility\Write-Warning `
                    "Failed to remove volume ${VolumeName}: $_"
            }
        }
        #######################################################################
        <#
        .SYNOPSIS
        Tests if the WireGuard service is healthy and responding.
 
        .DESCRIPTION
        Performs health checks on the WireGuard container by examining container
        logs for startup indicators and checking if the service port is
        listening. Returns true if healthy, false otherwise.
        #>

        function Test-ServiceHealth {

            try {

                # first check if container is running before health tests
                $containerRunning = Test-DockerContainerRunning `
                    $script:containerName

                if (-not $containerRunning) {

                    return $false
                }

                # check container logs for successful startup messages
                $logs = docker logs $script:containerName 2>&1

                # look for indications that wireguard is running properly
                if ($logs -match "Server started" -or
                    $logs -match "WireGuard started" -or
                    $logs -match "UDP listening") {

                    # log successful health check for debugging
                    Microsoft.PowerShell.Utility\Write-Verbose `
                        "WireGuard service health check passed"

                    return $true
                }

                # alternatively check if the port is listening using netstat
                $netstatOutput = & netstat -an |
                    Microsoft.PowerShell.Utility\Select-String `
                        -Pattern ":$($script:servicePort) "

                if (-not [string]::IsNullOrEmpty($netstatOutput)) {

                    # log successful health check for debugging
                    Microsoft.PowerShell.Utility\Write-Verbose `
                        ("WireGuard service health check passed " +
                         "(port is listening)")

                    return $true
                }

                return $false
            }
            catch {

                # log failed health check with error details for debugging
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "WireGuard service health check failed: $_"

                return $false
            }
        }
        #######################################################################
        <#
        .SYNOPSIS
        Waits for the WireGuard service to become healthy and ready.
 
        .DESCRIPTION
        Repeatedly checks service health until it becomes ready or timeout is
        reached. Uses configurable timeout and interval settings for retry
        logic. Returns true if service becomes ready, false on timeout.
        #>

        function Wait-ServiceReady {

            # output verbose information about waiting for service readiness
            Microsoft.PowerShell.Utility\Write-Verbose `
                "Waiting for WireGuard service to become ready..."

            # initialize retry counter for health check attempts
            $retryCount = 0

            # calculate maximum retry attempts based on timeout and interval
            $maxRetries = [math]::Floor($script:healthCheckTimeout / `
                $script:healthCheckInterval)

            while ($retryCount -lt $maxRetries) {

                # test service health and return success if ready
                if (Test-ServiceHealth) {

                    # log successful service readiness
                    Microsoft.PowerShell.Utility\Write-Verbose `
                        "WireGuard service is ready and responding"

                    return $true
                }

                # increment retry counter for next attempt
                $retryCount++

                # output verbose information about retry attempt progress
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "Service not ready yet, attempt $retryCount/$maxRetries..."

                # wait between health check attempts as configured
                Microsoft.PowerShell.Utility\Start-Sleep `
                    -Seconds $script:healthCheckInterval
            }

            # warn about service readiness timeout after all retries
            Microsoft.PowerShell.Utility\Write-Warning `
                ("WireGuard service did not become ready within " +
                 "$script:healthCheckTimeout seconds")

            return $false
        }
        #######################################################################
        <#
        .SYNOPSIS
        Pulls the latest WireGuard Docker image from registry.
 
        .DESCRIPTION
        Downloads the specified WireGuard Docker image from Docker Hub or
        configured registry. Provides verbose logging and error handling.
        Returns true on success, false on failure.
        #>

        function Get-WireGuardImage {

            try {

                # output verbose information about docker image pull operation
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "Pulling WireGuard image: $script:imageName"

                # pull the specified docker image from registry
                $pullResult = docker pull $script:imageName 2>&1

                # check if docker pull command failed
                if ($LASTEXITCODE -ne 0) {

                    throw "Failed to pull WireGuard image: $pullResult"
                }

                # log successful image pull completion
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "✅ WireGuard image pulled successfully"

                return $true
            }
            catch {

                # log error details for image pull failure
                Microsoft.PowerShell.Utility\Write-Error `
                    "Failed to pull WireGuard image: $_"

                return $false
            }
        }
        #######################################################################
        <#
        .SYNOPSIS
        Creates and starts a new WireGuard Docker container.
 
        .DESCRIPTION
        Creates a new WireGuard container with proper configuration including
        networking capabilities, persistent volume mounting, environment
        variables, and restart policies. Uses ShouldProcess for confirmation.
        #>

        function New-WireGuardContainer {

            [CmdletBinding(SupportsShouldProcess)]
            param()

            try {

                # output verbose information about container creation process
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "Creating WireGuard container..."

                # check if docker volume already exists in the system
                $volumeExists = docker volume ls `
                    --filter "name=^${script:volumeName}$" `
                    --format "{{.Name}}" 2>$null

                # create docker volume if it doesn't exist yet
                if ([string]::IsNullOrWhiteSpace($volumeExists)) {

                    # use shouldprocess to confirm volume creation
                    if ($PSCmdlet.ShouldProcess("$script:volumeName",
                        "Create Docker volume")) {

                        # output verbose information about volume creation
                        Microsoft.PowerShell.Utility\Write-Verbose `
                            "Creating Docker volume: $script:volumeName"

                        # create the docker volume for persistent storage
                        $volumeResult = docker volume create $script:volumeName `
                            2>&1

                        # check if volume creation failed
                        if ($LASTEXITCODE -ne 0) {

                            throw ("Failed to create Docker volume " +
                                  "$script:volumeName`: $volumeResult")
                        }
                    }
                }

                # prepare docker run arguments for container creation
                $dockerArgs = @(
                    "run", "-d"
                    "--name", $script:containerName
                    "--cap-add", "NET_ADMIN"
                    "--cap-add", "SYS_MODULE"
                    "-e", "PUID=$($script:puid)"
                    "-e", "PGID=$($script:pgid)"
                    "-e", "TZ=$($script:timezone)"
                    "-p", "$($script:servicePort):51820/udp"
                    "-v", "$($script:volumeName):/config"
                    "--restart", "unless-stopped"
                )

                # add the docker image name as final argument
                $dockerArgs += $script:imageName

                # output verbose information about docker command execution
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "Docker command: docker $($dockerArgs -join ' ')"

                # use shouldprocess to confirm container creation
                if ($PSCmdlet.ShouldProcess("$script:containerName",
                    "Create WireGuard container")) {

                    # execute docker run command to create container
                    $result = & docker @dockerArgs 2>&1

                    # check if container creation failed
                    if ($LASTEXITCODE -ne 0) {

                        throw "Failed to create container: $result"
                    }
                }

                # wait for container to initialize properly after creation
                Microsoft.PowerShell.Utility\Start-Sleep -Seconds 5

                # log successful container creation
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "✅ WireGuard container created successfully"

                return $true
            }
            catch {

                # log error details for container creation failure
                Microsoft.PowerShell.Utility\Write-Error `
                    "Failed to create WireGuard container: $_"

                return $false
            }
        }
        #######################################################################
    }    process {

        try {

            # ensure docker desktop is available and running properly
            Microsoft.PowerShell.Utility\Write-Verbose `
                "Ensuring Docker Desktop is available..."

            GenXdev.Windows\EnsureDockerDesktop

            # verify docker is responding to commands after ensuring it's running
            if (-not (Test-DockerAvailability)) {

                throw "Docker is not available or not responding"
            }

            # handle force cleanup if requested by user for fresh installation
            if ($Force) {

                # output verbose information about forced cleanup process
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "Force flag specified - cleaning up existing resources..."

                # remove existing container and volume for clean slate
                Remove-DockerContainer $script:containerName

                Remove-DockerVolume $script:volumeName
            }

            # ensure we have the latest wireguard image available locally
            if (-not (Test-DockerImage $script:imageName) -or $Force) {

                # pull the docker image if not present or force specified
                if (-not (Get-WireGuardImage)) {

                    throw "Failed to obtain WireGuard Docker image"
                }
            }
            else {

                # log that image is already available locally
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "✅ WireGuard image already available"
            }

            # check current container state for appropriate action
            $containerExists = Test-DockerContainer $script:containerName

            $containerRunning = Test-DockerContainerRunning $script:containerName

            # handle existing container scenarios based on current state
            if ($containerExists) {

                # check if container is currently running
                if ($containerRunning) {

                    # verify container health for running container
                    if (Test-ServiceHealth) {

                        # log successful health check result
                        Microsoft.PowerShell.Utility\Write-Verbose `
                            "✅ WireGuard container is healthy and responding"
                    }
                    else {

                        # restart unhealthy running container to fix issues
                        Microsoft.PowerShell.Utility\Write-Verbose `
                            ("Container is running but not responding - " +
                             "restarting...")

                        # restart the container to fix health issues
                        $null = docker restart $script:containerName 2>$null

                        # wait for container to restart properly
                        Microsoft.PowerShell.Utility\Start-Sleep -Seconds 10

                        # wait for service to become ready after restart
                        $serviceReady = Wait-ServiceReady

                        # warn if service is not ready after restart attempt
                        if (-not $serviceReady) {

                            Microsoft.PowerShell.Utility\Write-Warning `
                                ("WireGuard service may not be fully ready " +
                                 "after restart")
                        }
                    }
                }
                else {

                    # start existing stopped container
                    Microsoft.PowerShell.Utility\Write-Verbose `
                        "Starting existing container..."

                    # start the stopped container
                    $null = docker start $script:containerName 2>$null

                    # wait for container to start properly
                    Microsoft.PowerShell.Utility\Start-Sleep -Seconds 10

                    # wait for service to become ready after start
                    $serviceReady = Wait-ServiceReady

                    # log or warn about service readiness after start
                    if ($serviceReady) {

                        Microsoft.PowerShell.Utility\Write-Verbose `
                            "✅ WireGuard service is ready after container start"
                    }
                    else {

                        Microsoft.PowerShell.Utility\Write-Warning `
                            ("WireGuard service may not be fully ready " +
                             "after start")
                    }
                }
            }
            else {

                # create and start new container when none exists
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "Creating and starting WireGuard container..."

                # attempt to create new wireguard container
                if (-not (New-WireGuardContainer)) {

                    throw "Failed to create WireGuard container"
                }

                # wait for service to be ready after creation
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "Waiting for WireGuard service to be ready..."

                $serviceReady = Wait-ServiceReady

                # log or warn about service readiness after creation
                if ($serviceReady) {

                    Microsoft.PowerShell.Utility\Write-Verbose `
                        "✅ WireGuard service is ready"
                }
                else {

                    Microsoft.PowerShell.Utility\Write-Warning `
                        ("WireGuard service may not be fully ready " +
                         "after creation")
                }
            }

            # perform final validation of service state before returning
            Microsoft.PowerShell.Utility\Write-Verbose `
                "Performing final validation..."

            # check if container is running and service is healthy
            if ((Test-DockerContainerRunning $script:containerName) -and `
                (Test-ServiceHealth)) {

                # log successful service operation
                Microsoft.PowerShell.Utility\Write-Verbose `
                    ("✅ WireGuard VPN service is fully " +
                     "operational on port $script:servicePort")

                # display instructions for client configuration to user
                Microsoft.PowerShell.Utility\Write-Host -ForegroundColor Green @"
 
To generate client configurations:
- Run: docker exec -it $script:containerName /app/show-peer 1
This will display a QR code or config file for the client.
 
For Android 10 and above:
1. Install the official WireGuard app from Google Play Store
2. Scan the QR code or import the config file to connect
"@


                return $true
            }
            else {

                # warn about potential service issues
                Microsoft.PowerShell.Utility\Write-Warning `
                    "WireGuard service may not be fully operational"

                return $false
            }
        }
        catch {

            # log error details for any failures in the process
            Microsoft.PowerShell.Utility\Write-Error `
                "Failed to ensure WireGuard service: $_"

            throw
        }
    }    end {

        # restore original location for cleanup after function execution
        Microsoft.PowerShell.Management\Set-Location $script:originalLocation
    }
}
################################################################################