DNSサーバーと直接やり取りして、情報を取得する †
.NET Frameworkでは、「ホスト名からIPアドレス、IPアドレスからホスト名を取得する」で説明しているように、Dns.GetHostEntryメソッドを使用することで、DNSに問い合わせをして、逆引き(IPアドレスからドメイン名を取得)と正引き(ドメイン名からIPアドレスを取得)を行うことができます。しかし、NSやMXなどのレコードを検索したり、DNSサーバーを指定して検索することはできません。
もしこのようなDNSクライアントの機能が必要であれば、サードパーティー製のライブラリを使用するのが手っ取り早いでしょう。*1例えば、フリーで公開されているライブラリには、以下のようなものがあります。
しかしここでは、あえてSocket(サンプルでは、UdpClientクラス)を使用して、直接DNSサーバーとやり取りすることに挑戦します。
具体例 †
本来なら、RFC*2に書かれているDNSの決まり事をここで説明すべきなのだと思いますが、私にはそれだけの能力も余裕もありませんので、申し訳ありませんが、サンプルコードのみを示し、説明はコード内のコメントに記述しておきます。
このサンプルはコンソールアプリケーションで、指定されたDNSサーバーを使用して、Aレコードを検索し、ホスト名「microsoft.com」からIPアドレスを取得(正引き)しています。
このサンプルは、Aレコードの他、NS、CNAME、PTR、MXにも対応しています。検索するレコードを変更する場合は、MainメソッドのqTypeの値を変更してください。その場合、domainOrAddressの文字列も適当に(例えば、PTRならばIPアドレスに)変更してください。
このサンプルでは、DNSサーバーとして「Google Public DNS」を使用するようにしていますが、適当なDNSサーバーに変更してください。*3
このサンプルを作成するにあたり、以下の記事を参考にさせていただきました。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
| | Imports System.Net
Imports System.Text
Imports System.Collections.Generic
Class Program
Public Shared Sub Main(args As String())
Dim dns As String = "8.8.8.8"
Dim dnsPort As Integer = 53
Dim domainOrAddress As String = "microsoft.com"
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)
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()
ms.WriteByte(0)
ms.WriteByte(0)
ms.WriteByte(1)
ms.WriteByte(0)
ms.WriteByte(0)
ms.WriteByte(1)
ms.WriteByte(0)
ms.WriteByte(0)
ms.WriteByte(0)
ms.WriteByte(0)
ms.WriteByte(0)
ms.WriteByte(0)
Dim buf1 As String() = qName.Split("."c)
If qType = 12 Then
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)
ms.WriteByte(0)
ms.WriteByte(qType)
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
Dim resCode As Integer = data(3) And &HF
Console.WriteLine("Response Code:{0}", resCode)
pos = 6
Dim anCount As Integer = ConvertToShort(data, pos)
Console.WriteLine("Answers Count:{0}", anCount)
Dim nsCount As Integer = ConvertToShort(data, pos)
Dim arCount As Integer = ConvertToShort(data, pos)
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)
Dim ppType As Short = ConvertToShort(data, pos)
pos += 6
Dim rdLength As Short = ConvertToShort(data, pos)
Dim str As String = "???"
Select Case ppType
Case 1
str = GetIPv4Address(data, pos)
list.Add(str)
Exit Select
Case 2, 5, 12
PTR:
str = GetDomainName(data, pos)
list.Add(str)
Exit Select
Case 15
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
Private Shared Function GetDomainName( _
data As Byte(), ByRef pos As Integer) As String
Dim sb As New StringBuilder()
While pos < data.Length
Dim len As Integer = data(pos)
pos += 1
If len = 0 Then
Exit While
End If
If (len And &HC0) = &HC0 Then
Dim newpos As Integer = _
ConvertToShort(CByte(len And &H3F), data(pos))
pos += 1
If sb.Length > 0 Then
sb.Append("."c)
End If
sb.Append(GetDomainName(data, newpos))
Exit While
End If
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
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
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
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
| | using System;
using System.Net;
using System.Text;
using System.Collections.Generic;
class Program
{
public static void Main(string[] args)
{
string dns = "8.8.8.8";
int dnsPort = 53;
string domainOrAddress = "microsoft.com";
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))
{
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())
{
ms.WriteByte(0);
ms.WriteByte(0);
ms.WriteByte(1);
ms.WriteByte(0);
ms.WriteByte(0);
ms.WriteByte(1);
ms.WriteByte(0);
ms.WriteByte(0);
ms.WriteByte(0);
ms.WriteByte(0);
ms.WriteByte(0);
ms.WriteByte(0);
string[] buf1 = qName.Split('.');
if (qType == 12)
{
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);
ms.WriteByte(0);
ms.WriteByte(qType);
ms.WriteByte(0);
ms.WriteByte(1);
return ms.ToArray();
}
}
private static string[] GetAnswersFromResponse(byte[] data, int startPos)
{
int pos = 0;
int resCode = data[3] & 0xF;
Console.WriteLine("Response Code:{0}", resCode);
pos = 6;
int anCount = ConvertToShort(data, ref pos);
Console.WriteLine("Answers Count:{0}", anCount);
int nsCount = ConvertToShort(data, ref pos);
int arCount = ConvertToShort(data, ref pos);
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);
short ppType = ConvertToShort(data, ref pos);
pos += 6;
short rdLength = ConvertToShort(data, ref pos);
string str = "???";
switch (ppType)
{
case 1:
str = GetIPv4Address(data, ref pos);
list.Add(str);
break;
case 2:
case 5:
case 12:
str = GetDomainName(data, ref pos);
list.Add(str);
break;
case 15:
short preference = ConvertToShort(data, ref pos);
Console.WriteLine("Preference:{0}", preference);
goto case 2;
}
Console.WriteLine(str);
}
return list.ToArray();
}
private static string GetDomainName(byte[] data, ref int pos)
{
StringBuilder sb = new StringBuilder();
while (pos < data.Length)
{
int len = data[pos++];
if (len == 0) { break; }
if ((len & 0xC0) == 0xC0)
{
int newpos = ConvertToShort((byte)(len & 0x3F), data[pos]);
pos++;
if (sb.Length > 0) { sb.Append('.'); }
sb.Append(GetDomainName(data, ref newpos));
break;
}
if (sb.Length > 0) { sb.Append('.'); }
sb.Append(Encoding.ASCII.GetString(data, pos, len));
pos += len;
}
return sb.ToString();
}
private static string GetIPv4Address(byte[] data, ref int pos)
{
return string.Format("{0}.{1}.{2}.{3}",
data[pos++], data[pos++], data[pos++], data[pos++]);
}
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++]);
}
}
|
補足説明 †
上記サンプルについて、少し補足の説明をします。
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メソッドを使用してバイト順を正しく並べ直す必要があります。
最後に †
皆様のおかげで、今年も一年無事に過ごすことができました。ありがとうございました。来年もよろしくお願いいたします。それでは、よいお年を。
コメント †
|