Initial commit
This commit is contained in:
commit
a061edf3a7
3 changed files with 629 additions and 0 deletions
13
README.md
Normal file
13
README.md
Normal 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
571
dig.py
Normal 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
45
pyproject.toml
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue