From c22941a8e9ba24403d139f3387089a7a0093f21d Mon Sep 17 00:00:00 2001 From: Alexandre Plateau Date: Wed, 20 Jan 2021 19:39:44 +0100 Subject: [PATCH 1/9] starting a rewrite to add a configuration file --- .gitignore | 2 + README.md | 4 +- client.sh | 27 +++++++--- config.ini.example | 11 ++++ src/chatserver.py | 13 +++-- src/client.py | 1 - src/packet.py | 50 +++++++++++++++-- src/server.py | 131 ++++++++++++++++++++++++++++++++++----------- src/utils.py | 14 +++++ 9 files changed, 201 insertions(+), 52 deletions(-) create mode 100644 config.ini.example diff --git a/.gitignore b/.gitignore index d444115..fdee732 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ __pycache__/ *.py[ouc] .vscode/ venv/ + +server.ini diff --git a/README.md b/README.md index 3978e15..9ddd007 100644 --- a/README.md +++ b/README.md @@ -66,10 +66,8 @@ In a few steps I was able to configure my NS provider to set myself up as my own For this examples, let's say my server is named `example.com`. -1. In my DNS Zone, I added a `NS` entry for `dns.example.com`, pointing to `dns.example.com.` (mind the final dot) -1. Then I added a `A` entry for `dns.example.com`, pointing to `my server ip here` +1. I added a `A` entry for `dns.example.com`, pointing to `my server ip here` 1. In the DNS servers configuration, I already had things like `ns1.provider.com`, I added myself as a DNS server: `dns.example.com`, pointing to `my server ip here` -1. For the redirections, I added `dns.example.com` as a type `A`, pointing to `my server ip here` 1. Then, just wait a bit (can be as long as 48 hours) and you're good to go Now I just have to tell my client scripts to use the domain `dns.example.com` to send messages to it and it works like a charm, even when asking Google about it! diff --git a/client.sh b/client.sh index 3240ffc..6177bd1 100644 --- a/client.sh +++ b/client.sh @@ -1,13 +1,23 @@ #!/usr/bin/env bash -# send message -stripped_b32=`echo $1 | base32 | tr -d =` -crafted_domain="${stripped_b32}.dns.12f.pl" -answer=`dig @12f.pl $crafted_domain TXT` +if [[ $# != 2 ]]; then + echo "Usage:" + echo " $0 hostname message" + exit 1 +fi + +StartDate=`date -u +"%s.%N"` + +# create message, remove padding +stripped_b32=`echo $2 | base32 | tr -d =` +# create domain +crafted_domain="${stripped_b32}.$1" +# make the DNS query and retrieve the answers +answer=`dig $crafted_domain TXT` # decode answer message=`echo $answer | grep -A 1 ";; ANSWER SECTION:" | tail -n 1 | egrep -o "\".+\"" | cut -c 2- | rev | cut -c 2- | rev` length=$((4 - $(expr length "$message") % 4)) -# add padding accordingly +# add padding back accordingly case "$length" in "1") message="${message}=" @@ -21,7 +31,10 @@ case "$length" in *) ;; esac - +# decode decoded=`echo $message | base64 -d` -echo "Received: $decoded" \ No newline at end of file +FinalDate=`date -u +"%s.%N"` +elapsed=`date -u -d "0 $FinalDate sec - $StartDate sec" +"%S.%N"` +echo "Received in $elapsed seconds" +echo $decoded \ No newline at end of file diff --git a/config.ini.example b/config.ini.example new file mode 100644 index 0000000..2c16935 --- /dev/null +++ b/config.ini.example @@ -0,0 +1,11 @@ +[server] +interface= +domain= +host_ip= + +[packets] +ttl=60 + +[domain.site.com] +ip= +ttl=3600 \ No newline at end of file diff --git a/src/chatserver.py b/src/chatserver.py index 6b8dafb..e2fe0ff 100644 --- a/src/chatserver.py +++ b/src/chatserver.py @@ -2,6 +2,7 @@ import sys import time +from typing import List from server import Server from utils import get_ip_from_hostname @@ -16,13 +17,13 @@ def __init__(self, author: str, content: str): class ChatServer(Server): - def __init__(self, interface: str, domain: str, host_ip: str): - super().__init__(interface, domain, host_ip) + def __init__(self, *args): + super().__init__(*args) self.messages = [] self.users = {} - def on_query(self, message: str, src_ip: str) -> str: + def on_query(self, message: str, src_ip: str, domains: List[str]) -> str: message = message.strip() if src_ip not in self.users: @@ -37,7 +38,8 @@ def on_query(self, message: str, src_ip: str) -> str: elif message == "/consult": # get the user unread messages list history = [] - for msg in self.messages: + # get the last 5 messages in reverse order + for msg in self.messages[:-6:-1]: if self.users[src_ip] not in msg.seen_by: history.append(msg) # mark the message as seen @@ -45,7 +47,8 @@ def on_query(self, message: str, src_ip: str) -> str: # create the unread message list output = "" - for msg in history: + # append message in ascending order + for msg in history[::-1]: output += f"@{msg.author} [{msg.timestamp}]: {msg.content}\n" return output return "/error" diff --git a/src/client.py b/src/client.py index c7f486c..1a771ed 100644 --- a/src/client.py +++ b/src/client.py @@ -14,7 +14,6 @@ logger = None -import base64 class Client: def __init__(self, domain: str, ip: str, verbosity: int = 0): diff --git a/src/packet.py b/src/packet.py index d8bebbf..ad7cd90 100644 --- a/src/packet.py +++ b/src/packet.py @@ -10,14 +10,46 @@ from utils import DNSHeaders +def build_tos(precedence: int, lowdelay: bool, throughput: bool, reliability: bool, lowcost: bool) -> int: + """Building IP Type of Service value + + Args: + precedence (int): intended to denote the importance or priority of the datagram + 0b1000 -- minimize delay + 0b0100 -- maximize throughput + 0b0010 -- maximize reliability + 0b0001 -- minimize monetary cost + 0b0000 -- normal service + lowdelay (bool): low (True), normal (False) + throughput (bool): high (True) or low (False) + reliability (bool): high (True) or normal (False) + lowcost (bool): minimize memory cost (True) + + Returns: + int: type of service as describe in the RFC 1349 and 791 + """ + return (lowcost << 1) + (reliability << 2) + (throughput << 3) + \ + (lowdelay << 4) + (max(min(precedence, 0b111), 0b000) << 5) + + class Packet: @staticmethod def build_query(layer: dict, domain: str) -> object: - pkt = IP(dst=layer["dst"], tos=0x28) + """Build a DNS query packet + + Args: + layer (dict): dict of the different layer properties and values + domain (str): the domain the packet is from + + Returns: + object: a Packet object + """ + pkt = IP(dst=layer["dst"], tos=build_tos(1, 0, 1, 0, 0)) pkt /= UDP(sport=randint(0, 2 ** 16 - 1), dport=53) pkt /= DNS( + # random transaction id id=randint(0, 2 ** 16 - 1), - rd=0, # no recursion desired + rd=1, # recursion desired qr=DNSHeaders.QR.Query, # requests must be of type TXT otherwise our answers (of type TXT) # don't get transmitted if recursion occured @@ -28,6 +60,15 @@ def build_query(layer: dict, domain: str) -> object: @staticmethod def build_reply(layer: dict, domain: str) -> object: + """Build a DNS reply packet + + Args: + layer (dict): dict of the different layer properties and values + domain (str): the domain the packet is from + + Returns: + object: a Packet object + """ pkt = IP(dst=layer["dst"], src=layer["src"]) pkt /= UDP(dport=layer["dport"], sport=53) pkt /= DNS( @@ -44,14 +85,15 @@ def __init__(self, pkt: IP, domain: str): self._pkt = pkt self._domain = domain - def is_valid_dnsquery(self) -> bool: + def is_valid_dnsquery(self, qtype: int) -> bool: def check_qname(q: str) -> str: - return q[len(q) - len(self._domain) - 2 : len(q) - 2] + return q[len(q) - len(self._domain) - 2: len(q) - 2] return ( DNS in self._pkt and self._pkt[DNS].opcode == DNSHeaders.OpCode.StdQuery and self._pkt[DNS].qr == DNSHeaders.QR.Query + and self._pkt[DNSQR].type == qtype and check_qname(self._pkt[DNSQR].qname.decode("utf-8")) and self._pkt[DNS].ancount == 0 ) diff --git a/src/server.py b/src/server.py index 17a2530..888ecea 100644 --- a/src/server.py +++ b/src/server.py @@ -4,6 +4,8 @@ import socket import sys import threading +from configparser import ConfigParser +from typing import List from scapy.layers.dns import DNS, DNSQR, DNSRR from scapy.layers.inet import IP, UDP @@ -15,40 +17,82 @@ def socket_server(ip: str): + # bind UDP socket to port 53 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.bind((ip, 53)) + # and read until the end of the world while True: s.recvfrom(1024) s.close() class Server: - def __init__(self, interface: str, domain: str, host_ip: str): - self.interface = interface - self.host_ip = host_ip + @staticmethod + def from_file(filename: str): + config = ConfigParser() + config.read(filename) + + return Server( + config['server']['interface'], + config['server']['domain'], + config['server']['host_ip'], + config + ) + + def __init__(self, iface: str, domain: str, ip: str, config: ConfigParser = None): + self.interface = iface + self.host_ip = ip self.domain = domain + self.config = config + self.logger = init_logger() - def on_query(self, message: str, src_ip: str) -> str: + def on_query(self, message: str, src_ip: str, domains: List[str]) -> str: return "test" - def dns_responder(self, pkt: IP): - packet = Packet(pkt, self.domain) - - if packet.is_valid_dnsquery(): - self.logger.info("got a packet from %s:%i", packet.src, packet.sport) + def _make_message(self, qname: str, content: str) -> DNSRR: + return DNSRR( + rrname=qname, + rdata=Content.encode(content), + type=DNSAnswer.Type.Text, + ttl=self.config["packets"]["ttl"] if self.config else 60, + ) - subdomain = packet.subdomain_from_qname.split('.')[0] - self.logger.debug("subdomain: %s", subdomain) + def _make_txt(self, packet: Packet) -> Packet: + try: + subdomain, *domains = packet.subdomain_from_qname.split('.') + data = Domain.decode(subdomain) + except binascii.Error: + # couldn't decode, drop the packet and do nothing + return + + return Packet.build_reply( + { + "src": self.host_ip, + "dst": packet.src, + "dport": packet.sport, + "dns": { + "id": packet.id, + "question": packet.question, + "messages": [ + self._make_message( + packet.qname, + self.on_query(data, packet.src, domains) + ), + ], + }, + }, + self.domain, + ) - try: - data = Domain.decode(subdomain) - except binascii.Error: - # couldn't decode, drop the packet and do nothing - return + def _make_a(self, packet: Packet) -> Packet: + if self.config is None: + return - # keep destination - answer = Packet.build_reply( + # if we receive a DNS A query for a subdomain, answer it with an ip from + # the configuration file + if packet.qname in self.config.sections(): + return Packet.build_reply( { "src": self.host_ip, "dst": packet.src, @@ -56,13 +100,12 @@ def dns_responder(self, pkt: IP): "dns": { "id": packet.id, "question": packet.question, - # TODO ensure that we're under the 500 bytes limit "messages": [ DNSRR( rrname=packet.qname, - rdata=Content.encode(self.on_query(data, packet.src)), - type=DNSAnswer.Type.Text, - ttl=60, # a minute long + rdata=self.config[packet.qname]["ip"], + type=DNSAnswer.Type.HostAddr, + ttl=self.config[packet.qname]["ttl"], ), ], }, @@ -70,7 +113,25 @@ def dns_responder(self, pkt: IP): self.domain, ) - self.logger.debug("Answering %s", answer.dns.summary()) + def _dns_responder(self, pkt: IP): + packet = Packet(pkt, self.domain) + answer = None + + self.logger.info( + "[DNS %s] Source %s:%i - on %s", + DNSHeaders.Type.to_str(packet.question.type), + packet.src, + packet.sport, + packet.qname + ) + + # reject every packet which isn't a DNS TXT query + if packet.is_valid_dnsquery(DNSHeaders.Type.Text): + answer = self._make_txt(packet) + elif packet.is_valid_dnsquery(DNSHeaders.Type.HostAddr): + answer = self._make_a(packet) + + if answer is not None: send(answer.packet, verbose=0, iface=self.interface) def run(self): @@ -80,10 +141,10 @@ def run(self): t = threading.Thread(target=socket_server, args=(self.host_ip, )) t.start() - self.logger.info(f"DNS responder started on {self.host_ip}:53") + self.logger.info(f"DNS sniffer started on {self.host_ip}:53") sniff( filter=f"udp port 53 and ip dst {self.host_ip}", - prn=self.dns_responder, + prn=self._dns_responder, iface=self.interface, ) @@ -91,13 +152,19 @@ def run(self): if __name__ == "__main__": - if len(sys.argv) < 3: + if len(sys.argv) != 2 or len(sys.argv) != 3: print("Usage: %s interface hostname" % sys.argv[0]) + print(" %s config_file.ini" % sys.argv[0]) sys.exit(-1) - ip = get_ip_from_hostname(sys.argv[2]) - if ip is None: - sys.exit(-1) - - server = Server(sys.argv[1], sys.argv[2], ip) - server.run() + if len(sys.argv) == 3: + ip = get_ip_from_hostname(sys.argv[2]) + if ip is None: + print("Couldn't resolve IP from hostname, consider using a config file") + sys.exit(-1) + + server = Server(sys.argv[1], sys.argv[2], ip) + server.run() + elif len(sys.argv) == 2: + server = Server.from_file(sys.argv[1]) + server.run() diff --git a/src/utils.py b/src/utils.py index 79fee4e..25a31a0 100644 --- a/src/utils.py +++ b/src/utils.py @@ -53,6 +53,20 @@ class Type: Text = 0x0010 MailServer = 0x000F + @staticmethod + def to_str(qtype: int): + if qtype == DNSHeaders.Type.HostAddr: + return "A" + elif qtype == DNSHeaders.Type.NameServer: + return "NS" + elif qtype == DNSHeaders.Type.CName: + return "CNAME" + elif qtype == DNSHeaders.Type.Text: + return "TXT" + elif qtype == DNSHeaders.Type.MailServer: + return "MX" + return "???" + class RCode: NoErr = 0 FormatErr = 1 From 3983eb755cada91fe0bb9671c34ba59a160d8c97 Mon Sep 17 00:00:00 2001 From: Alexandre Plateau Date: Thu, 21 Jan 2021 10:09:03 +0100 Subject: [PATCH 2/9] enhancing the server class and chatserver --- src/chatserver.py | 23 +++++------------------ src/server.py | 25 +++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/chatserver.py b/src/chatserver.py index e2fe0ff..d3a8177 100644 --- a/src/chatserver.py +++ b/src/chatserver.py @@ -1,11 +1,9 @@ #!/usr/bin/env python3 -import sys import time from typing import List -from server import Server -from utils import get_ip_from_hostname +from server import main class Message: @@ -16,14 +14,12 @@ def __init__(self, author: str, content: str): self.seen_by = [] -class ChatServer(Server): - def __init__(self, *args): - super().__init__(*args) - +class ChatServer: + def __init__(self): self.messages = [] self.users = {} - def on_query(self, message: str, src_ip: str, domains: List[str]) -> str: + def __call__(self, message: str, src_ip: str, domains: List[str]) -> str: message = message.strip() if src_ip not in self.users: @@ -55,13 +51,4 @@ def on_query(self, message: str, src_ip: str, domains: List[str]) -> str: if __name__ == "__main__": - if len(sys.argv) < 3: - print("Usage: %s interface hostname" % sys.argv[0]) - sys.exit(-1) - - ip = get_ip_from_hostname(sys.argv[2]) - if ip is None: - sys.exit(-1) - - server = ChatServer(sys.argv[1], sys.argv[2], ip) - server.run() + main(chat=ChatServer()) diff --git a/src/server.py b/src/server.py index 888ecea..65bef34 100644 --- a/src/server.py +++ b/src/server.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import binascii +import os import socket import sys import threading @@ -29,6 +30,9 @@ def socket_server(ip: str): class Server: @staticmethod def from_file(filename: str): + if not os.path.exists(filename): + raise FileNotFoundError(filename) + config = ConfigParser() config.read(filename) @@ -45,9 +49,17 @@ def __init__(self, iface: str, domain: str, ip: str, config: ConfigParser = None self.domain = domain self.config = config + # subdomain => function(msg, ip, domains) + self.subservers = {} + self.logger = init_logger() + def register(self, **subservers): + self.subservers.update(subservers) + def on_query(self, message: str, src_ip: str, domains: List[str]) -> str: + if domains and self.subservers.get(domains[0]): + return self.subservers[domains[0]](message, src_ip, domains) return "test" def _make_message(self, qname: str, content: str) -> DNSRR: @@ -61,6 +73,7 @@ def _make_message(self, qname: str, content: str) -> DNSRR: def _make_txt(self, packet: Packet) -> Packet: try: subdomain, *domains = packet.subdomain_from_qname.split('.') + domains = domains[::-1] data = Domain.decode(subdomain) except binascii.Error: # couldn't decode, drop the packet and do nothing @@ -151,12 +164,14 @@ def run(self): t.join() -if __name__ == "__main__": +def main(**subservers): if len(sys.argv) != 2 or len(sys.argv) != 3: print("Usage: %s interface hostname" % sys.argv[0]) print(" %s config_file.ini" % sys.argv[0]) sys.exit(-1) + server = None + if len(sys.argv) == 3: ip = get_ip_from_hostname(sys.argv[2]) if ip is None: @@ -164,7 +179,13 @@ def run(self): sys.exit(-1) server = Server(sys.argv[1], sys.argv[2], ip) - server.run() elif len(sys.argv) == 2: server = Server.from_file(sys.argv[1]) + + if server is not None: + server.register(**subservers) server.run() + + +if __name__ == "__main__": + main() From d008ca0f2d6488ca084d9b7544cd4f3eb93c7ed0 Mon Sep 17 00:00:00 2001 From: Alexandre Plateau Date: Thu, 21 Jan 2021 04:32:19 -0500 Subject: [PATCH 3/9] fixing packet qtype checks --- .gitignore | 2 +- src/packet.py | 2 +- src/server.py | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index fdee732..ad89810 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ __pycache__/ .vscode/ venv/ -server.ini +*.ini diff --git a/src/packet.py b/src/packet.py index ad7cd90..eb605c7 100644 --- a/src/packet.py +++ b/src/packet.py @@ -93,7 +93,7 @@ def check_qname(q: str) -> str: DNS in self._pkt and self._pkt[DNS].opcode == DNSHeaders.OpCode.StdQuery and self._pkt[DNS].qr == DNSHeaders.QR.Query - and self._pkt[DNSQR].type == qtype + and self._pkt[DNSQR].qtype == qtype and check_qname(self._pkt[DNSQR].qname.decode("utf-8")) and self._pkt[DNS].ancount == 0 ) diff --git a/src/server.py b/src/server.py index 65bef34..d0ad111 100644 --- a/src/server.py +++ b/src/server.py @@ -132,7 +132,7 @@ def _dns_responder(self, pkt: IP): self.logger.info( "[DNS %s] Source %s:%i - on %s", - DNSHeaders.Type.to_str(packet.question.type), + DNSHeaders.Type.to_str(packet.question.qtype & 0b11), packet.src, packet.sport, packet.qname @@ -165,7 +165,8 @@ def run(self): def main(**subservers): - if len(sys.argv) != 2 or len(sys.argv) != 3: + if len(sys.argv) != 2 and len(sys.argv) != 3: + print(sys.argv) print("Usage: %s interface hostname" % sys.argv[0]) print(" %s config_file.ini" % sys.argv[0]) sys.exit(-1) From b2189faaaaf4b7c55a45609b4951892ec633d1e3 Mon Sep 17 00:00:00 2001 From: Alexandre Plateau Date: Thu, 21 Jan 2021 10:47:08 +0100 Subject: [PATCH 4/9] fixing code --- src/packet.py | 6 +++--- src/server.py | 8 ++++---- src/utils.py | 14 -------------- 3 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/packet.py b/src/packet.py index ad7cd90..9b09627 100644 --- a/src/packet.py +++ b/src/packet.py @@ -4,7 +4,7 @@ from functools import reduce from random import randint -from scapy.layers.dns import DNS, DNSQR, DNSRR +from scapy.layers.dns import DNS, DNSQR, DNSRR, dnstypes from scapy.layers.inet import IP, UDP from utils import DNSHeaders @@ -85,7 +85,7 @@ def __init__(self, pkt: IP, domain: str): self._pkt = pkt self._domain = domain - def is_valid_dnsquery(self, qtype: int) -> bool: + def is_valid_dnsquery(self, qtype: str) -> bool: def check_qname(q: str) -> str: return q[len(q) - len(self._domain) - 2: len(q) - 2] @@ -93,7 +93,7 @@ def check_qname(q: str) -> str: DNS in self._pkt and self._pkt[DNS].opcode == DNSHeaders.OpCode.StdQuery and self._pkt[DNS].qr == DNSHeaders.QR.Query - and self._pkt[DNSQR].type == qtype + and dnstypes[self._pkt[DNSQR].qtype] == qtype and check_qname(self._pkt[DNSQR].qname.decode("utf-8")) and self._pkt[DNS].ancount == 0 ) diff --git a/src/server.py b/src/server.py index 65bef34..d5d233e 100644 --- a/src/server.py +++ b/src/server.py @@ -8,7 +8,7 @@ from configparser import ConfigParser from typing import List -from scapy.layers.dns import DNS, DNSQR, DNSRR +from scapy.layers.dns import DNS, DNSQR, DNSRR, dnstypes from scapy.layers.inet import IP, UDP from scapy.sendrecv import send, sniff @@ -132,16 +132,16 @@ def _dns_responder(self, pkt: IP): self.logger.info( "[DNS %s] Source %s:%i - on %s", - DNSHeaders.Type.to_str(packet.question.type), + dnstypes[packet.question.qtype], packet.src, packet.sport, packet.qname ) # reject every packet which isn't a DNS TXT query - if packet.is_valid_dnsquery(DNSHeaders.Type.Text): + if packet.is_valid_dnsquery("A"): answer = self._make_txt(packet) - elif packet.is_valid_dnsquery(DNSHeaders.Type.HostAddr): + elif packet.is_valid_dnsquery("TXT"): answer = self._make_a(packet) if answer is not None: diff --git a/src/utils.py b/src/utils.py index 25a31a0..79fee4e 100644 --- a/src/utils.py +++ b/src/utils.py @@ -53,20 +53,6 @@ class Type: Text = 0x0010 MailServer = 0x000F - @staticmethod - def to_str(qtype: int): - if qtype == DNSHeaders.Type.HostAddr: - return "A" - elif qtype == DNSHeaders.Type.NameServer: - return "NS" - elif qtype == DNSHeaders.Type.CName: - return "CNAME" - elif qtype == DNSHeaders.Type.Text: - return "TXT" - elif qtype == DNSHeaders.Type.MailServer: - return "MX" - return "???" - class RCode: NoErr = 0 FormatErr = 1 From c69eeef5c0111e02ebf5a78d71e351084b59f18a Mon Sep 17 00:00:00 2001 From: Alexandre Plateau Date: Thu, 21 Jan 2021 05:40:47 -0500 Subject: [PATCH 5/9] fixing code, i interverted make_txt and make_a --- src/packet.py | 2 +- src/server.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/packet.py b/src/packet.py index 9b09627..7a72428 100644 --- a/src/packet.py +++ b/src/packet.py @@ -87,7 +87,7 @@ def __init__(self, pkt: IP, domain: str): def is_valid_dnsquery(self, qtype: str) -> bool: def check_qname(q: str) -> str: - return q[len(q) - len(self._domain) - 2: len(q) - 2] + return q.endswith(f"{self._domain}.") return ( DNS in self._pkt diff --git a/src/server.py b/src/server.py index 85fe470..92751d4 100644 --- a/src/server.py +++ b/src/server.py @@ -67,7 +67,7 @@ def _make_message(self, qname: str, content: str) -> DNSRR: rrname=qname, rdata=Content.encode(content), type=DNSAnswer.Type.Text, - ttl=self.config["packets"]["ttl"] if self.config else 60, + ttl=int(self.config["packets"]["ttl"]) if self.config else 60, ) def _make_txt(self, packet: Packet) -> Packet: @@ -77,6 +77,7 @@ def _make_txt(self, packet: Packet) -> Packet: data = Domain.decode(subdomain) except binascii.Error: # couldn't decode, drop the packet and do nothing + logger.debug("Couldn't decode subdomain in %s", packet.qname) return return Packet.build_reply( @@ -118,7 +119,7 @@ def _make_a(self, packet: Packet) -> Packet: rrname=packet.qname, rdata=self.config[packet.qname]["ip"], type=DNSAnswer.Type.HostAddr, - ttl=self.config[packet.qname]["ttl"], + ttl=int(self.config[packet.qname]["ttl"]), ), ], }, @@ -140,9 +141,9 @@ def _dns_responder(self, pkt: IP): # reject every packet which isn't a DNS TXT query if packet.is_valid_dnsquery("A"): - answer = self._make_txt(packet) - elif packet.is_valid_dnsquery("TXT"): answer = self._make_a(packet) + elif packet.is_valid_dnsquery("TXT"): + answer = self._make_txt(packet) if answer is not None: send(answer.packet, verbose=0, iface=self.interface) From d7883f163ecbf5f8963396787e2c0569c1f1fd70 Mon Sep 17 00:00:00 2001 From: Alexandre Plateau Date: Thu, 21 Jan 2021 05:52:05 -0500 Subject: [PATCH 6/9] fixing server to answer to preconfigured DNS A requests --- src/packet.py | 4 ++-- src/server.py | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/packet.py b/src/packet.py index 7a72428..5d1ffbd 100644 --- a/src/packet.py +++ b/src/packet.py @@ -85,9 +85,9 @@ def __init__(self, pkt: IP, domain: str): self._pkt = pkt self._domain = domain - def is_valid_dnsquery(self, qtype: str) -> bool: + def is_valid_dnsquery(self, qtype: str, domain: str="") -> bool: def check_qname(q: str) -> str: - return q.endswith(f"{self._domain}.") + return q.endswith(f"{domain if domain else self._domain}.") return ( DNS in self._pkt diff --git a/src/server.py b/src/server.py index 92751d4..a396f0f 100644 --- a/src/server.py +++ b/src/server.py @@ -105,7 +105,8 @@ def _make_a(self, packet: Packet) -> Packet: # if we receive a DNS A query for a subdomain, answer it with an ip from # the configuration file - if packet.qname in self.config.sections(): + qname = packet.qname[:-1] # remove final '.' + if qname in self.config.sections(): return Packet.build_reply( { "src": self.host_ip, @@ -117,9 +118,9 @@ def _make_a(self, packet: Packet) -> Packet: "messages": [ DNSRR( rrname=packet.qname, - rdata=self.config[packet.qname]["ip"], + rdata=self.config[qname]["ip"], type=DNSAnswer.Type.HostAddr, - ttl=int(self.config[packet.qname]["ttl"]), + ttl=int(self.config[qname]["ttl"]), ), ], }, @@ -139,8 +140,8 @@ def _dns_responder(self, pkt: IP): packet.qname ) - # reject every packet which isn't a DNS TXT query - if packet.is_valid_dnsquery("A"): + # reject every packet which isn't a DNS A/TXT query + if packet.is_valid_dnsquery("A", self.config["server"]["root"] if self.config else ""): answer = self._make_a(packet) elif packet.is_valid_dnsquery("TXT"): answer = self._make_txt(packet) From ad8ee1cbd082c95a06dbc0b537f7475b6cc28dfb Mon Sep 17 00:00:00 2001 From: Alexandre Plateau Date: Thu, 21 Jan 2021 06:09:57 -0500 Subject: [PATCH 7/9] linting code --- client.sh | 18 +++++++++--------- config.ini.example | 13 +++++++++---- src/chatserver.py | 2 +- src/packet.py | 15 +++++++++++---- src/server.py | 19 ++++++++++--------- 5 files changed, 40 insertions(+), 27 deletions(-) diff --git a/client.sh b/client.sh index 6177bd1..ba5a802 100644 --- a/client.sh +++ b/client.sh @@ -6,17 +6,17 @@ if [[ $# != 2 ]]; then exit 1 fi -StartDate=`date -u +"%s.%N"` +StartDate=$(date -u +"%s.%N") # create message, remove padding -stripped_b32=`echo $2 | base32 | tr -d =` +stripped_b32=$(echo "$2" | base32 | tr -d =) # create domain crafted_domain="${stripped_b32}.$1" # make the DNS query and retrieve the answers -answer=`dig $crafted_domain TXT` +answer=$(dig "$crafted_domain" TXT) # decode answer -message=`echo $answer | grep -A 1 ";; ANSWER SECTION:" | tail -n 1 | egrep -o "\".+\"" | cut -c 2- | rev | cut -c 2- | rev` -length=$((4 - $(expr length "$message") % 4)) +message=$(echo "$answer" | grep -A 1 ";; ANSWER SECTION:" | tail -n 1 | grep -E -o "\".+\"" | cut -c 2- | rev | cut -c 2- | rev) +length=$((4 - $(echo -n "$message" | wc -c) % 4)) # add padding back accordingly case "$length" in "1") @@ -32,9 +32,9 @@ case "$length" in ;; esac # decode -decoded=`echo $message | base64 -d` +decoded=$(echo $message | base64 -d) -FinalDate=`date -u +"%s.%N"` -elapsed=`date -u -d "0 $FinalDate sec - $StartDate sec" +"%S.%N"` +FinalDate=$(date -u +"%s.%N") +elapsed=$(date -u -d "0 $FinalDate sec - $StartDate sec" +"%S.%N") echo "Received in $elapsed seconds" -echo $decoded \ No newline at end of file +echo "$decoded" diff --git a/config.ini.example b/config.ini.example index 2c16935..254044d 100644 --- a/config.ini.example +++ b/config.ini.example @@ -1,11 +1,16 @@ [server] interface= -domain= +root=example.com +domain=dns.example.com host_ip= [packets] -ttl=60 +ttl=1 -[domain.site.com] +[example.com] ip= -ttl=3600 \ No newline at end of file +ttl=3600 + +[nice.example.com] +ip= +ttl=3600 diff --git a/src/chatserver.py b/src/chatserver.py index d3a8177..6ef1201 100644 --- a/src/chatserver.py +++ b/src/chatserver.py @@ -27,7 +27,7 @@ def __call__(self, message: str, src_ip: str, domains: List[str]) -> str: self.users[src_ip] = str(len(self.users)) # check for commands - if len(message) > 1 and message[0] != '/': + if len(message) > 1 and message[0] != "/": self.messages.append(Message(self.users[src_ip], message)) self.messages[-1].seen_by.append(self.users[src_ip]) return "/ok" diff --git a/src/packet.py b/src/packet.py index 5d1ffbd..c7143e3 100644 --- a/src/packet.py +++ b/src/packet.py @@ -10,7 +10,9 @@ from utils import DNSHeaders -def build_tos(precedence: int, lowdelay: bool, throughput: bool, reliability: bool, lowcost: bool) -> int: +def build_tos( + precedence: int, lowdelay: bool, throughput: bool, reliability: bool, lowcost: bool +) -> int: """Building IP Type of Service value Args: @@ -28,8 +30,13 @@ def build_tos(precedence: int, lowdelay: bool, throughput: bool, reliability: bo Returns: int: type of service as describe in the RFC 1349 and 791 """ - return (lowcost << 1) + (reliability << 2) + (throughput << 3) + \ - (lowdelay << 4) + (max(min(precedence, 0b111), 0b000) << 5) + return ( + (lowcost << 1) + + (reliability << 2) + + (throughput << 3) + + (lowdelay << 4) + + (max(min(precedence, 0b111), 0b000) << 5) + ) class Packet: @@ -85,7 +92,7 @@ def __init__(self, pkt: IP, domain: str): self._pkt = pkt self._domain = domain - def is_valid_dnsquery(self, qtype: str, domain: str="") -> bool: + def is_valid_dnsquery(self, qtype: str, domain: str = "") -> bool: def check_qname(q: str) -> str: return q.endswith(f"{domain if domain else self._domain}.") diff --git a/src/server.py b/src/server.py index a396f0f..631c95f 100644 --- a/src/server.py +++ b/src/server.py @@ -37,9 +37,9 @@ def from_file(filename: str): config.read(filename) return Server( - config['server']['interface'], - config['server']['domain'], - config['server']['host_ip'], + config["server"]["interface"], + config["server"]["domain"], + config["server"]["host_ip"], config ) @@ -72,7 +72,7 @@ def _make_message(self, qname: str, content: str) -> DNSRR: def _make_txt(self, packet: Packet) -> Packet: try: - subdomain, *domains = packet.subdomain_from_qname.split('.') + subdomain, *domains = packet.subdomain_from_qname.split(".") domains = domains[::-1] data = Domain.decode(subdomain) except binascii.Error: @@ -90,8 +90,7 @@ def _make_txt(self, packet: Packet) -> Packet: "question": packet.question, "messages": [ self._make_message( - packet.qname, - self.on_query(data, packet.src, domains) + packet.qname, self.on_query(data, packet.src, domains) ), ], }, @@ -137,11 +136,13 @@ def _dns_responder(self, pkt: IP): dnstypes[packet.question.qtype], packet.src, packet.sport, - packet.qname + packet.qname, ) # reject every packet which isn't a DNS A/TXT query - if packet.is_valid_dnsquery("A", self.config["server"]["root"] if self.config else ""): + if packet.is_valid_dnsquery( + "A", self.config["server"]["root"] if self.config else "" + ): answer = self._make_a(packet) elif packet.is_valid_dnsquery("TXT"): answer = self._make_txt(packet) @@ -153,7 +154,7 @@ def run(self): # bind a UDP socket server on port 53, otherwise we'll have # ICMP type 3 error as a client, because the port will be seen # as unreachable (nothing being binded on it) - t = threading.Thread(target=socket_server, args=(self.host_ip, )) + t = threading.Thread(target=socket_server, args=(self.host_ip,)) t.start() self.logger.info(f"DNS sniffer started on {self.host_ip}:53") From 47f55824e44fceb263cef08670a9749ff4221713 Mon Sep 17 00:00:00 2001 From: Alexandre Plateau Date: Thu, 21 Jan 2021 06:14:05 -0500 Subject: [PATCH 8/9] linter --- client.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.sh b/client.sh index ba5a802..f3aadfa 100644 --- a/client.sh +++ b/client.sh @@ -32,7 +32,7 @@ case "$length" in ;; esac # decode -decoded=$(echo $message | base64 -d) +decoded=$(echo "$message" | base64 -d) FinalDate=$(date -u +"%s.%N") elapsed=$(date -u -d "0 $FinalDate sec - $StartDate sec" +"%S.%N") From 3c8ba42d6924fb064397d24de5bc246e846f86ec Mon Sep 17 00:00:00 2001 From: Alexandre Plateau Date: Thu, 21 Jan 2021 06:16:28 -0500 Subject: [PATCH 9/9] linter --- src/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server.py b/src/server.py index 631c95f..5e71939 100644 --- a/src/server.py +++ b/src/server.py @@ -40,7 +40,7 @@ def from_file(filename: str): config["server"]["interface"], config["server"]["domain"], config["server"]["host_ip"], - config + config, ) def __init__(self, iface: str, domain: str, ip: str, config: ConfigParser = None):