Initial commit

This commit is contained in:
Corentin 2025-12-26 18:43:44 +09:00
commit a061edf3a7
Signed by: corentin
GPG key ID: 48C87E27C6C917F4
3 changed files with 629 additions and 0 deletions

13
README.md Normal file
View file

@ -0,0 +1,13 @@
# PyDNS
Small educational python implementation of DNS protocol. Prints information as the `dig` tool for easier comparison.
## Usage
Simply run `dig.py` with 2 parameters, a `nameserver` (like 8.8.8.8 or your local router like 192.168.0.1) and the URL to lookup. Like the following:
``` sh
python dig.py 8.8.8.8 codeberg.org
```

571
dig.py Normal file
View file

@ -0,0 +1,571 @@
from __future__ import annotations
from argparse import ArgumentParser
from dataclasses import dataclass
from datetime import datetime
from enum import Enum, Flag, auto
from io import BytesIO
import socket
import struct
import time
from typing import Any
def format_hex(data: bytes, online: bool = False) -> str:
hex_repr = []
line = []
for i in range(0, len(data), 2):
line.append(data[i:i + 2].hex())
if not online and i % 16 == 14:
hex_repr.append(' '.join(line))
line = []
if line:
hex_repr.append(' '.join(line))
return '\n'.join(hex_repr)
# From RFC 1035
class BaseType(Enum):
A = 1 # a host address
NS = 2 # an authoritative name server
MD = 3 # a mail destination (Obsolete - use MX)
MF = 4 # a mail forwarder (Obsolete - use MX)
CNAME = 5 # the canonical name for an alias
SOA = 6 # marks the start of a zone of authority
MB = 7 # a mailbox domain name (EXPERIMENTAL)
MG = 8 # a mail group member (EXPERIMENTAL)
MR = 9 # a mail rename domain name (EXPERIMENTAL)
NULL = 10 # a null RR (EXPERIMENTAL)
WKS = 11 # a well known service description
PTR = 12 # a domain name pointer
HINFO = 13 # host information
MINFO = 14 # mailbox or mail list information
MX = 15 # mail exchange
TXT = 16 # text strings
class BaseQType(Enum):
AXFR = 252 # A request for a transfer of an entire zone
MAILB = 253 # A request for mailbox-related records (MB, MG or MR)
MAILA = 254 # A request for mail agent RRs (Obsolete - see MX)
WILDCARD = 255 # A request for all records
# From https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4
class Type(Enum):
RESERVED_1 = 0 # [RFC6895]
A = 1 # a host address [RFC1035]
NS = 2 # an authoritative name server [RFC1035]
MD = 3 # a mail destination (OBSOLETE - use MX) [RFC1035]
MF = 4 # a mail forwarder (OBSOLETE - use MX) [RFC1035]
CNAME = 5 # the canonical name for an alias [RFC1035]
SOA = 6 # marks the start of a zone of authority [RFC1035]
MB = 7 # a mailbox domain name (EXPERIMENTAL) [RFC1035]
MG = 8 # a mail group member (EXPERIMENTAL) [RFC1035]
MR = 9 # a mail rename domain name (EXPERIMENTAL) [RFC1035]
NULL = 10 # a null RR (EXPERIMENTAL) [RFC1035]
WKS = 11 # a well known service description [RFC1035]
PTR = 12 # a domain name pointer [RFC1035]
HINFO = 13 # host information [RFC1035]
MINFO = 14 # mailbox or mail list information [RFC1035]
MX = 15 # mail exchange [RFC1035]
TXT = 16 # text strings [RFC1035]
RP = 17 # for Responsible Person [RFC1183]
AFSDB = 18 # for AFS Data Base location [RFC1183] [RFC5864]
X25 = 19 # for X.25 PSDN address [RFC1183]
ISDN = 20 # for ISDN address [RFC1183]
RT = 21 # for Route Through [RFC1183]
NSAP = 22 # for NSAP address, NSAP style A record (DEPRECATED) [RFC1706]
NSAP_PTR = 23 # for domain name pointer, NSAP style (DEPRECATED) [RFC1706]
SIG = 24 # for security signature [RFC2536][RFC2931][RFC3110] [RFC4034]
KEY = 25 # for security key [RFC2536][RFC2539][RFC3110] [RFC4034]
PX = 26 # X.400 mail mapping information [RFC2163]
GPOS = 27 # Geographical Position [RFC1712]
AAAA = 28 # IP6 Address [RFC3596]
LOC = 29 # Location Information [RFC1876]
NXT = 30 # Next Domain (OBSOLETE) [RFC2535] [RFC3755]
EID = 31 # Endpoint Identifier [Michael_Patton] [http://ana-3.lcs.mit.edu/~jnc/nimrod/dns.txt]
NIMLOC = 32 # Nimrod Locator [1][Michael_Patton] [http://ana-3.lcs.mit.edu/~jnc/nimrod/dns.txt]
SRV = 33 # Server Selection [1] [RFC2782]
ATMA = 34 # ATM Address [ ATM Forum Technical Committee, "ATM Name System, V2.0", Doc ID: AF-DANS-0152.000,
# July 2000. Available from and held in escrow by IANA.]
NAPTR = 35 # Naming Authority Pointer [RFC3403]
KX = 36 # Key Exchanger [RFC2230]
CERT = 37 # CERT [RFC4398]
A6 = 38 # A6 (OBSOLETE - use AAAA) [RFC2874][RFC3226] [RFC6563]
DNAME = 39 # DNAME [RFC6672]
SINK = 40 # SINK [Donald_E_Eastlake] [draft-eastlake-kitchen-sink-02]
OPT = 41 # OPT [RFC3225] [RFC6891]
APL = 42 # APL [RFC3123]
DS = 43 # Delegation Signer [RFC4034]
SSHFP = 44 # SSH Key Fingerprint [RFC4255]
IPSECKEY = 45 # IPSECKEY [RFC4025]
RRSIG = 46 # RRSIG [RFC4034]
NSEC = 47 # NSEC [RFC4034] [RFC9077]
DNSKEY = 48 # DNSKEY [RFC4034]
DHCID = 49 # DHCID [RFC4701]
NSEC3 = 50 # NSEC3 [RFC5155] [RFC9077]
NSEC3PARAM = 51 # NSEC3PARAM [RFC5155]
TLSA = 52 # TLSA [RFC6698]
SMIMEA = 53 # S/MIME cert association [RFC8162]
HIP = 55 # Host Identity Protocol [RFC8005]
NINFO = 56 # NINFO [Jim_Reid]
RKEY = 57 # RKEY [Jim_Reid]
TALINK = 58 # Trust Anchor LINK [Wouter_Wijngaards]
CDS = 59 # Child DS [RFC7344]
CDNSKEY = 60 # DNSKEY(s) the Child wants reflected in DS [RFC7344]
OPENPGPKEY = 61 # OpenPGP Key [RFC7929]
CSYNC = 62 # Child-To-Parent Synchronization [RFC7477]
ZONEMD = 63 # Message Digest Over Zone Data [RFC8976]
SVCB = 64 # General-purpose service binding [RFC9460]
HTTPS = 65 # SVCB-compatible type for use with HTTP [RFC9460]
DSYNC = 66 # Endpoint discovery for delegation synchronization [RFC9859]
HHIT = 67 # Hierarchical Host Identity Tag [draft-ietf-drip-registries-28]
BRID = 68 # UAS Broadcast Remote Identification [draft-ietf-drip-registries-28]
SPF = 99 # [RFC7208]
UINFO = 100 # [IANA-Reserved]
UID = 101 # [IANA-Reserved]
GID = 102 # [IANA-Reserved]
UNSPEC = 103 # [IANA-Reserved]
NID = 104 # [RFC6742]
L32 = 105 # [RFC6742]
L64 = 106 # [RFC6742]
LP = 107 # [RFC6742]
EUI48 = 108 # an EUI-48 address [RFC7043]
EUI64 = 109 # an EUI-64 address [RFC7043]
NXNAME = 128 # NXDOMAIN indicator for Compact Denial of Existence [RFC9824]
TKEY = 249 # Transaction Key [RFC2930]
TSIG = 250 # Transaction Signature [RFC8945]
IXFR = 251 # incremental transfer [RFC1995]
AXFR = 252 # transfer of an entire zone [RFC1035] [RFC5936]
MAILB = 253 # mailbox-related RRs (MB, MG or MR) [RFC1035]
MAILA = 254 # mail agent RRs (OBSOLETE - see MX) [RFC1035]
WILDCARD = 255 # A request for some or all records the server has available [RFC1035][RFC6895] [RFC8482]
URI = 256 # URI [RFC7553]
CAA = 257 # Certification Authority Restriction [RFC8659]
AVC = 258 # Application Visibility and Control [Wolfgang_Riedel]
DOA = 259 # Digital Object Architecture [draft-durand-doa-over-dns-02]
AMTRELAY = 260 # Automatic Multicast Tunneling Relay [RFC8777]
RESINFO = 261 # Resolver Information as Key/Value Pairs [RFC9606]
WALLET = 262 # Public wallet address [Paul_Hoffman]
CLA = 263 # BP Convergence Layer Adapter [draft-johnson-dns-ipn-cla-07]
IPN = 264 # BP Node Number [draft-johnson-dns-ipn-cla-07]
TA = 32768 # DNSSEC Trust Authorities [Sam_Weiler] [Deploying DNSSEC Without a Signed Root.
# Technical Report 1999-19, Information Networking Institute, Carnegie Mellon University, April 2004.]
DLV = 32769 # DNSSEC Lookaside Validation (OBSOLETE) [RFC8749] [RFC4431]
RESEVERD_2 = 65535
class Class(Enum):
IN = 1 # the Internet
CS = 2 # the CSNET class (Obsolete - used only for examples in some obsolete RFCs)
CH = 3 # the CHAOS class
HS = 4 # Hesiod [Dyer 87]
class QueryResponse(Enum):
QUERY = 0
RESPONSE = 1
class OpCode(Enum):
QUERY = 0
IQUERY = 1
STATUS = 2
class RCode(Enum):
NO_ERROR = 0
FORMAT_ERROR = 1
SERVER_FAILURE = 2
NAME_ERROR = 3
NOT_IMPLEMENTED = 4
REFUSED = 5
def read_labels(message: BytesIO) -> list[bytes]:
labels: list[bytes] = []
while (length := message.read(1)[0]) != 0:
if length & 0xc0 == 0xc0:
offset = (length & 0x3f_ff << 8) + message.read(1)[0]
current_pos = message.tell()
message.seek(offset)
while (length := message.read(1)[0]) != 0:
labels.append(message.read(length))
message.seek(current_pos)
break
labels.append(message.read(length))
return labels
def read_type(message: BytesIO) -> Type:
return Type(struct.unpack('!H', message.read(2))[0])
def read_class(message: BytesIO) -> Class:
return Class(struct.unpack('!H', message.read(2))[0])
@dataclass(slots=True)
class Header:
class Flags(Flag):
RA = auto() # recursion available
RD = auto() # recursion desired
TC = auto() # truncation
AA = auto() # authoritative answer
id: int
qr: QueryResponse # query/answer (0=query, 1=answer)
op_code: OpCode
flags: Flags
z: int # zero
r_code: RCode # response code
qd_count: int # question count
an_count: int # answer count
ns_count: int # authority/nameserver count
ar_count: int # additionals count
@staticmethod
def from_bytes(message: BytesIO) -> Header:
# 1 1 1 1 1 1
# 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 |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
msg_id, flags_1, flags_2, qd_count, an_count, ns_count, ar_count = struct.unpack('!H2B4H', message.read(12))
return Header(
id=msg_id,
qr=QueryResponse(flags_1 >> 7),
op_code=OpCode((flags_1 & 0x78) >> 3),
flags=Header.Flags(((flags_1 & 0x5) << 1) + ((flags_2 & 0x80) >> 7)),
z=(flags_2 & 0x70) >> 4,
r_code=RCode(flags_2 & 0xf),
qd_count=qd_count,
an_count=an_count,
ns_count=ns_count,
ar_count=ar_count)
def to_bytes(self) -> bytes:
return struct.pack(
'!HBB4H',
self.id,
(self.qr.value << 7) + (self.op_code.value << 3) + (self.flags.value >> 1),
((self.flags.value & 1) << 7) + (self.z << 4) + self.r_code.value,
self.qd_count, self.an_count, self.ns_count, self.ar_count)
def dig_repr(self) -> str:
flags = (['qr'] if self.qr.value else []) + (
self.flags.name.lower().split('|') if self.flags.name else [])[::-1]
return (
f';; ->>HEADER<<- opcode: {self.op_code.name}, status: {self.r_code.name}, id: {self.id}\n'
f';; flags: {" ".join(flags)}; QUERY: {self.qd_count}, ANSWER: {self.an_count}'
f', AUTHORITY: {self.ns_count}, ADDITIONAL: {self.ar_count}\n')
@dataclass(slots=True)
class Question:
qname: list[bytes]
qtype: Type
qclass: Class
@staticmethod
def from_bytes(message: BytesIO) -> Question:
# 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 |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
qname = read_labels(message)
qtype = read_type(message)
qclass = read_class(message)
return Question(qname=qname, qtype=qtype, qclass=qclass)
def to_bytes(self) -> bytes:
result: list[bytes] = []
for name in self.qname:
result.extend([struct.pack('!B', len(name)), name])
result.extend([b'\0', struct.pack('!H', self.qtype.value), struct.pack('!H', self.qclass.value)])
return b''.join(result)
def dig_repr(self) -> str:
return f';{b".".join([*self.qname, b""]).decode():<31}{self.qclass.name:<8}{self.qtype.name}\n'
@dataclass(slots=True)
class RessourceBase:
rname: list[bytes]
rtype: Type
@classmethod
def from_bytes(cls, message: BytesIO) -> RessourceBase:
rname = read_labels(message)
rtype = read_type(message)
return RessourceBase(rname=rname, rtype=rtype)
def dig_repr(self) -> str:
return f'{b".".join([*self.rname, b""]).decode():<40}{self.rtype.name:<8}\n'
@dataclass(slots=True)
class OPTRessource(RessourceBase):
class Flags(Flag):
DO = auto()
@dataclass(slots=True)
class Data:
# Data format
# +0 (MSB) +1 (LSB)
# +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
# 0: | OPTION-CODE |
# +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
# 2: | OPTION-LENGTH |
# +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
# 4: | |
# / OPTION-DATA /
# / /
# +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
# From https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#server-cookie-methods
class OptionCode(Enum):
RESERVED_1 = 0 # [RFC6891]
LLQ = 1 # [RFC8764]
UPDATE_LEASE = 2 # [RFC9664]
NSID = 3 # [RFC5001]
RESERVED_2 = 4 # [draft-cheshire-edns0-owner-option-00]
DAU = 5 # [RFC6975]
DHU = 6 # [RFC6975]
N3U = 7 # [RFC6975]
EDNS_CLIENT_SUBNET = 8 # [RFC7871]
EDNS_EXPIRE = 9 # [RFC7314]
COOKIE = 10 # [RFC7873]
EDNS_TCP_KEEPALIVE = 11 # [RFC7828]
PADDING = 12 # [RFC7830]
CHAIN = 13 # [RFC7901]
EDNS_KEY_TAG = 14 # [RFC8145]
EXTENDED_DNS_ERROR = 15 # [RFC8914]
EDNS_CLIENT_TAG = 16 # [draft-bellis-dnsop-edns-tags-01]
EDNS_SERVER_TAG = 17 # [draft-bellis-dnsop-edns-tags-01]
REPORT_CHANNEL = 18 # [RFC9567]
ZONEVERSION = 19 # [RFC9660]
MQTYPE_QUERY = 20 # [draft-ietf-dnssd-multi-qtypes-07]
MQTYPE_RESPONSE = 21 # [draft-ietf-dnssd-multi-qtypes-07]
# [https://developer.cisco.com/docs/cloud-security/#!integrating-network-devices/rdata-description]
# [Cisco_CIE_DNS_team]
UMBRELLA_IDENT = 20292
# [https://developer.cisco.com/docs/cloud-security/#!network-devices-getting-started/response-codes]
# [Cisco_CIE_DNS_team]
DEVICEID = 26946
option_code: OptionCode
option_length: int
option_data: bytes
def dig_repr(self) -> str:
return f'; {self.option_code.name}: {self.option_data.hex()}'
# From RFC 6891 (6.1.2 - Wire Format)
# +------------+--------------+------------------------------+
# | Field Name | Field Type | Description |
# +------------+--------------+------------------------------+
# | NAME | domain name | MUST be 0 (root domain) |
# | TYPE | u_int16_t | OPT (41) |
# | CLASS | u_int16_t | requestor's UDP payload size |
# | TTL | u_int32_t | extended RCODE and flags |
# | RDLEN | u_int16_t | length of all RDATA |
# | RDATA | octet stream | {attribute,value} pairs |
# +------------+--------------+------------------------------+
udp_payload: int
flags: Flags
extended_rcode: int
version: int
data_length: int
data: Data
@classmethod
def from_bytes(cls, message: BytesIO) -> OPTRessource:
base_ressource = super(OPTRessource, cls).from_bytes(message)
# Extended RCODE and flags (TTL's 32 bits)
# +0 (MSB) +1 (LSB)
# +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
# 0: | EXTENDED-RCODE | VERSION |
# +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
# 2: | DO| Z |
# +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
udp_payload, extended_rcode, version, z, data_length = struct.unpack('!H2B2H', message.read(8))
flags = OPTRessource.Flags(z >> 15)
z &= 0x7fff
option_code, option_length = struct.unpack('!2H', message.read(4))
option_data = message.read(option_length)
return OPTRessource(rname=base_ressource.rname, rtype=base_ressource.rtype, udp_payload=udp_payload,
flags=flags, extended_rcode=extended_rcode, version=version, data_length=data_length,
data=OPTRessource.Data(option_code=OPTRessource.Data.OptionCode(option_code),
option_length=option_length, option_data=option_data))
def dig_repr(self) -> str:
return (f'; EDNS: version: {self.version}'
f', flags: {self.flags.name.lower().replace("|", " ") if self.flags.name else ""}'
f'; udp: {self.udp_payload}\n{self.data.dig_repr()}')
@dataclass(slots=True)
class Ressource(RessourceBase):
rclass: Class
ttl: int
rd_length: int
rdata: Any
@classmethod
def from_bytes(cls, message: BytesIO) -> Ressource:
# 1 1 1 1 1 1
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | |
# / /
# / NAME /
# | |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | TYPE |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | CLASS |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | TTL |
# | |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
# | RDLENGTH |
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
# / RDATA /
# / /
# +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
base_ressource = super(Ressource, cls).from_bytes(message)
rclass = read_class(message)
ttl = struct.unpack('!I', message.read(4))[0]
rd_length = struct.unpack('!H', message.read(2))[0]
match base_ressource.rtype:
case Type.A:
assert rd_length == 4, f'Mismatch rd_length: expected 4 from type A, got {rd_length}'
rdata = struct.unpack('!4B', message.read(4))
case Type.NS:
rdata = read_labels(message)
case _:
rdata = message.read(rd_length)
return Ressource(rname=base_ressource.rname, rtype=base_ressource.rtype, rclass=rclass, ttl=ttl,
rd_length=rd_length, rdata=rdata)
def dig_repr(self) -> str:
match self.rtype:
case Type.A:
rdata = '.'.join([str(v) for v in self.rdata])
case Type.NS:
rdata = b'.'.join(self.rdata).decode()
case _:
rdata = self.rdata
return f'{b".".join(self.rname).decode():<24}{self.ttl:<8}{self.rclass.name:<8}{self.rtype.name:<8}{rdata}\n'
def additional_ressource_from_bytes(message: BytesIO) -> Ressource | OPTRessource:
current_pos = message.tell()
base = RessourceBase.from_bytes(message)
message.seek(current_pos)
if base.rtype == Type.OPT:
return OPTRessource.from_bytes(message)
return Ressource.from_bytes(message)
@dataclass(slots=True)
class Message:
header: Header
question: list[Question]
answer: list[Ressource]
authority: list[Ressource]
additional: list[Ressource | OPTRessource]
@staticmethod
def from_bytes(message: BytesIO) -> Message:
header = Header.from_bytes(message)
questions: list[Question] = [Question.from_bytes(message) for _ in range(header.qd_count)]
answers: list[Ressource] = [Ressource.from_bytes(message) for _ in range(header.an_count)]
authorities: list[Ressource] = [Ressource.from_bytes(message) for _ in range(header.ns_count)]
additionals: list[Ressource | OPTRessource] = [
additional_ressource_from_bytes(message) for _ in range(header.ar_count)]
return Message(
header=header, question=questions, answer=answers, authority=authorities, additional=additionals)
def to_bytes(self) -> bytes:
return b''.join([section.to_bytes() for section in [
self.header, *self.question, *self.answer, *self.authority, *self.additional]])
def dig_repr(self) -> str:
sections: list[str] = [f'{self.header.dig_repr()}']
opt_section: list[OPTRessource] = []
additional_section: list[Ressource] = []
for res in self.additional:
if isinstance(res, OPTRessource):
opt_section.append(res)
else:
additional_section.append(res)
if opt_section:
sections.append(f';; OPT PSEUDOSECTION:\n{"".join([a.dig_repr() for a in opt_section])}')
if self.question:
sections.append(f';; QUESTION SECTION:\n{"".join([q.dig_repr() for q in self.question])}')
if self.answer:
sections.append(f';; ANSWER SECTION:\n{"".join([a.dig_repr() for a in self.answer])}')
if self.authority:
sections.append(f';; AUTHORITY SECTION:\n{"".join([a.dig_repr() for a in self.authority])}')
if self.additional:
sections.append(f';; ADDITIONAL SECTION:\n{"".join([a.dig_repr() for a in additional_section])}')
return '\n'.join(sections)
def main():
parser = ArgumentParser()
parser.add_argument('nameserver')
parser.add_argument('url')
parser.add_argument('--port', type=int, default=53)
arguments = parser.parse_args()
nameserver: str = arguments.nameserver
url: str = arguments.url
port: int = arguments.port
message = Message(
header=Header(id=0x1234, qr=QueryResponse.QUERY, op_code=OpCode.QUERY, flags=Header.Flags.RD | Header.Flags.RA,
z=0, r_code=RCode.NO_ERROR, qd_count=1, an_count=0, ns_count=0, ar_count=0),
question=[Question(qname=url.encode().split(b'.'), qtype=Type.A, qclass=Class.IN)],
answer=[], authority=[], additional=[])
print('\n;; Sending:')
print(message.dig_repr())
print(f';; Query Size: {len(message.to_bytes())}\n')
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp_socket:
query_time = time.monotonic()
udp_socket.sendto(message.to_bytes(), (nameserver, port))
response_message, _ = udp_socket.recvfrom(4096)
response_time = time.monotonic()
response = Message.from_bytes(BytesIO(response_message))
print(';; Got answer:')
print(response.dig_repr())
print(f';; Query time: {int((response_time - query_time) * 1000)} msec\n'
f';; SERVER: {nameserver}#{port}({nameserver}) (UDP)\n'
f';; WHEN: {datetime.now(datetime.now().astimezone().tzinfo).strftime("%a %b %d %H:%M:%S %Z %Y")}\n'
f';; MSG SIZE rcvd: {len(response_message)}\n')
if __name__ == '__main__':
main()

45
pyproject.toml Normal file
View file

@ -0,0 +1,45 @@
[project]
name = "pydig"
version = "0.0.1"
requires-python = ">=3.10"
[tool.ruff]
cache-dir = "/tmp/ruff"
exclude = [
".git",
".ruff_cache",
".venv"
]
line-length = 120
indent-width = 4
target-version = "py310"
[tool.isort]
combine_as_imports = true
force_sort_within_sections = true
lexicographical = true
lines_after_imports = 2
multi_line_output = 4
no_sections = false
order_by_type = true
[tool.ruff.lint]
preview = true
select = ["A", "ARG", "B", "C", "E", "F", "FURB", "G", "I","ICN", "ISC", "PERF", "PIE", "PL", "PLE", "PTH",
"Q", "RET", "RSE", "RUF", "SLF", "SIM", "T20", "TCH", "UP", "W"]
ignore = ["E275", "FURB140", "I001", "PERF203", "PLR2004", "RET502", "RET503", "SIM105", "T201"]
[tool.ruff.lint.flake8-quotes]
inline-quotes = "single"
[tool.ruff.lint.pylint]
max-args=16
max-branches=24
max-locals=16
max-nested-blocks=8
max-public-methods=16
max-returns=8
max-statements=96
[tool.ruff.lint.mccabe]
max-complexity = 20