diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b17d89 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# Virtual environment +env/ +venv/ + +# Distribution / packaging +*.egg-info/ +dist/ +build/ + +# IDE settings +.idea/ +.vscode/ +*.swp diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5a8e332 --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..256704b --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# Deem and Nets + +**Deem and Nets** is a primitive DNS implementation. The name was chosen at random, you could see it in multiple ways: +- Say deem and nets fast and it sounds like DNS +- See it as "Demand nets" since DNS places several demands on networks + +You can choose it means whatever. + +## Features + +- Parses the DNS message header (including flags and counts). +- Server provides structured output to help visualize DNS query components. +- Serves as a foundation for future expansions, such as resolving DNS queries and supporting various DNS record types. + +## Directory Structure + +``` +deem-and-nets +├── README.md # Project documentation +├── requirements.txt # Python dependencies +└── src/ # Source code + ├── message/ + │   ├── builder.py # Future DNS message builder + │   ├── parser.py # Current DNS message parser + ├── records/ + │   └── types.py # DNS record types (e.g., A, CNAME, etc.) + ├── resolver/ + │   ├── cache.py # Future caching mechanism + │   └── resolver.py # Future DNS resolver logic + └── server.py # UDP DNS server +``` + +## Getting Started + +### Prerequisites + +- Python 3.12+ (as indicated by the virtual environment) +- `pip` for installing dependencies + +### Installation + +1. Clone the repository: + +```bash +git clone https://github.com/pindjouf/deem-and-nets.git +cd deem-and-nets +``` + +2. Set up a virtual environment: + +```bash +python3 -m venv env +source env/bin/activate +pip install -r requirements.txt +``` + +### Usage + +1. Start the UDP server: + +```bash +python src/server.py +``` + +2. Send a DNS query to the server: + +```bash +dig @127.0.0.1 -p 53053 example.com +``` + +or + +```bash +nslookup -port=53053 -querytype=A example.com 127.0.0.1 +``` + +I recommend `nslookup` since it provides valid flags, unlike `dig`. + +3. Analyze the parsed DNS header output. + +### Current Limitations + +- Only parses the DNS message header. +- No support for response generation or DNS record resolution yet. + +## Roadmap + +- Add DNS response generation. +- Implement caching for better performance. +- Expand support for DNS record types (A, AAAA, CNAME, etc.). +- Implement advanced features like recursion and authoritative responses. + +## Contributing + +Contributions are welcome! Feel free to open issues or submit pull requests. + +## License + +This project is licensed under the WTFPL License - see the [LICENSE](LICENSE) file for details. diff --git a/docs/protocol.md b/docs/protocol.md new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..572b352 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pydantic diff --git a/src/message/builder.py b/src/message/builder.py new file mode 100644 index 0000000..915faa8 --- /dev/null +++ b/src/message/builder.py @@ -0,0 +1,2 @@ +query1 = "30940120000100000000000106676f6f676c6503636f6d000001000100002904d000000000000c000a0008ac27df40c4bdd64d" +query2 = "39140120000100000000000106676f6f676c6503636f6d000001000100002904d000000000000c000a00081852df91ed3d89be" diff --git a/src/message/parser.py b/src/message/parser.py new file mode 100644 index 0000000..d1597eb --- /dev/null +++ b/src/message/parser.py @@ -0,0 +1,145 @@ +from pydantic import BaseModel +from typing import List + +class DomainLabel(BaseModel): + length: int + value: str + +class Flags(BaseModel): + qr: int + opcode: int + authoritative_answer: int + truncation: int + recursion_desired: int + recursion_available: int + zero: int + response_code: int + +class Header(BaseModel): + id: bytes + flags: Flags + question_count: int + answer_count: int + authority_count: int + additional_records_count: int + +class Question(BaseModel): + labels: List[DomainLabel] + zero_byte_terminator: int + q_type: int + q_class: int + +class Query(BaseModel): + header: Header + question: Question + +class OpcodeToken: + def get_token(self, flags: str) -> dict: + bin_opcode = "0b" + flags + opcode_value = int(bin_opcode, 2) + + opcode_token = "" + match opcode_value: + case 0: + opcode_token = "Standard query" + case 1: + opcode_token = "Inverse query" + case 2: + opcode_token = "Server status request" + case 3: + opcode_token = "Reserved" + + return { 'value': opcode_value, 'meaning': opcode_token } + +class rCodeToken: + def get_token(self, flags: str) -> dict: + bin_rcode = "0b" + flags + rcode_value = int(bin_rcode, 2) + + rcode_token = "" + match rcode_value: + case 0: + rcode_token = "Success" + case 1: + rcode_token = "Format specification error" + case 2: + rcode_token = "Server Failure" + case 3: + rcode_token = "Query does not exist in domain" + case 4: + rcode_token = "Type is not supported by the server" + case 5: + rcode_token = "Nonexecution of queries by server due to policy reasons" + + return { 'value': rcode_value, 'meaning': rcode_token } + +def hex_to_bin(hex_string: str) -> str: + return bin(int(hex_string, 16))[2:].zfill(16) + +def parse_flags(flags: str) -> dict: + opcode = OpcodeToken() + rcode = rCodeToken() + # pseudo-index flags: 0 1234 5 6 7 8 910 1234 + # Example flags: 0 0000 0 0 1 0 010 0000 + data = { + 'qr': {'value': 1, 'meaning': "response"} if int(flags[0]) == 1 else {'value': 0, 'meaning': "request"}, + 'opcode': opcode.get_token(flags[1:5]), + 'authoritative_answer': {'value': 1, 'meaning': "authoritative"} if int(flags[5]) == 1 else {'value': 0, 'meaning': "non-authoritative"}, + 'truncation': {'value': 1, 'meaning': "Truncated"} if int(flags[6]) == 1 else {'value': 0, 'meaning': "Not truncated"}, + 'recursion_desired': {'value': 1, 'meaning': "Recursion requested"} if int(flags[7]) == 1 else {'value': 0, 'meaning': "Recursion not requested"}, + 'recursion_available': {'value': 1, 'meaning': "Recursion available"} if int(flags[8]) == 1 else {'value': 0, 'meaning': "Recursion not available"}, + 'zero': {'value': 0, 'meaning': "Reserved"} if int(flags[9:12], 2) == 0 else {'value': int(flags[9:12], 2), 'meaning': "Error (should be zero)"}, + 'rcode': rcode.get_token(flags[12:16]) + } + + return data + +def pretty_print(header: Header, tokenized_flags, binary_flags, query): + print("\nDNS Query Analysis") + print("=" * 60) + print(f"Base Query: {query}") + print("\nDNS Header") + print("=" * 60) + print(f"Transaction ID: 0x{header.id.hex()} (bytes: {header.id})") + print("=" * 60) + print("Counts:") + print(f" Questions: {header.question_count}") + print(f" Answers: {header.answer_count}") + print(f" Authority RRs: {header.authority_count}") + print(f" Additional RRs: {header.additional_records_count}") + print("=" * 60) + print("") + print("Tokenized Flags:") + print("=" * 60) + print(f"Binary Flags: {binary_flags}") + + for key, value in tokenized_flags.items(): + print(f"{key.replace('_', ' ').title():20}: {value['value']} - {value['meaning']}") + + print("=" * 60) + +def parser(query: str): + flags_substring = query[4:8] + binary_flags = hex_to_bin(flags_substring) + tokenized_flags = parse_flags(binary_flags) + + flags = Flags( + qr=tokenized_flags['qr']['value'], + opcode=tokenized_flags['opcode']['value'], + authoritative_answer=tokenized_flags['authoritative_answer']['value'], + truncation=tokenized_flags['truncation']['value'], + recursion_desired=tokenized_flags['recursion_desired']['value'], + recursion_available=tokenized_flags['recursion_available']['value'], + zero=tokenized_flags['zero']['value'], + response_code=tokenized_flags['rcode']['value'] + ) + header = Header( + id=bytes.fromhex(query[0:4]), + flags=flags, + question_count=int(query[8:12], 16), + answer_count=int(query[12:16], 16), + authority_count=int(query[16:20], 16), + additional_records_count=int(query[20:24], 16) + ) + + pretty_print(header, tokenized_flags, binary_flags, query) diff --git a/src/records/types.py b/src/records/types.py new file mode 100644 index 0000000..e69de29 diff --git a/src/resolver/cache.py b/src/resolver/cache.py new file mode 100644 index 0000000..e69de29 diff --git a/src/resolver/resolver.py b/src/resolver/resolver.py new file mode 100644 index 0000000..e69de29 diff --git a/src/server.py b/src/server.py new file mode 100644 index 0000000..da994a5 --- /dev/null +++ b/src/server.py @@ -0,0 +1,10 @@ +import socket +from message import parser + +server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +server_socket.bind(("127.0.0.1", 53053)) + +while True: + (client_socket, address) = server_socket.recvfrom(1024) + parser.parser(client_socket.hex()) + print()