Functions/GenXdev.Windows.WireGuard/Get-WireGuardPeers.ps1

################################################################################
<#
.SYNOPSIS
Gets information about all WireGuard VPN peers configured on the system.
 
.DESCRIPTION
This function retrieves comprehensive information about all WireGuard VPN peers
configured on the server running in a Docker container. It provides detailed
information including peer names, public keys, allowed IP addresses, connection
endpoints, handshake status, data transfer statistics, and current connection
status. The function can operate in conjunction with parent functions or
independently manage Docker container initialization.
 
.PARAMETER NoDockerInitialize
Skip Docker initialization process when this function is called by a parent
function that has already handled container setup.
 
.PARAMETER Force
Force complete rebuild of the Docker container and remove all existing
persistent data before starting fresh.
 
.PARAMETER ContainerName
The name identifier for the Docker container running the WireGuard service.
Must be a valid Docker container name.
 
.PARAMETER VolumeName
The name identifier for the Docker volume used for persistent storage of
WireGuard configuration and peer data.
 
.PARAMETER ServicePort
The UDP port number on which the WireGuard service listens for VPN connections.
Must be within valid port range.
 
.PARAMETER HealthCheckTimeout
Maximum time in seconds to wait for the WireGuard service to respond to health
check attempts before considering it failed.
 
.PARAMETER HealthCheckInterval
Interval in seconds between consecutive health check attempts when waiting for
the service to become ready.
 
.PARAMETER ImageName
Custom Docker image name to use instead of the default linuxserver/wireguard
image for the container.
 
.PARAMETER PUID
User ID for file permissions and process ownership within the Docker container.
Used for security and permission management.
 
.PARAMETER PGID
Group ID for file permissions and process ownership within the Docker container.
Used for security and permission management.
 
.PARAMETER TimeZone
Timezone setting to use for the container's system clock and log timestamps.
Should be a valid timezone identifier.
 
.EXAMPLE
Get-WireGuardPeers
 
.EXAMPLE
Get-WireGuardPeers -NoDockerInitialize -ContainerName "custom_wireguard" `
                   -ServicePort 55555
 
.NOTES
This function interacts with the linuxserver/wireguard Docker container to
retrieve information about configured WireGuard peers. The container must be
running and accessible. Use EnsureWireGuard function first if container setup
is required.
#>

###############################################################################
function Get-WireGuardPeers {

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

    param(
        ###############################################################################
        [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,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "The name for the Docker container"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $ContainerName = "wireguard",
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "The name for the Docker volume for persistent storage"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $VolumeName = "wireguard_data",
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "The port number for the WireGuard service"
        )]
        [ValidateRange(1, 65535)]
        [int] $ServicePort = 51820,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ("Maximum time in seconds to wait for service health " +
                          "check")
        )]
        [ValidateRange(10, 300)]
        [int] $HealthCheckTimeout = 60,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Interval in seconds between health check attempts"
        )]
        [ValidateRange(1, 10)]
        [int] $HealthCheckInterval = 3,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Custom Docker image name to use"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $ImageName = "linuxserver/wireguard",
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "User ID for permissions in the container"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $PUID = "1000",
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Group ID for permissions in the container"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $PGID = "1000",
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Timezone to use for the container"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $TimeZone = "Etc/UTC"
        ###############################################################################
    )    begin {

        # ensure the WireGuard service is running
        if (-not $NoDockerInitialize) {

            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 {

            Microsoft.PowerShell.Utility\Write-Verbose `
                "Skipping Docker initialization as requested"
        }
    }

    process {

        try {

            Microsoft.PowerShell.Utility\Write-Verbose `
                "Retrieving WireGuard peers"

            # get peer information from WireGuard container using wg show command
            $wgOutput = docker exec $ContainerName wg show

            # check if the docker command executed successfully
            if ($LASTEXITCODE -ne 0) {

                throw "Failed to retrieve WireGuard peers: $wgOutput"
            }

            # parse the wg show output and convert to objects
            $peers = @()
            $currentPeer = $null

            # process each line of the wg show output
            foreach ($line in $wgOutput -split "`n") {

                # check if line contains peer public key information
                if ($line -match '^peer: (.+)$') {

                    # save previous peer if it exists
                    if ($null -ne $currentPeer) {

                        $peers += $currentPeer
                    }

                    # create new peer object with default values
                    $currentPeer = @{
                        PublicKey = $matches[1]
                        Name = $null
                        AllowedIPs = $null
                        Endpoint = $null
                        LatestHandshake = $null
                        TransferRx = $null
                        TransferTx = $null
                        PersistentKeepalive = $null
                        Status = $null
                    }
                }
                elseif ($null -ne $currentPeer) {

                    # check if line contains allowed IPs information
                    if ($line -match '^\s+allowed ips: (.+)$') {

                        $currentPeer.AllowedIPs = $matches[1]
                    }
                    # check if line contains endpoint information
                    elseif ($line -match '^\s+endpoint: (.+)$') {

                        $currentPeer.Endpoint = $matches[1]
                    }
                    # check if line contains latest handshake information
                    elseif ($line -match '^\s+latest handshake: (.+)$') {

                        $currentPeer.LatestHandshake = $matches[1]

                        # determine status based on handshake time
                        if ($matches[1] -match 'Never') {

                            $currentPeer.Status = 'Disconnected'
                        }
                        else {

                            # calculate time since last handshake
                            $handshakeTime = if ($matches[1] -match '(\d+) minutes? ago') {

                                [int]$matches[1]
                            }
                            elseif ($matches[1] -match '(\d+) seconds? ago') {

                                0  # Less than a minute
                            }
                            else {

                                9999  # Some other format, assume old
                            }

                            # if handshake was less than 3 minutes ago, consider connected
                            $currentPeer.Status = if ($handshakeTime -le 3) {

                                'Connected'
                            }
                            else {

                                'Inactive'
                            }
                        }
                    }                    # check if line contains transfer statistics information
                    elseif ($line -match ('^\\s+transfer: (\\d+\\.?\\d* \\w+) received, ' +
                                         '(\\d+\\.?\\d* \\w+) sent$')) {

                        $currentPeer.TransferRx = $matches[1]
                        $currentPeer.TransferTx = $matches[2]
                    }
                    # check if line contains persistent keepalive information
                    elseif ($line -match '^\s+persistent keepalive: (.+)$') {

                        $currentPeer.PersistentKeepalive = $matches[1]
                    }
                }
            }

            # add the last peer if it exists
            if ($null -ne $currentPeer) {

                $peers += $currentPeer
            }

            # get peer names from configuration folders
            $peerFolders = docker exec $ContainerName sh -c "ls -1d /config/peer_*" `
                2>$null

            # process peer folders if command was successful
            if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrEmpty($peerFolders)) {

                # iterate through each peer folder
                foreach ($folder in $peerFolders -split "`n") {

                    # extract peer name from folder path
                    if ($folder -match '/config/peer_(.+)$') {

                        $peerName = $matches[1]

                        # get the public key for this peer
                        $publicKey = docker exec $ContainerName sh -c `
                            "cat /config/peer_$peerName/publickey" 2>$null

                        # process if public key was retrieved successfully
                        if ($LASTEXITCODE -eq 0 -and `
                            -not [string]::IsNullOrEmpty($publicKey)) {

                            # find the peer with this public key and add the name
                            foreach ($peer in $peers) {

                                if ($peer.PublicKey -eq $publicKey.Trim()) {

                                    $peer.Name = $peerName
                                    break
                                }
                            }

                            # if not found in peers list (disconnected), add it
                            if (-not ($peers | `
                                Microsoft.PowerShell.Core\Where-Object { `
                                    $_.PublicKey -eq $publicKey.Trim() })) {

                                $peers += @{
                                    PublicKey = $publicKey.Trim()
                                    Name = $peerName
                                    AllowedIPs = "Unknown"
                                    Endpoint = "Unknown"
                                    LatestHandshake = "Never"
                                    TransferRx = "0 B"
                                    TransferTx = "0 B"
                                    PersistentKeepalive = "Off"
                                    Status = "Disconnected"
                                }
                            }
                        }
                    }
                }
            }

            # convert to custom objects
            $peerObjects = $peers | `
                Microsoft.PowerShell.Core\ForEach-Object {

                [PSCustomObject]@{
                    Name = $_.Name
                    PublicKey = $_.PublicKey
                    AllowedIPs = $_.AllowedIPs
                    Endpoint = $_.Endpoint
                    LatestHandshake = $_.LatestHandshake
                    TransferReceived = $_.TransferRx
                    TransferSent = $_.TransferTx
                    PersistentKeepalive = $_.PersistentKeepalive
                    Status = $_.Status
                }
            }

            # return the results
            return $peerObjects
        }
        catch {

            Microsoft.PowerShell.Utility\Write-Error `
                "Failed to retrieve WireGuard peers: $_"
            throw
        }
    }

    end {
    }
}
################################################################################