Functions/GenXdev.Windows.WireGuard/Add-WireGuardPeer.ps1

################################################################################
<#
.SYNOPSIS
Adds a new WireGuard VPN peer (client) configuration to the server.
 
.DESCRIPTION
This function adds a new peer to the WireGuard VPN server running in a Docker
container. It generates a new client configuration with a unique IP address,
creates necessary cryptographic keys, and returns the configuration details.
The function can optionally save the configuration to a file or display a QR
code for easy mobile device setup. The function validates peer names, checks
for duplicates, and handles various error conditions gracefully.
 
.PARAMETER PeerName
A unique name for the peer that will be used to identify the peer in the
WireGuard configuration and for the generated configuration filename. The name
must be less than 100 characters and cannot contain invalid characters like
< > " / \ | ? * :
 
.PARAMETER AllowedIPs
The IP ranges that will be routed through the VPN for this peer. Specify
multiple ranges separated by commas. Default is "0.0.0.0/0, ::/0" which
routes all IPv4 and IPv6 traffic through the VPN tunnel.
 
.PARAMETER DNS
DNS servers to use for this peer when connected to the VPN. Specify multiple
servers separated by commas. Default is "1.1.1.1, 1.0.0.1" which uses
Cloudflare's secure DNS servers.
 
.PARAMETER OutputPath
The directory path where the peer configuration file should be saved when
SaveConfig is enabled. The directory will be created if it doesn't exist.
Default is "$env:USERPROFILE\WireGuardConfigs".
 
.PARAMETER ContainerName
The name of the Docker container running the WireGuard service. Must match
the container name used when initializing the service. Default is "wireguard".
 
.PARAMETER VolumeName
The name of the Docker volume used for persistent storage of WireGuard
configurations and keys. Must match the volume name used during
initialization. Default is "wireguard_data".
 
.PARAMETER ServicePort
The UDP port number that the WireGuard service listens on for VPN
connections. Must be a valid port number between 1 and 65535. Default is
51820 which is the standard WireGuard port.
 
.PARAMETER HealthCheckTimeout
Maximum time in seconds to wait for the WireGuard service health check to
complete. Valid range is 10 to 300 seconds. Default is 60 seconds.
 
.PARAMETER HealthCheckInterval
Interval in seconds between health check attempts when waiting for the
service to become ready. Valid range is 1 to 10 seconds. Default is 3
seconds.
 
.PARAMETER ImageName
The Docker image name to use for the WireGuard container. Default is
"linuxserver/wireguard" which is the official LinuxServer.io WireGuard image.
 
.PARAMETER PUID
User ID for file permissions inside the Docker container. Should match the
host user ID to avoid permission issues with volume mounts. Default is "1000".
 
.PARAMETER PGID
Group ID for file permissions inside the Docker container. Should match the
host group ID to avoid permission issues with volume mounts. Default is
"1000".
 
.PARAMETER TimeZone
Timezone to use for the container logs and scheduling. Should be a valid
timezone identifier. Default is "Etc/UTC" for consistent UTC timestamps.
 
.PARAMETER SaveConfig
When specified, saves the generated peer configuration to a .conf file in the
OutputPath directory. The file will be named with the peer name followed by
.conf extension.
 
.PARAMETER ShowQRCode
When specified, displays a QR code in the console that can be scanned by the
WireGuard mobile app for easy configuration import. Useful for mobile device
setup.
 
.PARAMETER NoDockerInitialize
When specified, skips the Docker container initialization check. Use this
when the function is called by another function that has already ensured the
WireGuard service is running.
 
.PARAMETER Force
When specified, forces a rebuild of the Docker container and removes existing
data. This will destroy all existing peer configurations and server keys.
 
.PARAMETER WhatIf
Shows what would happen if the cmdlet runs. The cmdlet is not run.
 
.PARAMETER Confirm
Prompts you for confirmation before running the cmdlet.
 
.EXAMPLE
Add-WireGuardPeer -PeerName "MyPhone" -AllowedIPs "0.0.0.0/0, ::/0" `
    -DNS "1.1.1.1, 1.0.0.1" -SaveConfig -OutputPath `
    "$env:USERPROFILE\WireGuardConfigs" -ShowQRCode -ContainerName "wireguard" `
    -VolumeName "wireguard_data" -ServicePort 51820 -HealthCheckTimeout 60 `
    -HealthCheckInterval 3 -ImageName "linuxserver/wireguard" -PUID "1000" `
    -PGID "1000" -TimeZone "Etc/UTC"
 
.EXAMPLE
Add-WireGuardPeer "MyTablet" -ShowQRCode
 
.NOTES
This function interacts with the linuxserver/wireguard Docker container to
manage WireGuard peers. It requires Docker to be installed and the WireGuard
container to be running. Use EnsureWireGuard function first to initialize the
service if needed.
#>


###############################################################################
function Add-WireGuardPeer {

    [CmdletBinding(SupportsShouldProcess)]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]

    param(
        #######################################################################
        [Parameter(
            Position = 0,
            Mandatory = $true,
            HelpMessage = "A unique name for the peer"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $PeerName,
        #######################################################################
        [Parameter(
            Position = 1,
            Mandatory = $false,
            HelpMessage = ("The IP ranges that will be routed through the " +
                "VPN")
        )]
        [ValidateNotNullOrEmpty()]
        [string] $AllowedIPs = "0.0.0.0/0, ::/0",
        #######################################################################
        [Parameter(
            Position = 2,
            Mandatory = $false,
            HelpMessage = "DNS servers to use for this peer"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $DNS = "1.1.1.1, 1.0.0.1",
        #######################################################################
        [Parameter(
            Position = 3,
            Mandatory = $false,
            HelpMessage = ("The path where the peer configuration file " +
                "should be saved")
        )]
        [ValidateNotNullOrEmpty()]
        [string] $OutputPath = "$env:USERPROFILE\WireGuardConfigs",
        #######################################################################
        [Parameter(
            Position = 4,
            Mandatory = $false,
            HelpMessage = "The name for the Docker container"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $ContainerName = "wireguard",
        #######################################################################
        [Parameter(
            Position = 5,
            Mandatory = $false,
            HelpMessage = ("The name for the Docker volume for persistent " +
                "storage")
        )]
        [ValidateNotNullOrEmpty()]
        [string] $VolumeName = "wireguard_data",
        #######################################################################
        [Parameter(
            Position = 6,
            Mandatory = $false,
            HelpMessage = "The port number for the WireGuard service"
        )]
        [ValidateRange(1, 65535)]
        [int] $ServicePort = 51820,
        #######################################################################
        [Parameter(
            Position = 7,
            Mandatory = $false,
            HelpMessage = ("Maximum time in seconds to wait for service " +
                "health check")
        )]
        [ValidateRange(10, 300)]
        [int] $HealthCheckTimeout = 60,
        #######################################################################
        [Parameter(
            Position = 8,
            Mandatory = $false,
            HelpMessage = ("Interval in seconds between health check " +
                "attempts")
        )]
        [ValidateRange(1, 10)]
        [int] $HealthCheckInterval = 3,
        #######################################################################
        [Parameter(
            Position = 9,
            Mandatory = $false,
            HelpMessage = "Custom Docker image name to use"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $ImageName = "linuxserver/wireguard",
        #######################################################################
        [Parameter(
            Position = 10,
            Mandatory = $false,
            HelpMessage = "User ID for permissions in the container"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $PUID = "1000",
        #######################################################################
        [Parameter(
            Position = 11,
            Mandatory = $false,
            HelpMessage = "Group ID for permissions in the container"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $PGID = "1000",
        #######################################################################
        [Parameter(
            Position = 12,
            Mandatory = $false,
            HelpMessage = "Timezone to use for the container"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $TimeZone = "Etc/UTC",
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Save the peer configuration to a file"
        )]
        [switch] $SaveConfig,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ("Show QR code in the console for easy mobile " +
                "setup")
        )]
        [switch] $ShowQRCode,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ("Skip Docker initialization (used when already " +
                "called by parent function)")
        )]
        [switch] $NoDockerInitialize,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ("Force rebuild of Docker container and remove " +
                "existing data")
        )]
        [Alias("ForceRebuild")]
        [switch] $Force
        #######################################################################
    )    begin {

        # ensure the wireguard service is running unless explicitly skipped
        if (-not $NoDockerInitialize) {

            # log verbose message about ensuring service availability
            Microsoft.PowerShell.Utility\Write-Verbose `
                "Ensuring WireGuard service is available"

            # copy matching parameters to pass to ensurewireguard function
            $ensureParams = GenXdev.Helpers\Copy-IdenticalParamValues `
                -BoundParameters $PSBoundParameters `
                -FunctionName 'EnsureWireGuard' `
                -DefaultValues (Microsoft.PowerShell.Utility\Get-Variable `
                    -Scope Local `
                    -ErrorAction SilentlyContinue)

            # initialize wireguard service with specified parameters
            $null = GenXdev.Windows\EnsureWireGuard @ensureParams
        }
        else {

            # log verbose message about skipping docker initialization
            Microsoft.PowerShell.Utility\Write-Verbose `
                "Skipping Docker initialization as requested"
        }

        #######################################################################
        # define helper function to validate peer name format and constraints
        function Test-PeerNameFormat {

            param([string]$peerName)

            # check for null or whitespace peer name
            if ([string]::IsNullOrWhiteSpace($peerName)) {
                throw "Peer name cannot be empty or whitespace"
            }

            # check for peer name length exceeding maximum allowed
            if ($peerName.Length -gt 100) {
                throw ("Peer name is too long. Maximum length is 100 " +
                    "characters")
            }

            # define list of characters that are invalid for peer names
            $invalidChars = @('<', '>', '"', '/', '\', '|', '?', '*', ':')

            # check each character in peer name for invalid characters
            foreach ($char in $invalidChars) {

                # throw error if invalid character is found
                if ($peerName.Contains($char)) {
                    throw "Peer name contains invalid character '$char'"
                }
            }

            return $true
        }

        #######################################################################
        # define helper function to check if peer configuration already exists
        function Test-PeerExist {

            param([string]$peerName)

            try {

                # check for existing peer configuration files in container
                $existingPeers = & docker exec $ContainerName sh -c `
                    "ls -1 /config/peer_*"

                # iterate through existing peer configurations
                foreach ($existingPeer in $existingPeers) {

                    # check if current peer name matches existing peer
                    if ($existingPeer -match "peer_$peerName") {
                        return $true
                    }
                }

                return $false
            }
            catch {

                # log warning if unable to verify peer existence
                Microsoft.PowerShell.Utility\Write-Warning `
                    "Unable to verify peer existence: $_"

                # proceed with creation attempt if verification fails
                return $false
            }
        }
    }

    process {

        try {

            # validate peer name format using helper function
            Test-PeerNameFormat -peerName $PeerName

            # check if peer already exists using helper function
            if (Test-PeerExist -peerName $PeerName) {
                throw "A peer with name '$PeerName' already exists"
            }

            # ask for confirmation before creating the peer
            if ($PSCmdlet.ShouldProcess("WireGuard peer '$PeerName'",
                "Add peer")) {

                # log verbose message about adding new peer
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "Adding new WireGuard peer: $PeerName"

                # create the peer in the docker container using add-peer script
                $result = docker exec $ContainerName sh -c "/app/add-peer $PeerName"

                # check if peer creation command succeeded
                if ($LASTEXITCODE -ne 0) {
                    throw "Failed to add peer: $result"
                }

                # log verbose message about successful peer addition
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "Peer $PeerName added successfully"

                # get the peer configuration details from container
                $peerConfig = docker exec $ContainerName sh -c `
                    "cat /config/peer_$PeerName/peer_$PeerName.conf"

                # check if configuration retrieval succeeded and is not empty
                if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrEmpty($peerConfig)) {
                    throw "Failed to retrieve peer configuration"
                }

                # save configuration to file if requested by user
                if ($SaveConfig) {

                    # ask for confirmation before saving configuration file
                    if ($PSCmdlet.ShouldProcess(
                        "Configuration file for peer '$PeerName'",
                        "Save to $OutputPath")) {

                        # create output directory if it doesn't exist
                        if (-not (Microsoft.PowerShell.Management\Test-Path `
                            $OutputPath)) {

                            # create directory with force flag to create parent dirs
                            $null = Microsoft.PowerShell.Management\New-Item `
                                -ItemType Directory `
                                -Path $OutputPath `
                                -Force
                        }

                        # build full path for configuration file
                        $configFile = Microsoft.PowerShell.Management\Join-Path `
                            -Path $OutputPath `
                            -ChildPath "$PeerName.conf"

                        # write peer configuration to file with utf8 encoding
                        $peerConfig |
                            Microsoft.PowerShell.Utility\Out-File `
                                -FilePath $configFile `
                                -Encoding utf8 `
                                -Force

                        # display success message with configuration file location
                        Microsoft.PowerShell.Utility\Write-Host `
                            -ForegroundColor Green `
                            "Peer configuration saved to: $configFile"
                    }
                }

                # show qr code if requested by user
                if ($ShowQRCode) {

                    # display message about qr code generation
                    Microsoft.PowerShell.Utility\Write-Host `
                        "Generating QR code for peer $PeerName..."

                    # generate qr code using show-peer script in container
                    $qrCode = docker exec $ContainerName sh -c `
                        "/app/show-peer $PeerName"

                    # check if qr code generation succeeded
                    if ($LASTEXITCODE -eq 0) {

                        # display the qr code to console
                        Microsoft.PowerShell.Utility\Write-Host $qrCode

                        # display instruction message for qr code usage
                        Microsoft.PowerShell.Utility\Write-Host `
                            -ForegroundColor Green `
                            "Scan this QR code with the WireGuard mobile app"
                    }
                    else {

                        # log warning if qr code generation failed
                        Microsoft.PowerShell.Utility\Write-Warning `
                            "Failed to generate QR code: $qrCode"
                    }
                }

                # create hash table with peer details for return object
                $peerDetails = @{
                    PeerName = $PeerName
                    Configuration = $peerConfig
                    ConfigurationPath = if ($SaveConfig) { $configFile } else {
                        $null
                    }
                    QRCodeGenerated = $ShowQRCode
                }

                # return peer details as powershell custom object
                return [PSCustomObject]$peerDetails
            }
        }
        catch [System.Net.WebException] {

            # log error for network-related exceptions
            Microsoft.PowerShell.Utility\Write-Error `
                "Network error when adding WireGuard peer: $_"
            throw
        }
        catch [System.TimeoutException] {

            # log error for timeout-related exceptions
            Microsoft.PowerShell.Utility\Write-Error `
                "Timeout when adding WireGuard peer: $_"
            throw
        }
        catch {

            # log error for all other exceptions
            Microsoft.PowerShell.Utility\Write-Error `
                "Failed to add WireGuard peer: $_"
            throw
        }
    }

    end {

    }
}
################################################################################