commit a061edf3a7e9f1d5ba10e3a485feb3aee40b1539 Author: Corentin Date: Fri Dec 26 18:43:44 2025 +0900 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..8541691 --- /dev/null +++ b/README.md @@ -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 +``` + diff --git a/dig.py b/dig.py new file mode 100644 index 0000000..b308b78 --- /dev/null +++ b/dig.py @@ -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() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2dacaf4 --- /dev/null +++ b/pyproject.toml @@ -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