Private/AzStackHci.DNS.Helpers.ps1

# ////////////////////////////////////////////////////////////////////////////
# Check DNS to for domain name, and return IP address if found
Function Get-DnsRecord {
    param (
        [Parameter(Mandatory=$true)]    
        [ValidateLength(1, 255)]
        [string]$url,

        [Parameter(Mandatory=$false)]
        [switch]$SkipRfc1918Check
    )

    begin {
        # Write-Debug "Get-DnsRecord: Beginning DNS lookup process"
        # Initialize variables
        [bool]$dnsExists = $False
    }

    process {
        Write-Debug "Checking to see if $($url) returns an IP address from DNS"
        # Remove variables
        Remove-Variable ipAddress -ErrorAction SilentlyContinue

        # Call Resolve-DnsName with exponential backoff: retry up to $script:DNS_MAX_RETRIES times
        # with increasing delays (1s, 2s, 4s + jitter) to avoid flooding DNS on flaky networks.
        For ($i=1; $i -le $script:DNS_MAX_RETRIES; $i++) {
        
            # Remove variables
            Remove-Variable DNSCheckError -ErrorAction SilentlyContinue
            # Initialize variables
            $DNSCheck = @()
            # Check if the domain name exists in DNS
            if(-not($dnsExists)){
                Write-Debug "DNS attempt $i of 3: Checking DNS server for endpoint '$url'"

                try {
            
                    # Check if the domain name exists in DNS using Resolve-DnsName.
                    # //// Use "5>$null" to suppress internal tracing messages such as "DEBUG: 72136".
                    $DNSCheck = Resolve-DnsName -Name $url -Type A -DnsOnly -ErrorAction Stop -ErrorVariable DNSCheckError 5>$null

                # /////////////////////
                # Error handling logic
                # /////////////////////
                } catch [System.Management.Automation.CommandNotFoundException] { # Catch if Resolve-DnsName is not found, not expected
                    Write-HostAzS "Resolve-DnsName is not found, please run this script on a Windows 8/Server 2012 or later machine" -ForegroundColor Red
                    Exit

                } catch { 
                    # Catch DNS Errors
                    # Check if the error message contains 'DNS name does not exist'
                    if($_.Exception.Message.ToString().Contains('DNS name does not exist')) {
                        Write-Debug "DNS Error for '$url' Exception: $($_.Exception.Message)"
                    } else {
                        # All other DNS errors
                        if($i -eq 3){
                            Write-HostAzS "Error: DNS lookup failed for '$url' - Exception Message: $($_.Exception.Message)" -ForegroundColor Red
                        }
                    }
                    # Exponential backoff before next retry: delay = base * 2^(attempt-1) + random jitter
                    if ($i -lt $script:DNS_MAX_RETRIES) {
                        $backoffDelay = $script:DNS_RETRY_BASE_DELAY_SEC * [math]::Pow(2, ($i - 1))
                        $jitter = Get-Random -Minimum 0.0 -Maximum 0.5
                        $totalDelay = [math]::Round($backoffDelay + $jitter, 1)
                        Write-HostAzS "DNS lookup failed for '$url' (attempt $i of $($script:DNS_MAX_RETRIES)), retrying in $($totalDelay)s..." -ForegroundColor Yellow
                        Start-Sleep -Milliseconds (($backoffDelay + $jitter) * 1000)
                    } else {
                        Write-HostAzS "DNS lookup failed for '$url' (attempt $i of $($script:DNS_MAX_RETRIES)), no more retries." -ForegroundColor Red
                    }

                } Finally {
                    # If no DNS errors, set the ipAddress variable to IP address returned from DNS
                    if(-not($DNSCheckError)) {
                        
                        # Check if the DNS name exists, and that $DnsExists is false (IP address not yet found)
                        if($DNSCheck -and (-not($dnsExists))){
                            if(($DNSCheck.IPAddress).count -gt 1){
                                # Use first IP address returned from DNS
                                $ipAddress = ($DNSCheck.IPAddress)[0]
                                Write-Debug "Multiple IP addresses returned from DNS for $url, using first IP from list of addresses: $($DNSCheck.IPAddress)"
                                $dnsExists = $True
                            } else {
                                # Only one IP address returned from DNS
                                $ipAddress = $DNSCheck.IPAddress
                                Write-Debug "Single IP address returned from DNS for $url, $ipAddress"
                                $dnsExists = $True
                            }

                        } elseif((-not($DNSCheck))){
                            # No IP address returned from DNS, but record exists
                            $ipAddress = "No Type A record found in DNS"
                            $dnsExists = $False

                        } else {
                            # Do nothing, DNS already exists
                        }
                    
                    } else {
                        # DNS Error variable exists, set IP address to "DNS Lookup Failed"
                        $ipAddress = "DNS name does not exist"
                        $dnsExists = $False
                    }

                } # End of Finally block

            } else {
                # DNS already exists, skip further checks, but will be on second loop
                Write-Debug "IP address found from DNS on attempt $($i -1), skipping further name resolution attempts"
                Break
            }

        } # End of For loop three attempts

        if($dnsExists){
            Write-Verbose "DNS lookup successful for $url, returned IP Address: $ipAddress"
        } else {
            Write-HostAzS "DNS lookup failed for $url" -ForegroundColor Red
            Write-Verbose  "DNS lookup failed three times for $url - $ipAddress"
        }

        # Test if the IP address is RFC1918 private address
        if(-not($SkipRfc1918Check.IsPresent)){
            # Only test if the SkipRfc1918Check switch is not present
            if($ipAddress -and (-not($ipAddress -in @("No Type A record found in DNS","DNS name does not exist","")))){
                # Check if the IP address is in valid IPv4 format
                if(($IpAddress -match '^(\d{1,3}\.){3}\d{1,3}$')) {
                    # Only test if the IP address is valid
                    Write-Verbose "Testing if returned IP Address '$ipAddress' is an RFC1918 private address"
                    # Check if the IP address is an RFC1918 private address
                    if(Test-IPv4IsRfc1918 -IpAddress $ipAddress){
                        # IP Address is an RFC1918 private address
                        Remove-Variable testUrl -ErrorAction SilentlyContinue
                        # Ensure URL is lowercase for comparison
                        $testUrl = $url.ToLower()
                        # Check if URL matches any patterns in the critical Arc service Private Link endpoints list
                        Remove-Variable isUrlArcServicePrivateLink -ErrorAction SilentlyContinue
                        $isUrlArcServicePrivateLink = $script:PrivateLinkCriticalEndpoints | Where-Object { $testUrl -like $_ }
                        # If URL matches critical Arc service Private Link endpoint
                        if($isUrlArcServicePrivateLink){
                            Write-Debug "URL '$url' matches critical Arc service Private Link endpoint pattern for: '$isUrlArcServicePrivateLink'"
                            # Critical Private Link Endpoint detected
                            Write-HostAzS "IP Address resolved to an RFC1918 private address! Possible use of Azure Arc Private Link!?" -ForegroundColor Red
                            Write-HostAzS "`nCritical Private Link Endpoint detected for Azure Arc service, which is not supported for Azure Local!" -ForegroundColor Red
                            Write-HostAzS "IP Address returned from DNS: '$ipAddress'" -ForegroundColor Red
                            Write-HostAzS "Check for CNAME Alias of endpoint in your DNS zones configuration, IP Address returned from DNS: '$ipAddress'" -ForegroundColor Red
                            Write-HostAzS "Arc services do not support Private Link endpoints, please reconfigure to use public endpoints." -ForegroundColor Red
                            Write-HostAzS "Sleeping for 10 seconds..." -ForegroundColor Red
                            Start-Sleep -Seconds 10

                        # Else not a critical Arc service Private Link endpoint
                        } else {
                            # Not a Critical Arc Service Private Link Endpoint
                            Write-HostAzS "IP Address resolved to an RFC1918 private address! Possible use of Private Link?" -ForegroundColor Yellow
                            Write-HostAzS "Check for CNAME Alias of endpoint in your DNS zones configuration, IP Address returned from DNS: '$ipAddress'" -ForegroundColor Yellow
                        }

                        $script:PrivateLinkDetected = $true
                        $script:PrivateLinkDetectedArray += $url
                    } else {
                        # Do nothing
                        Write-Verbose "Returned IP Address is NOT an RFC1918 private address."
                    }
                } else {
                    Write-Verbose "Returned IP Address '$ipAddress' is not in valid IPv4 format, skipping RFC1918 private address test"
                }
            } else {
                Write-Debug "Not testing if returned IP Address '$ipAddress' is an RFC1918 private address, as it is not a valid IP address"
            }

        } else {
            Write-Debug "SkipRfc1918Check switch present, skipping RFC1918 private address test"
        }

    } # End of process block

    end {
        # Write-Debug "Get-DnsRecord: DNS lookup process completed"

        # Return True/False and IP Address output as a PSObject.
        $DNSReturnVariable = New-Object PsObject -Property @{
            # True/False
            DNSExists = $dnsExists
            # IP Address, or "DNS Lookup Failed"
            IPAddress = $ipAddress
        }
        return $DNSReturnVariable

    } # End of end block

} # End of Get-DnsRecord function


# ////////////////////////////////////////////////////////////////////////////
# Function to test if an IP address is in the RFC 1918 private IP range.
# Returns $true if the IP address is in the private range, otherwise returns $false.
Function Test-IPv4IsRfc1918 {
    param (
        [Parameter(Mandatory = $true)]
        [ipaddress]$IpAddress
    )

    begin {
        # Write-Debug "Test-IPv4IsRfc1918: Beginning RFC1918 private IP address check for '$IpAddress'"
    }

    process {

        $IpAddressString = $IpAddress.ToString()
        # Validated IP is correct IPv4 format
        if (-not ($IpAddressString -match '^(\d{1,3}\.){3}\d{1,3}$')) {
            Write-Error "Invalid IPv4 address format."
            Return $false
        }

        $octets = $IpAddressString.Split('.')
        if ($octets.Count -ne 4) { Return $false }

        # Convert octets to integers
        $o1 = [int]$octets[0]
        $o2 = [int]$octets[1]

        # 10.0.0.0/8 (10.0.0.0 - 10.255.255.255)
        if ($o1 -eq 10) { Return $true }
        # 172.16.0.0/12 (172.16.0.0 - 172.31.255.255)
        if ($o1 -eq 172 -and $o2 -ge 16 -and $o2 -le 31) { Return $true }
        # 192.168.0.0/16 (192.168.0.0 - 192.168.255.255)
        if ($o1 -eq 192 -and $o2 -eq 168) { Return $true }

        # Not in RFC 1918 private IP range
        Return $false
    } # End of process block

    end {
        # Write-Debug "Test-IPv4IsRfc1918: RFC1918 private IP address check completed"
    }
} # End Function Test-IPv4IsRfc1918