DOBON.NET プログラミング道: .NET Framework, VB.NET, C#, Visual Basic, Visual Studio, インストーラ, ...

DNSサーバーと直接やり取りして、A、PTR、MX等のレコードを検索する

.NET Frameworkでは、「ホスト名からIPアドレス、IPアドレスからホスト名を取得する」で紹介しているように、DNSサーバーに問い合わせをして、正引き(ホスト名からIPアドレスの取得)と逆引き(IPアドレスからホスト名の取得)を行う方法は用意されています。しかし、MXなどのレコードを検索したり、DNSサーバーを指定して検索する方法は用意されていません。

そこでここでは、Socket(サンプルでは、UdpClientクラス)を使用して、DNSサーバーと直接やり取りをする方法を紹介します。

ただしここではサンプルコードを紹介するだけで、DNSに関する説明はしません。その分、サンプルコードには多くのコメントを付けました。

DNSの仕様については、RFC1034RFC1035RFC1885RFC2915などに書かれています。また、このサンプルを作成するにあたり、「DNS クライアントを作ってみよう (1)」や「How to get mx records for a dns name with System.Net.DNS? - Stack Overflow」を参考にしました。

もしDNSサーバーと直接やり取りをする方法には興味がなく、単にDNSクライアントの機能が必要なだけであれば、サードパーティー製のライブラリを使用した方が安全でしょう。例えば、フリーのライブラリには以下のようなものがあります。

補足:上記以外の方法としては、DnsQuery functionを使用する方法や、nslookup.exeを使用するという方法もあります。なお、DnsQueryを使用する時にDNSサーバーを指定する方法は、「How does one specify a specific DNS server to query using DnsQuery? - Stack Overflow」にあります。

それでは、サンプルコードを紹介します。このサンプルはコンソールアプリケーションで、実行すると「google.com」のAレコードを検索します。Aレコード(ホストのIPv4アドレス、正引き)の他に、NS(ホストのDNSサーバ名)、CNAME(ホスト名のエイリアス)、PTR(IPアドレスからホスト名、逆引き)、MX(ホストのメールサーバー名)、AAAA(ホストのIPv6アドレス)に対応しています。DNSサーバーは、「Google Public DNS」(8.8.8.8)を使用しています。

VB.NET
コードを隠すコードを選択
Imports System.Net
Imports System.Net.Sockets
Imports System.Text
Imports System.Collections.Generic

Class Program
    'エントリポイント
    Public Shared Sub Main(args As String())
        '設定
        '検索するホスト名(FQDN)またはIPアドレス
        Dim hostOrAddress As String = "google.com"
        '検索するレコード
        'A=1 NS=2 CNAME=5 PTR=12 MX=15 AAAA=28
        Dim recordType As Byte = 1
        'DNSサーバー
        Dim dnsServer As String = "8.8.8.8"
        'DNSサーバーのポート番号
        Dim dnsPort As Integer = 53

        'DNSサーバーに問い合わせをする
        Dim answers As String() = _
            LookupDns(hostOrAddress, recordType, dnsServer, dnsPort)

        '結果を表示する
        Console.WriteLine("結果:")
        For Each s As String In answers
            Console.WriteLine(s)
        Next

        Console.ReadLine()
    End Sub

    ''' <summary>
    ''' 指定したDNSサーバーで、指定したレコードの検索を行う。
    ''' </summary>
    ''' <param name="hostOrAddress">検索するホスト名またはIPアドレス。</param>
    ''' <param name="qType">レコード種別。
    ''' A=1 NS=2 CNAME=5 PTR=12 MX=15 AAAA=28 のみに対応。</param>
    ''' <param name="dnsServer">DNSサーバーのIPアドレスまたはホスト名。</param>
    ''' <param name="dnsPort">DNSサーバーのポート番号。</param>
    ''' <returns>回答結果。ホスト名かIPアドレス。</returns>
    Public Shared Function LookupDns(hostOrAddress As String, qType As Byte, _
                                     dnsServer As String, dnsPort As Integer) _
                                 As String()
        '送信するデータを作成する
        Dim req As Byte() = CreateRequestMessage(hostOrAddress, qType)
        Console.WriteLine(BitConverter.ToString(req))

        Dim res As Byte() = Nothing
        Using udpc As New UdpClient(dnsServer, dnsPort)
            'UDPでリクエストメッセージを送信する
            udpc.Send(req, req.Length)

            'レスポンスを受信する
            Dim ep As IPEndPoint = Nothing
            res = udpc.Receive(ep)
        End Using
        Console.WriteLine(BitConverter.ToString(res))

        '受信したデータから回答を取り出す
        Return GetAnswersFromResponse(res, req.Length)
    End Function

    'リクエストメッセージを作成する
    Private Shared Function CreateRequestMessage( _
            qName As String, qType As Byte) As Byte()
        Using ms As New System.IO.MemoryStream()
            'Header section
            'ID(ここでは、「00 00」に固定)
            ms.WriteByte(0)
            ms.WriteByte(0)
            'QR、Opcode、AA、TC、RD(ここでは、RDのみ1としている)
            ms.WriteByte(1)
            'RA、Z、RCODE
            ms.WriteByte(0)
            'QDCOUNT(質問数)(質問数は、1つ)
            ms.WriteByte(0)
            ms.WriteByte(1)
            'ANCOUNT(回答数)
            ms.WriteByte(0)
            ms.WriteByte(0)
            'NSCOUNT
            ms.WriteByte(0)
            ms.WriteByte(0)
            'ADCOUNT
            ms.WriteByte(0)
            ms.WriteByte(0)

            'Question section
            'QNAME を作成
            If qType = 12 Then
                'PTRの時は、arpaアドレスに変換
                qName = ConvertToArpaAddress(qName)
            End If
            'ホスト名を"."で分割
            Dim buf1 As String() = qName.Split("."c)
            For Each s As String In buf1
                '文字列をバイト配列に変換
                Dim bs1 As Byte() = Encoding.ASCII.GetBytes(s)
                '長さと文字列データを書き込む
                ms.WriteByte(CByte(bs1.Length))
                ms.Write(bs1, 0, bs1.Length)
            Next
            ms.WriteByte(0)
            'QTYPE
            ms.WriteByte(0)
            ms.WriteByte(qType)
            'QCLASS(ここでは、IN(Internet)の1に固定)
            ms.WriteByte(0)
            ms.WriteByte(1)

            Return ms.ToArray()
        End Using
    End Function

    'IPアドレスを、逆引きのためのarpaアドレスに変換する
    Private Shared Function ConvertToArpaAddress(ipString As String) As String
        Dim sb As New StringBuilder()
        'IPAddressオブジェクトを作成
        Dim ip As IPAddress = IPAddress.Parse(ipString)
        If ip.AddressFamily = AddressFamily.InterNetwork Then
            'IPv4の時
            'バイト配列に変換
            Dim bs As Byte() = ip.GetAddressBytes()
            '逆順に並び替えて、in-addr.arpaを付ける
            Dim i As Integer = bs.Length - 1
            While 0 <= i
                sb.Append(bs(i)).Append("."c)
                i -= 1
            End While
            sb.Append("in-addr.arpa")
        ElseIf ip.AddressFamily = AddressFamily.InterNetworkV6 Then
            'IPv6の時
            Dim bs As Byte() = ip.GetAddressBytes()
            '逆順に並び替えて、ip6.arpaを付ける
            Dim i As Integer = bs.Length - 1
            While 0 <= i
                sb.AppendFormat("{0:x}", bs(i) And &HF).Append("."c) _
                    .AppendFormat("{0:x}", (bs(i) >> 4) And &HF).Append("."c)
                i -= 1
            End While
            sb.Append("ip6.arpa")
        End If

        Return sb.ToString()

        'または、IPv4の場合だけならば、以下のようにもできる
        'Return System.Text.RegularExpressions.Regex.Replace( _
        '    ipString, "(\d+)\.(\d+)\.(\d+)\.(\d+)", _
        '    "$4.$3.$2.$1.in-addr.arpa")
    End Function

    'レスポンスメッセージから回答を取得する
    Private Shared Function GetAnswersFromResponse( _
            data As Byte(), startPos As Integer) As String()
        'データの読み込み位置
        Dim pos As Integer = 0

        'RCODE (0=No error condition)
        Dim resCode As Integer = data(3) And &HF
        Console.WriteLine("Response Code:{0}", resCode)
        'ANCOUNT(回答数)
        pos = 6
        Dim anCount As Integer = ConvertToShort(data, pos)
        Console.WriteLine("Answers Count:{0}", anCount)
        'NSCOUNT
        Dim nsCount As Integer = ConvertToShort(data, pos)
        'ARCOUNT
        Dim arCount As Integer = ConvertToShort(data, pos)
        'Questionを飛ばす
        pos = startPos

        Dim list As New List(Of String)()
        '回答数だけ繰り返す
        For i As Integer = 0 To anCount - 1
            'ドメイン名を取り出す
            Dim domainName As String = GetDomainName(data, pos)
            Console.WriteLine("Domain:{0}", domainName)
            'TYPE
            Dim ppType As Short = ConvertToShort(data, pos)
            'CLASS
            Dim ppClass As Short = ConvertToShort(data, pos)
            'TTL
            Dim ttl As Integer = ConvertToInt(data, pos)
            Console.WriteLine("TTL:{0}", ttl)
            'RDLENGTH
            Dim rdLength As Short = ConvertToShort(data, pos)

            Dim str As String = String.Empty
            Select Case ppType
                Case 1, 28
                    'A
                    'AAAA
                    'IPアドレスを抜き出す
                    str = GetIPAddress(data, rdLength, pos)
                    Exit Select
                Case 2, 5, 12
                    'NS
                    'CNAME
                    'PTR
                    'ホスト名を抜き出す
                    str = GetDomainName(data, pos)
                    Exit Select
                Case 15
                    'MX
                    'Preference(優先度)
                    Dim preference As Short = ConvertToShort(data, pos)
                    Console.WriteLine("Preference:{0}", preference)
                    'ホスト名を抜き出す
                    str = GetDomainName(data, pos)
                    Exit Select
                Case Else
                    str = String.Format("(対応していないType({0})の回答)", _
                                        ppType)
                    pos += rdLength
                    Exit Select
            End Select
            list.Add(str)
            Console.WriteLine(str)
        Next
        'Answerのみを解析し、AuthorityとAdditionalは無視

        Return list.ToArray()
    End Function

    'データの指定位置からDomain名を取得する
    Private Shared Function GetDomainName( _
            data As Byte(), ByRef pos As Integer) As String
        Dim sb As New StringBuilder()

        While pos < data.Length
            'labelの長さを取得
            Dim len As Integer = data(pos)
            pos += 1
            '長さが0の時、終了
            If len = 0 Then
                Exit While
            End If
            If (len And &HC0) = &HC0 Then
                'Message compressionの時
                '新たな位置を取得
                Dim newpos As Integer = _
                    ConvertToShort(CByte(len And &H3F), data(pos))
                pos += 1
                '新たな位置から取得したDomainを追加
                If sb.Length > 0 Then
                    sb.Append("."c)
                End If
                sb.Append(GetDomainName(data, newpos))
                Exit While
            End If

            'byteを文字列に変換して、labelを取得し、追加する
            If sb.Length > 0 Then
                sb.Append("."c)
            End If
            sb.Append(Encoding.ASCII.GetString(data, pos, len))
            pos += len
        End While

        Return sb.ToString()
    End Function

    'データの指定位置からIPアドレスを取得する
    Private Shared Function GetIPAddress( _
            data As Byte(), len As Integer, ByRef pos As Integer) As String
        Dim bs As Byte() = New Byte(len - 1) {}
        Array.Copy(data, pos, bs, 0, len)
        pos += len
        Dim ip As New IPAddress(bs)
        Return ip.ToString()

        'または、IPv4の時は、
        'pos += 4
        'Return String.Format("{0}.{1}.{2}.{3}", _
        '    data(pos - 4), data(pos - 3), data(pos - 2), data(pos - 1))
    End Function

    '2つのバイトからInt16を作成
    Private Shared Function ConvertToShort(b1 As Byte, b2 As Byte) As Short
        Dim i As Integer = 0
        Return ConvertToShort(New Byte() {b1, b2}, i)
    End Function
    Private Shared Function ConvertToShort( _
            data As Byte(), ByRef pos As Integer) As Short
        Dim r As Short = _
            IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, pos))
        pos += 2
        Return r

        'または、
        'pos += 2
        'Return ConvertToShort(data(pos - 2), data(pos - 1))
    End Function

    '4つのバイトからInt32を作成
    Private Shared Function ConvertToInt( _
            data As Byte(), ByRef pos As Integer) As Integer
        Dim r As Integer = _
            IPAddress.NetworkToHostOrder(BitConverter.ToInt32(data, pos))
        pos += 4
        Return r
    End Function
End Class
C#
コードを隠すコードを選択
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Collections.Generic;

class Program
{
    //エントリポイント
    public static void Main(string[] args)
    {
        //設定
        //検索するホスト名(FQDN)またはIPアドレス
        string hostOrAddress = "google.com";
        //検索するレコード
        //A=1 NS=2 CNAME=5 PTR=12 MX=15 AAAA=28
        byte recordType = 1;
        //DNSサーバー
        string dnsServer = "8.8.8.8";
        //DNSサーバーのポート番号
        int dnsPort = 53;

        //DNSサーバーに問い合わせをする
        string[] answers =
            LookupDns(hostOrAddress, recordType, dnsServer, dnsPort);

        //結果を表示する
        Console.WriteLine("結果:");
        foreach (string s in answers)
        {
            Console.WriteLine(s);
        }

        Console.ReadLine();
    }

    /// <summary>
    /// 指定したDNSサーバーで、指定したレコードの検索を行う。
    /// </summary>
    /// <param name="hostOrAddress">検索するホスト名またはIPアドレス。</param>
    /// <param name="qType">レコード種別。
    /// A=1 NS=2 CNAME=5 PTR=12 MX=15 AAAA=28 のみに対応。</param>
    /// <param name="dnsServer">DNSサーバーのIPアドレスまたはホスト名。</param>
    /// <param name="dnsPort">DNSサーバーのポート番号。</param>
    /// <returns>回答結果。ホスト名かIPアドレス。</returns>
    public static string[] LookupDns(string hostOrAddress, byte qType,
        string dnsServer, int dnsPort)
    {
        //送信するデータを作成する
        byte[] req = CreateRequestMessage(hostOrAddress, qType);
        Console.WriteLine(BitConverter.ToString(req));

        byte[] res = null;
        using (UdpClient udpc = new UdpClient(dnsServer, dnsPort))
        {
            //UDPでリクエストメッセージを送信する
            udpc.Send(req, req.Length);

            //レスポンスを受信する
            IPEndPoint ep = null;
            res = udpc.Receive(ref ep);
        }
        Console.WriteLine(BitConverter.ToString(res));

        //受信したデータから回答を取り出す
        return GetAnswersFromResponse(res, req.Length);
    }

    //リクエストメッセージを作成する
    private static byte[] CreateRequestMessage(string qName, byte qType)
    {
        using (System.IO.MemoryStream ms = new System.IO.MemoryStream())
        {
            //Header section
            //ID(ここでは、「00 00」に固定)
            ms.WriteByte(0);
            ms.WriteByte(0);
            //QR、Opcode、AA、TC、RD(ここでは、RDのみ1としている)
            ms.WriteByte(1);
            //RA、Z、RCODE
            ms.WriteByte(0);
            //QDCOUNT(質問数)(質問数は、1つ)
            ms.WriteByte(0);
            ms.WriteByte(1);
            //ANCOUNT(回答数)
            ms.WriteByte(0);
            ms.WriteByte(0);
            //NSCOUNT
            ms.WriteByte(0);
            ms.WriteByte(0);
            //ADCOUNT
            ms.WriteByte(0);
            ms.WriteByte(0);

            //Question section
            //QNAME を作成
            if (qType == 12)
            {
                //PTRの時は、arpaアドレスに変換
                qName = ConvertToArpaAddress(qName);
            }
            //ホスト名を"."で分割
            string[] buf1 = qName.Split('.');
            foreach (string s in buf1)
            {
                //文字列をバイト配列に変換
                byte[] bs1 = Encoding.ASCII.GetBytes(s);
                //長さと文字列データを書き込む
                ms.WriteByte((byte)bs1.Length);
                ms.Write(bs1, 0, bs1.Length);
            }
            ms.WriteByte(0);
            //QTYPE
            ms.WriteByte(0);
            ms.WriteByte(qType);
            //QCLASS(ここでは、IN(Internet)の1に固定)
            ms.WriteByte(0);
            ms.WriteByte(1);

            return ms.ToArray();
        }
    }

    //IPアドレスを、逆引きのためのarpaアドレスに変換する
    private static string ConvertToArpaAddress(string ipString)
    {
        StringBuilder sb = new StringBuilder();
        //IPAddressオブジェクトを作成
        IPAddress ip = IPAddress.Parse(ipString);
        if (ip.AddressFamily == AddressFamily.InterNetwork)
        {
            //IPv4の時
            //バイト配列に変換
            byte[] bs = ip.GetAddressBytes();
            //逆順に並び替えて、in-addr.arpaを付ける
            for (int i = bs.Length - 1; 0 <= i; i--)
            {
                sb.Append(bs[i]).Append('.');
            }
            sb.Append("in-addr.arpa");
        }
        else if (ip.AddressFamily == AddressFamily.InterNetworkV6)
        {
            //IPv6の時
            byte[] bs = ip.GetAddressBytes();
            //逆順に並び替えて、ip6.arpaを付ける
            for (int i = bs.Length - 1; 0 <= i; i--)
            {
                sb.AppendFormat("{0:x}", bs[i] & 0xF).Append('.').
                    AppendFormat("{0:x}", (bs[i] >> 4) & 0xF).Append('.');
            }
            sb.Append("ip6.arpa");
        }

        return sb.ToString();

        //または、IPv4の場合だけならば、以下のようにもできる
        //return System.Text.RegularExpressions.Regex.Replace(
        //    ipString, @"(\d+)\.(\d+)\.(\d+)\.(\d+)",
        //    "$4.$3.$2.$1.in-addr.arpa");
    }

    //レスポンスメッセージから回答を取得する
    private static string[] GetAnswersFromResponse(byte[] data, int startPos)
    {
        //データの読み込み位置
        int pos = 0;

        //RCODE (0=No error condition)
        int resCode = data[3] & 0xF;
        Console.WriteLine("Response Code:{0}", resCode);
        //ANCOUNT(回答数)
        pos = 6;
        int anCount = ConvertToShort(data, ref pos);
        Console.WriteLine("Answers Count:{0}", anCount);
        //NSCOUNT
        int nsCount = ConvertToShort(data, ref pos);
        //ARCOUNT
        int arCount = ConvertToShort(data, ref pos);
        //Questionを飛ばす
        pos = startPos;

        List<string> list = new List<string>();
        //回答数だけ繰り返す
        for (int i = 0; i < anCount; i++)
        {
            //ドメイン名を取り出す
            string domainName = GetDomainName(data, ref pos);
            Console.WriteLine("Domain:{0}", domainName);
            //TYPE
            short ppType = ConvertToShort(data, ref pos);
            //CLASS
            short ppClass = ConvertToShort(data, ref pos);
            //TTL
            int ttl = ConvertToInt(data, ref pos);
            Console.WriteLine("TTL:{0}", ttl);
            //RDLENGTH
            short rdLength = ConvertToShort(data, ref pos);

            string str = string.Empty;
            switch (ppType)
            {
                case 1:
                    //A
                case 28:
                    //AAAA
                    //IPアドレスを抜き出す
                    str = GetIPAddress(data, rdLength, ref pos);
                    break;
                case 2:
                    //NS
                case 5:
                    //CNAME
                case 12:
                    //PTR
                    //ホスト名を抜き出す
                    str = GetDomainName(data, ref pos);
                    break;
                case 15:
                    //MX
                    //Preference(優先度)
                    short preference = ConvertToShort(data, ref pos);
                    Console.WriteLine("Preference:{0}", preference);
                    //ホスト名を抜き出す
                    str = GetDomainName(data, ref pos);
                    break;
                default:
                    str = string.Format("(対応していないType({0})の回答)",
                        ppType);
                    pos += rdLength;
                    break;
            }
            list.Add(str);
            Console.WriteLine(str);
        }
        //Answerのみを解析し、AuthorityとAdditionalは無視

        return list.ToArray();
    }

    //データの指定位置からDomain名を取得する
    private static string GetDomainName(byte[] data, ref int pos)
    {
        StringBuilder sb = new StringBuilder();

        while (pos < data.Length)
        {
            //labelの長さを取得
            int len = data[pos++];
            //長さが0の時、終了
            if (len == 0) { break; }
            if ((len & 0xC0) == 0xC0)
            {
                //Message compressionの時
                //新たな位置を取得
                int newpos = ConvertToShort((byte)(len & 0x3F), data[pos++]);
                //新たな位置から取得したDomainを追加
                if (sb.Length > 0) { sb.Append('.'); }
                sb.Append(GetDomainName(data, ref newpos));
                break;
            }

            //byteを文字列に変換して、labelを取得し、追加する
            if (sb.Length > 0) { sb.Append('.'); }
            sb.Append(Encoding.ASCII.GetString(data, pos, len));
            pos += len;
        }

        return sb.ToString();
    }

    //データの指定位置からIPアドレスを取得する
    private static string GetIPAddress(byte[] data, int len, ref int pos)
    {
        byte[] bs = new byte[len];
        Array.Copy(data, pos, bs, 0, len);
        pos += len;
        IPAddress ip = new IPAddress(bs);
        return ip.ToString();

        //または、IPv4の時は、
        //return string.Format("{0}.{1}.{2}.{3}",
        //    data[pos++], data[pos++], data[pos++], data[pos++]);
    }


    //2つのバイトからInt16を作成
    private static short ConvertToShort(byte b1, byte b2)
    {
        int i = 0;
        return ConvertToShort(new byte[] { b1, b2 }, ref i);
    }
    private static short ConvertToShort(byte[] data, ref int pos)
    {
        short r = IPAddress.NetworkToHostOrder(
            BitConverter.ToInt16(data, pos));
        pos += 2;
        return r;

        //または、
        //return (short)(data[post++] << 8 | data[post++]);
    }

    //4つのバイトからInt32を作成
    private static int ConvertToInt(byte[] data, ref int pos)
    {
        int r = IPAddress.NetworkToHostOrder(
            BitConverter.ToInt32(data, pos));
        pos += 4;
        return r;
    }
}

注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。

  • .NET Tipsをご利用いただく際は、注意事項をお守りください。