src/Resolve-DnsNameCrossPlatform.psm1

Function Resolve-DnsNameCrossPlatform
{
    [CmdletBinding()]
    [Alias('Resolve-DnsName')]
    Param(
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias('InputObject', 'DomainName')]
        [String] $Name,

        [Switch] $DnsOnly,

        [Switch] $DnssecCd,

        [Switch] $DnssecOk,

        [Switch] $NoIdn,

        [Switch] $NoRecursion,

        [String[]] $Server,

        [Switch] $TcpOnly,

        [Parameter(Position=1)]
        [ValidateSet("UNKNOWN", "A_AAAA", "A", "AAAA", "CNAME", "MX", "NS", "SOA", "SRV", "TXT")]
        [String] $Type = "A_AAAA"
    )

    Write-Verbose "Performing a DNS lookup for $Name ($Type)."

    # Check and see if the Resolve-DnsName cmdlet is available. If so, just
    # pass all of our parameters to it.
    #
    # It and the DnsClient module has been available since PowerShell 3.0,
    # but only when running on Windows 8/Windows Server 2012 or newer.
    If (Get-Command -Module 'DnsClient' -Name 'Resolve-DnsName' -ErrorAction SilentlyContinue)
    {
        $Parameters = @{
            "Debug" = $DebugPreference;
            "DnssecCd" = $DnssecCd;
            "DnssecOk" = $DnssecOk;
            "ErrorAction" = $ErrorActionPreference;
            "Name" = $Name;
            "NoIdn" = $NoIdn;
            "NoRecursion" = $NoRecursion;
            "Server" = $Server;
            "TcpOnly" = $TcpOnly;
            "Type" = $Type;
            "Verbose" = $VerbosePreference
        }
        Return (DnsClient\Resolve-DnsName @Parameters)
    }

    # If Resolve-DnsName is not available, we need to use the system's copy of
    # dig, and try to emulate the style of output that Resolve-DnsName creates.
    Else
    {
        # Build our dig command by translating Resolve-DnsName parameters
        # into dig options.
        $CommandLine = "/usr/bin/dig -t $Type $Name +short"
        If ($DnssecCd)
        {
            $CommandLine += " +cdflag"
        }
        If ($DnssecOk)
        {
            $CommandLine += " +dnssec"
        }
        If ($NoIdn)
        {
            $CommandLine += " +noidnout"
        }
        If ($NoRecursion)
        {
            $CommandLine += " +norecurse"
        }
        If ($null -ne $Server)
        {
            If ($Server.Count -gt 1)
            {
                Write-Warning "Multiple DNS servers are not yet supported. Only the first one will be tried."
                $CommandLine += " @$($Server[0])"
            }
            Else
            {
                $CommandLine += " @$Server"
            }
        }
        If ($TcpOnly)
        {
            $CommandLine += " +tcp"
        }

        # Resolve-DnsName supports the "meta-type" A_AAAA, which returns both
        # A records and AAAA records. dig does not support that natively, so
        # we will need to call it twice.
        If ($Type -eq "A_AAAA")
        {
            # Explicitly cast it to String[] so we add to the array properly.
            [String[]] $dnsLookup  = @()
            [String[]] $dnsLookup  = Invoke-Expression ($CommandLine -Replace $Type,"A")
            [String[]] $dnsLookup += Invoke-Expression ($CommandLine -Replace $Type,"AAAA")
        }
        # If the user specified a real RR type, then we'll just call dig
        # normally.
        Else
        {
            $dnsLookup = Invoke-Expression $CommandLine
        }

        If (-Not $dnsLookup)
        {
            Write-Debug "DNS record not found."
            Return $null
        }

        # Our result.
        $ResourceRecords = @()

        # To best mimic Resolve-DnsName, return results as custom objects with
        # the same properties. This makes things as similar as possible.
        Switch ($Type)
        {
            "A"
            {
                $dnsLookup | ForEach-Object `
                {
                    Write-Debug "$Name has the IPv4 address $_."
                    $DnsRecord = [PSCustomObject]@{
                        "Name" = $Name;
                        "Type" = $Type;
                        "Section" = "Answer";
                        "IP4Address" = $_
                    }

                    # On Windows, Resolve-DnsName will return object(s) of type
                    # Microsoft.DnsClient.Commands.DnsRecord_A. Try to add as
                    # many matching properties as we can.
                    $DnsRecord `
                        | Add-Member -MemberType AliasProperty -Name "Address" -Value "IP4Address" -PassThru `
                        | Add-Member -MemberType AliasProperty -Name "IPAddress" -Value "IP4Address" -PassThru `
                        | Add-Member -MemberType AliasProperty -Name "QueryType" -Value "Type"

                    # Append it to our results.
                    $ResourceRecords += $DnsRecord
                }
            }

            "AAAA"
            {
                $dnsLookup | ForEach-Object `
                {
                    Write-Debug "$Name has the IPv6 address $_."
                    $DnsRecord = [PSCustomObject]@{
                        "Name" = $Name;
                        "Type" = $Type;
                        "Section" = "Answer";
                        "IP6Address" = $_
                    }

                    # On Windows, Resolve-DnsName will return object(s) of type
                    # Microsoft.DnsClient.Commands.DnsRecord_AAAA. Try to add
                    # as many matching properties as we can.
                    $DnsRecord `
                        | Add-Member -MemberType AliasProperty -Name "Address" -Value "IP6Address" -PassThru `
                        | Add-Member -MemberType AliasProperty -Name "IPAddress" -Value "IP6Address" -PassThru `
                        | Add-Member -MemberType AliasProperty -Name "QueryType" -Value "Type"

                    # Append it to our results.
                    $ResourceRecords += $DnsRecord
                }
            }

            {$_ -in @("A_AAAA", "UNKNOWN")}
            {
                $dnsLookup | ForEach-Object `
                {
                    $DnsRecord = [PSCustomObject]@{
                        "Name" = $Name;
                        "Type" = "A";
                        "Section" = "Answer";
                    }
                    $DnsRecord | Add-Member -MemberType AliasProperty -Name "QueryType" -Value "Type"

                    If ($_ -Match ':')
                    {
                        Write-Debug "$Name has the IPv6 address $_."
                        $DnsRecord.Type = "AAAA"
                        $DnsRecord `
                            | Add-Member -MemberType NoteProperty -Name "IP6Address" -Value $_ -PassThru `
                            | Add-Member -MemberType AliasProperty -Name "IPAddress" -Value "IP6Address" -PassThru `
                            | Add-Member -MemberType AliasProperty -Name "Address" -Value "IP6Address"
                    }
                    Else
                    {
                        Write-Debug "$Name has the IPv4 address $_."
                        $DnsRecord `
                            | Add-Member -MemberType NoteProperty -Name "IP4Address" -Value $_ -PassThru `
                            | Add-Member -MemberType AliasProperty -Name "IPAddress" -Value "IP4Address" -PassThru `
                            | Add-Member -MemberType AliasProperty -Name "Address" -Value "IP4Address"
                    }

                    # Append it to our results.
                    $ResourceRecords += $DnsRecord
                }
            }

            "CNAME"
            {
                $dnsLookup | ForEach-Object `
                {
                    Write-Debug "$Name has the CNAME $_"
                    $DnsRecord = [PSCustomObject]@{
                        "Name" = $Name;
                        "Type" = $Type;
                        "Section" = "Answer";
                        "NameHost" = $_ -Replace "\.$"  # to emulate Resolve-DnsName
                    }
                    $DnsRecord | Add-Member -MemberType AliasProperty -Name "Server" -Value "NameHost"

                    # Append it to our results.
                    $ResourceRecords += $DnsRecord
                }
            }

            "MX"
            {
                $dnsLookup | ForEach-Object `
                {
                    $splits = $_ -Split ' '
                    Write-Debug "$Name has a mail exchanger $($splits[1]) at priority $($splits[0])."
                    $DnsRecord = [PSCustomObject]@{
                        "Name" = $Name;
                        "Type" = $Type;
                        "Section" = "Answer";
                        "Preference" = $splits[0]
                        "NameExchange" = $splits[1] -Replace "\.$"  # to emulate Resolve-DnsName
                    }

                    $DnsRecord | Add-Member -MemberType AliasProperty -Name "Exchange" -Value "NameExchange"

                    # Append it to our results.
                    $ResourceRecords += $DnsRecord
                }
            }

            "NS"
            {
                $dnsLookup | ForEach-Object `
                {
                    Write-Debug "$Name has a nameserver $_"
                    $DnsRecord = [PSCustomObject]@{
                        "Name" = $Name;
                        "Type" = $Type;
                        "Section" = "Answer";
                        "NameHost" = $_ -Replace "\.$"  # to emulate Resolve-DnsName
                    }
                    $DnsRecord | Add-Member -MemberType AliasProperty -Name "Server" -Value "NameHost"
                    # Append it to our results.
                    $ResourceRecords += $DnsRecord
                }
            }

            "SOA"
            {
                $dnsLookup | ForEach-Object `
                {
                    $splits = $_ -Split ' '
                    Write-Debug "$Name has an SOA record: MNAME $($splits[0]), RNAME $($splits[1]), serial $($splits[2]), refresh $($splits[3]), retry $($splits[4]), expire $($splits[5]), negative cache TTL $($splits[6])."
                    $DnsRecord = [PSCustomObject]@{
                        "Name"                   = $Name;
                        "Type"                   = $Type;
                        "Section"                = "Authority";
                        "PrimaryServer"          = $splits[0] -Replace "\.$"
                        "NameAdministrator"      = $splits[1] -Replace "\.$"
                        "SerialNumber"           = [UInt32]$splits[2]
                        "TimeToZoneRefresh"      = [UInt32]$splits[3]
                        "TimeToZoneFailureRetry" = [UInt32]$splits[4]
                        "TimeToExpiration"       = [UInt32]$splits[5]
                        "DefaultTTL"             = [UInt32]$splits[6]
                    }

                    $DnsRecord | Add-Member -MemberType AliasProperty -Name "Administrator" -Value "NameAdministrator"

                    # Append it to our results.
                    $ResourceRecords += $DnsRecord
                }
            }

            "SRV"
            {
                $dnsLookup | ForEach-Object `
                {
                    $splits = $_ -Split ' '
                    Write-Debug "$Name is a service on $($splits[3]):$($splits[2]), priority $($splits[0]), weight $($splits[1])."
                    $DnsRecord = [PSCustomObject]@{
                        "Name"       = $Name;
                        "Type"       = $Type;
                        "Section"    = "Answer";
                        "Priority"   = [UInt16]($_ -Split ' ')[0]
                        "Weight"     = [UInt16]($_ -Split ' ')[1]
                        "Port"       = [UInt16]($_ -Split ' ')[2]
                        "NameTarget" = [String]($_ -Split ' ')[3] -Replace "\.$"  # to emulate Resolve-DnsName
                    }

                    $DnsRecord | Add-Member -MemberType AliasProperty -Name "Target" -Value "NameTarget"
                    # Append it to our results.
                    $ResourceRecords += $DnsRecord
                }
            }

            "TXT"
            {
                $dnsLookup | ForEach-Object `
                {
                    Write-Debug "$Name has a text record: $_"
                    $DnsRecord = [PSCustomObject]@{
                        "Name"    = $Name;
                        "Type"    = $Type;
                        "Section" = "Answer";
                        "Strings" = $_
                    }

                    $DnsRecord | Add-Member -MemberType AliasProperty -Name "Text" -Value "Strings"
                    # Append it to our results.
                    $ResourceRecords += $DnsRecord
                }
            }

            default
            {
                Write-Error "$Type records are not yet implemented."
            }
        }
    }

    # We're sorting by preference or priority/weight, to make our MX and SRV
    # records appear in "order."
    If ($Type -eq "SRV")
    {
        Return $ResourceRecords | Sort-Object -Property Priority,Weight
    }
    ElseIf ($Type -eq "MX")
    {
        Return $ResourceRecords | Sort-Object -Property Preference
    }
    Else
    {
        Return $ResourceRecords
    }
}