This article is the 24th day article of Python Advent Calendar 2015.
Until now, it's a guy who can do Name <-> IP Address without understanding DNS or anything. There was only about recognition. Well, the specifications are clear, and I tried it with a light glue that I could write it soon.
It's pretty good for everyone, but I think it's a good idea to put what you want to write now.
PyPI has DNS protocol related libraries. (And quite a lot) I usually pick up some of them and try them, but this time I was motivated to write them myself, so I decided not to use them. (Because the DNS specifications do not come to my mind when I use it ...)
I uploaded it to Gist for the time being. https://gist.github.com/TakesxiSximada/802d36068f09a3393541
It does not work as a proper DNS server. I can barely draw an A record. (But that's also a definite decision ...)
The only third party library I used was binarize. https://pypi.python.org/pypi/binarize This is a library for performing pack (encode) and unpack (decode) operations by giving binary data to a class like a structure. You can do almost the same thing as the struct module in the standard library, but it feels like you can have class variables carry data information.
For example, suppose there is a header that expresses code in the first 1 byte and status in the next 2 bytes. In that case, binarize can be expressed as follows.
>>> from binarize import Structure, UINT8, UINT16
>>> class Header(Structure):
... code = UINT8()
... status = UINT16()
...
>>>
The order of data is the order of variable definition. Byte order is big endian by default. This class has a class method called decode. You can treat the data as a Header object by passing a byte string to it.
>>> header = Header.decode(b'\x01\x00\x02')
>>> header.code
1
>>> header.status
2
The header has encode () and can be serialized to a byte string.
>>> header.encode()
b'\x01\x00\x02'
>>>
This binarize seems to be rarely used. It didn't have any Github stars, and the number of PyPI downloads wasn't that great (it looks like it's almost not DL, and it's a little neglected). However, it was written relatively well, and it was relatively good in terms of usability.
The DNS request header has this format.
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ID |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR| Opcode |AA|TC|RD|RA| Z | RCODE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QDCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ANCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NSCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ARCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
It looks like the following 16 bits from the beginning.
name | meaning |
---|---|
ID | identifier |
QR, Opcode, AA, TC, RD, RA, Z, RCODE | flag |
QDCOUNT | Number of question RRs |
ANCOUNT | Number of answer RRs |
NSCOUNT | Authority RR number |
ARCOUNT | Number of additional RRs |
RR is a resource record.
The meanings of the flags are as follows.
name | meaning |
---|---|
QR | inquiry=0,response=1 |
Opcode | Forward=1,Reverse resolution=2,Get server status=3 |
AA | Information managed by the DNS server=1 |
TC | The answer was too long and split=1 |
RD | Recursive query=1,Iterative query=0 |
RA | Recursive=1 |
Z | Reserve |
RCODE | Response status(Successful completion=0,Illegal format=1,Server error=2,Name error=3,Unimplemented=4,Reject=5) |
See the RFC for more details.
It is described in 4.1.1. Header section format
at http://www.ietf.org/rfc/rfc1035.txt.
The representation of these headers using binarize is as follows.
class Header(Structure):
identifier = UINT16()
flags = UINT16()
query_count = UINT16()
answer_count = UINT16()
authority_rr_count = UINT16()
addon_rr_count = UINT16()
https://gist.github.com/TakesxiSximada/802d36068f09a3393541#file-dns-py-L133-L139 It feels like a structure. Structure is binarize.Structure. I reserved 16 bits in Header.flags and made it feel like operating it. https://gist.github.com/TakesxiSximada/802d36068f09a3393541#file-dns-py-L141-L186
The query record has the following format:
1 1 1 1 1 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| |
/ QNAME /
/ /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QTYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QCLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
name | meaning |
---|---|
QNAME | Data to query("word count,Domain name,word countDomain name,0x00" <-Like this) |
QTYPE | Inquiry type(This time I only inquire about A record, so 1 will fly) |
QCLASS | Communication you are using(the Internet=1) |
Details can be found in 4.1.2. Question section format
at http://www.ietf.org/rfc/rfc1035.txt.
Expressed using binarize, it looks like this:
class QuestionRecord(Structure):
qname = BYTES(size=32, fill=b'')
qtype = UINT16()
qclass = UINT16()
https://gist.github.com/TakesxiSximada/802d36068f09a3393541#file-dns-py-L188-L191
One thing to keep in mind here is that qnames are variable length. The (uncompressed) qname is represented by a combination of the number of characters and the domain name string, ending in 0x00. This format cannot be parsed by binarize, so I had to decode it myself.
@classmethod
def decode(cls, data):
class _AnalyzeRecord(Structure):
qtype = UINT16()
qclass = UINT16()
record = cls()
qname_end = data.index(b'\x00') + 1
record.qname = data[:qname_end]
data = data[qname_end:]
analyze_record = _AnalyzeRecord.decode(data)
record.qtype = analyze_record.qtype
record.qclass = analyze_record.qclass
return record
https://gist.github.com/TakesxiSximada/802d36068f09a3393541#file-dns-py-L193-L207
Also, binarize.BYTES () was a specification to insert the number of subsequent bytes into the first byte if left alone.
def pack_bytes(bytes_, size=-1, fill=b'\x00'):
"""Pack Bytes."""
if size < 0:
yield from pack_size(len(bytes_))
yield bytes_
else:
missing = size - len(bytes_)
if missing < 0 or (missing > 0 and fill is None):
raise ValueError()
yield bytes_
yield fill * missing
https://github.com/socialcube/binarize/blob/4f0c71c985e22d1975dd8f5dfefdb7e8b5c01c57/binarize/primitives.py#L502
This time I cut corners and got the entire qname as a byte string, but if you think about it, it's a pretty nifty specification, so if you can add fields dynamically, you can also hold the qname in the BYTES () list. I thought it would be nice to have it as.
Of course, you should use the data saved in the DB here, but it's a little troublesome, so I'll write it directly.
NAME_IPADDR = {
(b'test', b''): b'\x7f\x00\x00\x01', # 127.0.0.1
}
https://gist.github.com/TakesxiSximada/802d36068f09a3393541#file-dns-py-L29-L31
The name test returns the loopback address.
Start the server. DNS is running on sudo because the port number is 53.
$ sudo python dns.py
Password:
Try pulling the name from another terminal with the dig command. The address after @ is the DNS address to go to the inquiry. Without this, it will query the serious DNS. This time I'm just running it on localhost, so specify 127.0.0.1.
$ dig A test @127.0.0.1
; <<>> DiG 9.8.3-P1 <<>> A test @127.0.0.1
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 3096
;; flags: qr; QUERY: 0, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; ANSWER SECTION:
test. 9 IN A 127.0.0.1
;; Query time: 2 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Fri Dec 25 21:43:04 2015
;; MSG SIZE rcvd: 32
$
Oh, it looks like it's closed. 127.0.0.1 was closed properly on the A record.
While working, I disliked the following message.
$ dig A test @127.0.0.1
;; Warning: Message parser reports malformed message packet.
Since the returned data was in a strange format, the dig command just issued a warning, but I had a hard time not knowing the cause. Most of the time, it was fairly rudimentary, such as the fact that the length of the data was incorrect or the number of data was incorrect, and the information was attached.
No matter how much you read the RFC or collect the information on the Web, if you don't notice it by mistake, you don't really notice it ... You can't really notice it while looking at the RFC and Web information and analyzing the data with Wireshark. It was often strange. It was the best way to see the data that was actually flowing.
A few days ago, I had a cold and fell asleep, so I posted an article one day later. I'm still a little dull, so I sleep with antibiotics. goodbye.
Recommended Posts