DNS server in Python ....

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.

Mystery Rule-> Don't use DNS related libraries

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 ...)

What I wrote

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 ...)

Library used-> binarize

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.

When I write a DNS request header with binarize, it looks like this

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

When I write a query record with binarize, it looks like this

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.

Draw test and return loopback address

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.

Try to pull the IP address with dig

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.

I had a lot of trouble ...

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.

Eventually wireshark ...

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.

Afterword

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

DNS server in Python ....
Write an HTTP / 2 server in Python
Quadtree in Python --2
CURL in python
Metaprogramming in Python
Python 3.3 in Anaconda
Geocoding in python
SendKeys in Python
Meta-analysis in Python
Unittest in python
Discord in Python
DCI in Python
quicksort in python
nCr in python
N-Gram in Python
Programming in python
Plink in Python
Constant in python
Lifegame in Python.
Sqlite in python
StepAIC in Python
N-gram in python
LINE-Bot [0] in Python
Csv in python
Disassemble in Python
Reflection in Python
Constant in python
nCr in Python.
format in python
Scons in Python3
Puyo Puyo in python
python in virtualenv
PPAP in Python
Quad-tree in Python
Reflection in Python
Chemistry in Python
Hashable in python
DirectLiNGAM in Python
LiNGAM in Python
Flatten in python
flatten in python
CGI server (1) python edition in one line
Sorted list in Python
Daily AtCoder # 36 in Python
Clustering text in Python
Daily AtCoder # 2 in Python
Implement Enigma in python
Daily AtCoder # 32 in Python
Set up a simple HTTPS server in Python 3
Daily AtCoder # 6 in Python
Edit fonts in Python
Singleton pattern in Python
File operations in Python
Read DXF in python
Daily AtCoder # 53 in Python
Key input in Python
Use config.ini in Python
Daily AtCoder # 33 in Python
Solve ABC168D in Python
Logistic distribution in Python
python> udp> echo server