Public/Get-PSUPublicIP.ps1

function Get-PSUPublicIP {
    <#
    .SYNOPSIS
        Retrieves the public IP address of the current machine.
 
    .DESCRIPTION
        Attempts to determine public IP using multiple methods with caching:
        1. DNS lookup via OpenDNS (fastest)
        2. HTTP requests to several endpoints in turn, taking the first valid answer
        Results are cached for 5 minutes to improve performance.
 
    .PARAMETER TimeoutSec
        Timeout in seconds for each HTTP request. Default is 3 seconds. The HTTP fallback
        tries endpoints one at a time, so a total outage of all four can take up to four
        times this value before the command reports failure.
 
    .PARAMETER NoCache
        Skip cache and force fresh lookup.
 
    .PARAMETER CacheMinutes
        How long to cache the IP address. Default is 5 minutes.
 
    .EXAMPLE
        Get-PSUPublicIP
        # Returns: 203.0.113.42
 
    .EXAMPLE
        Get-PSUPublicIP -NoCache -TimeoutSec 5
        # Forces fresh lookup with 5 second timeout
 
    .OUTPUTS
        [System.String]
 
    .NOTES
        Author: Lakshmanachari Panuganti
        Date: 11 December 2025
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter()]
        [ValidateRange(1, 30)]
        [int]$TimeoutSec = 3,

        [Parameter()]
        [switch]$NoCache,

        [Parameter()]
        [ValidateRange(1, 60)]
        [int]$CacheMinutes = 5
    )

    # Return cached IP if available and not expired
    if (-not $NoCache -and
        $script:CachedPublicIP -and
        $script:CachedIPExpiry -and
        (Get-Date) -lt $script:CachedIPExpiry) {
        Write-Verbose "Using cached public IP: $script:CachedPublicIP (expires: $script:CachedIPExpiry)"
        return $script:CachedPublicIP
    }

    try {
        Write-Verbose "Attempting DNS lookup via OpenDNS..."
        $dnsResult = Resolve-DnsName -Name myip.opendns.com -Server resolver1.opendns.com -ErrorAction Stop -DnsOnly
        $ip = $dnsResult.Where({ $_.Type -eq "A" }, 'First').IPAddress

        if ($ip -match '^\d{1,3}(\.\d{1,3}){3}$') {
            $script:CachedPublicIP = $ip
            $script:CachedIPExpiry = (Get-Date).AddMinutes($CacheMinutes)
            Write-Verbose "Public IP retrieved via DNS: $ip (cached until $script:CachedIPExpiry)"
            return $ip
        }
    }
    catch {
        Write-Verbose "DNS lookup failed: $($_.Exception.Message)"
    }

    # 2️⃣ Fallback: try HTTP endpoints in order and take the first valid answer.
    # Sequential on purpose. The parallel version used Start-ThreadJob, which Windows
    # PowerShell 5.1 does not ship, so on 5.1 this fallback raised CommandNotFoundException
    # instead of returning an address. Declaring ThreadJob in the manifest would have pushed
    # that dependency onto every module that requires Core, for a path that only runs when
    # the DNS lookup above has already failed.
    Write-Verbose "DNS failed, trying HTTP endpoints..."

    $endpoints = @(
        'https://checkip.amazonaws.com'
        'https://api.ipify.org'
        'https://icanhazip.com'
        'https://ifconfig.me/ip'
    )

    $ip = $null
    foreach ($endpoint in $endpoints) {
        try {
            Write-Verbose "Querying $endpoint ..."
            $response = Invoke-RestMethod -Uri $endpoint -TimeoutSec $TimeoutSec -ErrorAction Stop
            $candidate = ([string]$response).Trim()

            if ($candidate -match '^\d{1,3}(\.\d{1,3}){3}$') {
                $ip = $candidate
                Write-Verbose "Received valid IP from ${endpoint}: $ip"
                break
            }

            Write-Verbose "Endpoint $endpoint returned no usable address."
        }
        catch {
            Write-Verbose "Endpoint $endpoint failed: $($_.Exception.Message)"
        }
    }

    if ($ip) {
        $script:CachedPublicIP = $ip
        $script:CachedIPExpiry = (Get-Date).AddMinutes($CacheMinutes)
        Write-Verbose "Public IP retrieved via HTTP: $ip (cached until $script:CachedIPExpiry)"
        return $ip
    }

    # All methods failed
    $errorMsg = "Unable to determine public IP address after trying DNS and $($endpoints.Count) HTTP endpoints"
    Write-Error $errorMsg
    throw $errorMsg
}