.NET Frameworkでは、「ホスト名からIPアドレス、IPアドレスからホスト名を取得する」で紹介しているように、DNSサーバーに問い合わせをして、正引き(ホスト名からIPアドレスの取得)と逆引き(IPアドレスからホスト名の取得)を行う方法は用意されています。しかし、MXなどのレコードを検索したり、DNSサーバーを指定して検索する方法は用意されていません。
そこでここでは、Socket(サンプルでは、UdpClientクラス)を使用して、DNSサーバーと直接やり取りをする方法を紹介します。
ただしここではサンプルコードを紹介するだけで、DNSに関する説明はしません。その分、サンプルコードには多くのコメントを付けました。
DNSの仕様については、RFC1034、RFC1035、RFC1885、RFC2915などに書かれています。また、このサンプルを作成するにあたり、「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)を使用しています。
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
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; } }
注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。