#title(DNSサーバーと直接やり取りして、情報を取得する) #navi(.NETプログラミング研究) #contents *DNSサーバーと直接やり取りして、情報を取得する [#g8a63083] .NET Frameworkでは、「[[ホスト名からIPアドレス、IPアドレスからホスト名を取得する>http://dobon.net/vb/dotnet/internet/dnslookup.html]]」で説明しているように、Dns.GetHostEntryメソッドを使用することで、DNSに問い合わせをして、逆引き(IPアドレスからドメイン名を取得)と正引き(ドメイン名からIPアドレスを取得)を行うことができます。しかし、NSやMXなどのレコードを検索したり、DNSサーバーを指定して検索することはできません。 もしこのようなDNSクライアントの機能が必要であれば、サードパーティー製のライブラリを使用するのが手っ取り早いでしょう。((それ以外の方法としては、nslookup.exeを使用する、[[DnsQuery function:MSDN]]を使用するなどが考えられます。))例えば、フリーで公開されているライブラリには、以下のようなものがあります。 -[[C# .NET DNS query component>http://www.codeproject.com/Articles/12072/C-NET-DNS-query-component]] -[[DNS.NET Resolver>http://www.codeproject.com/Articles/23673/DNS-NET-Resolver-C]] -[[DNS Client Library for .NET>http://www.simpledns.com/dns-client-lib.aspx]] -[[OpenDNS.NET>http://sourceforge.net/projects/opendnsnet/]] -[[ARSoft.Tools.Net - C#/.Net DNS client/server, SPF and SenderID Library>http://arsofttoolsnet.codeplex.com/]] -[[DnDns and PocketDnDns - A .NET DNS Client Resolver Library>http://dndns.codeplex.com/]] しかしここでは、あえてSocket(サンプルでは、UdpClientクラス)を使用して、直接DNSサーバーとやり取りすることに挑戦します。 **具体例 [#j39108b4] 本来なら、RFC((RFC1034、RFC1035、RFC1885、RFC2915等))に書かれているDNSの決まり事をここで説明すべきなのだと思いますが、私にはそれだけの能力も余裕もありませんので、申し訳ありませんが、サンプルコードのみを示し、説明はコード内のコメントに記述しておきます。 このサンプルはコンソールアプリケーションで、指定されたDNSサーバーを使用して、Aレコードを検索し、ホスト名「microsoft.com」からIPアドレスを取得(正引き)しています。 このサンプルは、Aレコードの他、NS、CNAME、PTR、MXにも対応しています。検索するレコードを変更する場合は、MainメソッドのqTypeの値を変更してください。その場合、domainOrAddressの文字列も適当に(例えば、PTRならばIPアドレスに)変更してください。 このサンプルでは、DNSサーバーとして「[[Google Public DNS>https://developers.google.com/speed/public-dns/]]」を使用するようにしていますが、適当なDNSサーバーに変更してください。((公開されているDNSサーバーの情報は、「[[Free and Public DNS Server List>http://pcsupport.about.com/od/tipstricks/a/free-public-dns-servers.htm]]」などにあります。)) このサンプルを作成するにあたり、以下の記事を参考にさせていただきました。 -[[RFC 1035>http://www.faqs.org/rfcs/rfc1035.html]] -[[DNS クライアントを作ってみよう (2)>http://x68000.q-e-d.net/~68user/net/resolver-2.html]] -[[c# - How to get mx records for a dns name with System.Net.DNS? - Stack Overflow>http://stackoverflow.com/questions/2669841/how-to-get-mx-records-for-a-dns-name-with-system-net-dns/7546721]] #code(vbnet){{ Imports System.Net Imports System.Text Imports System.Collections.Generic Class Program 'エントリポイント Public Shared Sub Main(args As String()) 'DNSのIPアドレスまたはホスト名 Dim dns As String = "8.8.8.8" 'DNSのポート番号 Dim dnsPort As Integer = 53 '検索するホスト名またはIPv4アドレス Dim domainOrAddress As String = "microsoft.com" 'Dim domainOrAddress As String = "134.170.185.46" 'レコード種別 'A=1 NS=2 CNAME=5 PTR=12 MX=15 のみに対応 Dim qType As Byte = 1 '送信するデータを作成する Dim req As Byte() = CreateRequestMessage(domainOrAddress, qType) Console.WriteLine(BitConverter.ToString(req)) Dim res As Byte() = Nothing Using udpc As New System.Net.Sockets.UdpClient(dns, dnsPort) 'UDPでリクエストメッセージを送信する udpc.Send(req, req.Length) 'レスポンスを受信する Dim ep As IPEndPoint = Nothing res = udpc.Receive(ep) End Using Console.WriteLine(BitConverter.ToString(res)) '受信したデータから回答を取り出す Dim answers As String() = GetAnswersFromResponse(res, req.Length) '回答を表示する Console.WriteLine("回答:") For Each s As String In answers Console.WriteLine(s) Next Console.ReadLine() End Sub 'リクエストメッセージを作成する Private Shared Function CreateRequestMessage( _ qName As String, qType As Byte) As Byte() qName = qName.TrimEnd("."c) Using ms As New System.IO.MemoryStream() 'Header section 'ID ms.WriteByte(0) ms.WriteByte(0) 'QR、Opcode、AA、TC、RD(RDのみ1) ms.WriteByte(1) 'RA、Z、RCODE ms.WriteByte(0) 'QDCOUNT(質問数) 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 を作成 Dim buf1 As String() = qName.Split("."c) If qType = 12 Then 'PTRの時は、IPアドレスを逆に並べて、.in-addr.arpa を付ける buf1 = New String() _ {buf1(3), buf1(2), buf1(1), buf1(0), "in-addr", "arpa"} End If 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(Internetは1) ms.WriteByte(0) ms.WriteByte(1) Return ms.ToArray() End Using 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、TTLを飛ばす pos += 6 'RDLENGTH Dim rdLength As Short = ConvertToShort(data, pos) Dim str As String = "???" Select Case ppType Case 1 'A 'IPアドレスを抜き出す str = GetIPv4Address(data, pos) list.Add(str) Exit Select Case 2, 5, 12 PTR: 'NS 'CNAME 'PTR 'ドメイン名を抜き出す str = GetDomainName(data, pos) list.Add(str) Exit Select Case 15 'MX 'Preference Dim preference As Short = ConvertToShort(data, pos) Console.WriteLine("Preference:{0}", preference) 'ドメイン名を取得する GoTo PTR End Select Console.WriteLine(str) Next 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 GetIPv4Address( _ data As Byte(), ByRef pos As Integer) As String 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 Return CShort(b1 << 8 Or b2) End Function Private Shared Function ConvertToShort( _ data As Byte(), ByRef pos As Integer) As Short pos += 2 Return ConvertToShort(data(pos - 2), data(pos - 1)) End Function End Class }} #code(csharp){{ using System; using System.Net; using System.Text; using System.Collections.Generic; class Program { //エントリポイント public static void Main(string[] args) { //DNSのIPアドレスまたはホスト名 string dns = "8.8.8.8"; //DNSのポート番号 int dnsPort = 53; //検索するホスト名またはIPv4アドレス string domainOrAddress = "microsoft.com"; //string domainOrAddress = "134.170.185.46"; //レコード種別 //A=1 NS=2 CNAME=5 PTR=12 MX=15 のみに対応 byte qType = 1; //送信するデータを作成する byte[] req = CreateRequestMessage(domainOrAddress, qType); Console.WriteLine(BitConverter.ToString(req)); byte[] res = null; using (System.Net.Sockets.UdpClient udpc = new System.Net.Sockets.UdpClient(dns, dnsPort)) { //UDPでリクエストメッセージを送信する udpc.Send(req, req.Length); //レスポンスを受信する IPEndPoint ep = null; res = udpc.Receive(ref ep); } Console.WriteLine(BitConverter.ToString(res)); //受信したデータから回答を取り出す string[] answers = GetAnswersFromResponse(res, req.Length); //結果を表示する Console.WriteLine("結果:"); foreach (string s in answers) { Console.WriteLine(s); } Console.ReadLine(); } //リクエストメッセージを作成する private static byte[] CreateRequestMessage(string qName, byte qType) { qName = qName.TrimEnd('.'); using (System.IO.MemoryStream ms = new System.IO.MemoryStream()) { //Header section //ID ms.WriteByte(0); ms.WriteByte(0); //QR、Opcode、AA、TC、RD(RDのみ1) ms.WriteByte(1); //RA、Z、RCODE ms.WriteByte(0); //QDCOUNT(質問数) 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 を作成 string[] buf1 = qName.Split('.'); if (qType == 12) { //PTRの時は、IPアドレスを逆に並べて、.in-addr.arpa を付ける buf1 = new string[] { buf1[3], buf1[2], buf1[1], buf1[0], "in-addr", "arpa" }; } foreach (string s in buf1) { //文字列をバイト配列に変換 byte[] bs1 = System.Text.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(Internetは1) ms.WriteByte(0); ms.WriteByte(1); return ms.ToArray(); } } //レスポンスメッセージから回答を取得する 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、TTLを飛ばす pos += 6; //RDLENGTH short rdLength = ConvertToShort(data, ref pos); string str = "???"; switch (ppType) { case 1: //A //IPアドレスを抜き出す str = GetIPv4Address(data, ref pos); list.Add(str); break; case 2: //NS case 5: //CNAME case 12: //PTR //ドメイン名を抜き出す str = GetDomainName(data, ref pos); list.Add(str); break; case 15: //MX //Preference short preference = ConvertToShort(data, ref pos); Console.WriteLine("Preference:{0}", preference); //ドメイン名を取得する goto case 2; } Console.WriteLine(str); } 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]); 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 GetIPv4Address(byte[] data, ref int pos) { 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) { return (short)(b1 << 8 | b2); } private static short ConvertToShort(byte[] data, ref int pos) { return ConvertToShort(data[pos++], data[pos++]); } } }} **補足説明 [#n4fbbed6] 上記サンプルについて、少し補足の説明をします。 CreateRequestMessageメソッドで、DNSサーバーに送信するデータを作成しています。ここでは、ホスト名とレコード種別をバイト配列に変換する以外は、すべて定数を使っています。IDは、00 00 に固定しています。QCLASSは、"IN"を表す 1 にしています。 GetAnswersFromResponseメソッドで、DNSサーバーから受信したデータを解析しています。ここでは、Header部分は、RCODE(Response Code)とANCOUNT(Answer数)だけを取得して、それ以外は無視しています(NSCOUNTとARCOUNTも取得はしていますが、使用していません)。Question部分は、送信したQuestionと同じ長さだと決めつけて、読み飛ばしています。その後のAnswer部分のみを解析して、AuthorityとAdditionalはあっても(NSCOUNTかARCOUNTが0でなくても)無視しています。 Answer部分の解析は、NAME(ドメイン名)、TYPE(レコード種別)、RDLENGTH(RDATAの長さ)、RDATAの取得だけを行い、他(CLASSとTTL)は無視しています。RDATAの解析は、それがドメイン名ならばGetDomainNameメソッド、IPアドレスならばGetIPv4Addressメソッドで行っています。MXレコードの場合は、その前にPreference(優先度)を取得しています。 ConvertToShortメソッドでは、2つのバイト値から1つのInt16値を作成しています。.NET Frameworkには、よく似た機能のメソッドにBitConverter.ToInt16メソッドがありますが、このメソッドはシステムによって結果が変わる可能性があります(Windowsの場合は、そのままのバイト配列順では、間違った値になってしまいます)。BitConverterを使用した場合は、その後でIPAddress.NetworkToHostOrderメソッドを使用してバイト順を正しく並べ直す必要があります。 **最後に [#hbed5db1] 皆様のおかげで、今年も一年無事に過ごすことができました。ありがとうございました。来年もよろしくお願いいたします。それでは、よいお年を。 **コメント [#l631ec6a] #comment //これより下は編集しないでください #pageinfo([[:Category/.NET]] [[:Category/ASP.NET]],2014-12-30 (火) 04:41:32,DOBON!,2014-12-30 (火) 04:41:32,DOBON!) |