Dnslib
A Python library to encode/decode DNS wire-format packets
Install / Use
/learn @paulc/DnslibREADME
From Version 0.9.12 the master repository for dnslib has been moved to GitHub (https://github.com/paulc/dnslib). Please update any links to the original BitBucket repository as this will no longer be maintained.
~~Release 0.9.24 (2024-01-02) will be the last release supporting Python 2.7 and Python <3.7. Supporting old Python versions is increasingly painful and holds back adoption of new features so it's probably time to move on.~~
Given the scale of the changes (and API changes) add typing support/modernise the codebase I've decided to leave dnslib as is in maintenance mode for users who are relying on the old API (and Python2 support). I'll try to fix any minor bugs but there isnt going to be any more active development (which is more or less the status quo anyway).
dnslib
A library to encode/decode DNS wire-format packets supporting both Python 2.7 and Python 3.2+.
The library provides:
-
Support for encoding/decoding DNS packets between wire format, python objects, and Zone/DiG textual representation (dnslib.dns)
-
A server framework allowing the simple creation of custom DNS resolvers (dnslib.server) and a number of example servers created using this framework
-
A number of utilities for testing (dnslib.client, dnslib.proxy, dnslib.intercept)
Python 3 support was added in Version 0.9.0 which represented a fairly major update to the library - the key changes include:
-
Python 2.7/3.2+ support (the last version supporting Python 2.6 or earlier was version 0.8.3)
-
The 'Bimap' interface was changed significantly to explicitly split forward (value->text) lookups via getitem and reverse (text->value) lookups via getattr. Applications using the old interface will need to be updated.
-
Hostnames are now returned with a trailing dot by default (in line with RFC)
-
Most object attributes are now typed in line with the record definitions to make it harder to generate invalid packets
-
Support for encoding/decoding resource records in 'Zone' (BIND) file format
-
Support for encoding/decoding packets in 'DiG' format
-
Server framework allowing (in most cases) custom resolvers to be created by just subclassing the DNSResolver class and overriding the 'resolve' method
-
A lot of fixes to error detection/handling which should make the library much more robust to invalid/unsupported data. The library should now either return a valid DNSRecord instance when parsing a packet or raise DNSError (tested via fuzzing)
-
Improved utilities (dnslib.client, dnslib.proxy, dnslib.intercept)
-
Improvements to encoding/decoding tests including the ability to generate test data automatically in test_decode.py (comparing outputs against DiG)
-
Ability to compare and diff DNSRecords
Classes
The key DNS packet handling classes are in dnslib.dns and map to the standard DNS packet sections:
- DNSRecord - container for DNS packet. Contains:
- DNSHeader
- Question section containing zero or more DNSQuestion objects
- Answer section containing zero or more RR objects
- Authority section containing zero or more RR objects
- Additional section containing zero or more RR objects
- DNS RRs (resource records) contain an RR header and an RD object)
- Specific RD types are implemented as subclasses of RD
- DNS labels are represented by a DNSLabel class - in most cases this handles conversion to/from textual representation however does support arbitatry labels via a tuple of bytes objects
Usage
To decode a DNS packet:
>>> packet = binascii.unhexlify(b'd5ad818000010005000000000377777706676f6f676c6503636f6d0000010001c00c0005000100000005000803777777016cc010c02c0001000100000005000442f95b68c02c0001000100000005000442f95b63c02c0001000100000005000442f95b67c02c0001000100000005000442f95b93')
>>> d = DNSRecord.parse(packet)
>>> d
<DNS Header: id=0xd5ad type=RESPONSE opcode=QUERY flags=RD,RA rcode='NOERROR' q=1 a=5 ns=0 ar=0>
<DNS Question: 'www.google.com.' qtype=A qclass=IN>
<DNS RR: 'www.google.com.' rtype=CNAME rclass=IN ttl=5 rdata='www.l.google.com.'>
<DNS RR: 'www.l.google.com.' rtype=A rclass=IN ttl=5 rdata='66.249.91.104'>
<DNS RR: 'www.l.google.com.' rtype=A rclass=IN ttl=5 rdata='66.249.91.99'>
<DNS RR: 'www.l.google.com.' rtype=A rclass=IN ttl=5 rdata='66.249.91.103'>
<DNS RR: 'www.l.google.com.' rtype=A rclass=IN ttl=5 rdata='66.249.91.147'>
The default text representation of the DNSRecord is in zone file format:
>>> print(d)
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 54701
;; flags: qr rd ra; QUERY: 1, ANSWER: 5, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;www.google.com. IN A
;; ANSWER SECTION:
www.google.com. 5 IN CNAME www.l.google.com.
www.l.google.com. 5 IN A 66.249.91.104
www.l.google.com. 5 IN A 66.249.91.99
www.l.google.com. 5 IN A 66.249.91.103
www.l.google.com. 5 IN A 66.249.91.147
To create a DNS Request Packet:
>>> d = DNSRecord.question("google.com")
(This is equivalent to: d = DNSRecord(q=DNSQuestion("google.com") )
>>> d
<DNS Header: id=... type=QUERY opcode=QUERY flags=RD rcode='NOERROR' q=1 a=0 ns=0 ar=0>
<DNS Question: 'google.com.' qtype=A qclass=IN>
>>> str(DNSRecord.parse(d.pack())) == str(d)
True
>>> print(d)
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ...
;; flags: rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;google.com. IN A
>>> d = DNSRecord.question("google.com","MX")
(This is equivalent to: d = DNSRecord(q=DNSQuestion("google.com",QTYPE.MX) )
>>> str(DNSRecord.parse(d.pack())) == str(d)
True
>>> print(d)
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ...
;; flags: rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;google.com. IN MX
To create a DNS Response Packet:
>>> d = DNSRecord(DNSHeader(qr=1,aa=1,ra=1),
... q=DNSQuestion("abc.com"),
... a=RR("abc.com",rdata=A("1.2.3.4")))
>>> d
<DNS Header: id=... type=RESPONSE opcode=QUERY flags=AA,RD,RA rcode='NOERROR' q=1 a=1 ns=0 ar=0>
<DNS Question: 'abc.com.' qtype=A qclass=IN>
<DNS RR: 'abc.com.' rtype=A rclass=IN ttl=0 rdata='1.2.3.4'>
>>> str(DNSRecord.parse(d.pack())) == str(d)
True
>>> print(d)
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ...
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;abc.com. IN A
;; ANSWER SECTION:
abc.com. 0 IN A 1.2.3.4
It is also possible to create RRs from a string in zone file format
>>> RR.fromZone("abc.com IN A 1.2.3.4")
[<DNS RR: 'abc.com.' rtype=A rclass=IN ttl=0 rdata='1.2.3.4'>]
(Note: this produces a list of RRs which should be unpacked if being
passed to add_answer/add_auth/add_ar etc)
>>> q = DNSRecord.question("abc.com")
>>> a = q.reply()
>>> a.add_answer(*RR.fromZone("abc.com 60 A 1.2.3.4"))
>>> print(a)
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ...
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;abc.com. IN A
;; ANSWER SECTION:
abc.com. 60 IN A 1.2.3.4
The zone file can contain multiple entries and supports most of the normal format defined in RFC1035 (specifically not $INCLUDE)
>>> z = '''
... $TTL 300
... $ORIGIN abc.com
...
... @ IN MX 10 mail.abc.com.
... www IN A 1.2.3.4
... IN TXT "Some Text"
... mail IN CNAME www.abc.com.
... '''
>>> for rr in RR.fromZone(textwrap.dedent(z)):
... print(rr)
abc.com. 300 IN MX 10 mail.abc.com.
www.abc.com. 300 IN A 1.2.3.4
www.abc.com. 300 IN TXT "Some Text"
mail.abc.com. 300 IN CNAME www.abc.com.
To create a skeleton reply to a DNS query:
>>> q = DNSRecord(q=DNSQuestion("abc.com",QTYPE.ANY))
>>> a = q.reply()
>>> a.add_answer(RR("abc.com",QTYPE.A,rdata=A("1.2.3.4"),ttl=60))
>>> str(DNSRecord.parse(a.pack())) == str(a)
True
>>> print(a)
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ...
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;abc.com. IN ANY
;; ANSWER SECTION:
abc.com. 60 IN A 1.2.3.4
Add additional RRs:
>>> a.add_answer(RR("xxx.abc.com",QTYPE.A,rdata=A("1.2.3.4")))
>>> a.add_answer(RR("xxx.abc.com",QTYPE.AAAA,rdata=AAAA("1234:5678::1")))
>>> str(DNSRecord.parse(a.pack())) == str(a)
True
>>> print(a)
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ...
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;abc.com. IN ANY
;; ANSWER SECTION:
abc.com. 60 IN A 1.2.3.4
xxx.abc.com. 0 IN A 1.2.3.4
xxx.abc.com. 0 IN AAAA 1234:5678::1
It is also possible to create a reply from a string in zone file format:
>>> q = DNSRecord(q=DNSQuestion("abc.com",QTYPE.ANY))
>>> a = q.replyZone("abc.com 60 IN CNAME xxx.abc.com")
>>> print(a)
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ...
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTI
Related Skills
node-connect
349.9kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
claude-opus-4-5-migration
109.8kMigrate prompts and code from Claude Sonnet 4.0, Sonnet 4.5, or Opus 4.1 to Opus 4.5
frontend-design
109.8kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
model-usage
349.9kUse CodexBar CLI local cost usage to summarize per-model usage for Codex or Claude, including the current (most recent) model or a full model breakdown. Trigger when asked for model-level usage/cost data from codexbar, or when you need a scriptable per-model summary from codexbar cost JSON.
