From 9be1eafac01e93dd0d74790ea212b065380b5ff6 Mon Sep 17 00:00:00 2001 From: Austin Burk Date: Sat, 16 Feb 2019 15:01:50 -0500 Subject: [PATCH] Initial commit for sudomemoDNS --- README.md | 17 ++++ sudomemoDNS.py | 238 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 README.md create mode 100644 sudomemoDNS.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..e05e0a1 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# sudomemoDNS + +This server will allow you to connect to Sudomemo when +your Internet Service Provider does not work with custom DNS. + +## Setup + +The setup process does not differ from what is shown at +https://support.sudomemo.net/setup except for the values +to enter in your custom DNS settings. + +First, make sure that your Nintendo DSi is connected to the +same network as this computer. + +# Running on Windows + +Right click on sudomemoDNS_##.exe (the ## will instead show the version number) and click on "Run as administrator". diff --git a/sudomemoDNS.py b/sudomemoDNS.py new file mode 100644 index 0000000..8bf2d24 --- /dev/null +++ b/sudomemoDNS.py @@ -0,0 +1,238 @@ +from datetime import datetime +from time import sleep + +from dnslib import DNSLabel, QTYPE, RD, RR +from dnslib import A, AAAA, CNAME, MX, NS, SOA, TXT +from dnslib.server import DNSServer + +import socket +import requests +import json +import sys + +def get_platform(): + platforms = { + 'linux1' : 'Linux', + 'linux2' : 'Linux', + 'darwin' : 'OS X', + 'win32' : 'Windows' + } + if sys.platform not in platforms: + return sys.platform + + return platforms[sys.platform] + +SUDOMEMODNS_VERSION = "0.1" + +def get_ip(): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + # doesn't even have to be reachable + s.connect(('10.255.255.255', 1)) + IP = s.getsockname()[0] + except: + IP = '127.0.0.1' + finally: + s.close() + return IP + +EPOCH = datetime(1970, 1, 1) +SERIAL = int((datetime.utcnow() - EPOCH).total_seconds()) +MY_IP = get_ip() + +print("+===============================+") +print("| Sudomemo DNS Server |") +print("| Version " + SUDOMEMODNS_VERSION + " |") +print("+===============================+\n") + +print("Hello! This server will allow you to connect to Sudomemo when") +print("your Internet Service Provider does not work with custom DNS.\n") + +print("#### How To Use ####\n") +print("The setup process does not differ from what is shown at") +print("https://support.sudomemo.net/setup except for the values") +print("to enter in your custom DNS settings.\n") + +print("First, make sure that your Nintendo DSi is connected to the") +print("same network as this computer.") + +print("\nHere are the settings you will put in for DNS on your Nintendo DSi:\n") +print("Primary DNS: ",MY_IP) +print("Secondary DNS: 8.8.8.8") + +print("All other settings should match what is shown at the above URL.\n") + +print("#### Starting up ####\n") + +TYPE_LOOKUP = { + A: QTYPE.A, + AAAA: QTYPE.AAAA, + CNAME: QTYPE.CNAME, + MX: QTYPE.MX, + NS: QTYPE.NS, + SOA: QTYPE.SOA, + TXT: QTYPE.TXT, +} + +# Can't seem to turn off DNSLogger with a None type so let's just null it out with a dummy function + +class SudomemoDNSLogger(object): + def log_recv(self, handler, data): + pass + def log_send(self, handler, data): + pass + def log_request(self, handler, request): + print("[INFO] Received DNS request from DSi at " + handler.client_address[0]) + def log_reply(self, handler, reply): + print("[INFO] Sent response to DSi at " + handler.client_address[0]) + def log_error(self, handler, e): + logger.error("[INFO] Invalid DNS request from " + handler.client_address[0]) + def log_truncated(self, handler, reply): + pass + def log_data(self, dnsobj): + pass + + +class Record: + def __init__(self, rdata_type, *args, rtype=None, rname=None, ttl=None, **kwargs): + if isinstance(rdata_type, RD): + # actually an instance, not a type + self._rtype = TYPE_LOOKUP[rdata_type.__class__] + rdata = rdata_type + else: + self._rtype = TYPE_LOOKUP[rdata_type] + if rdata_type == SOA and len(args) == 2: + # add sensible times to SOA + args += (( + SERIAL, # serial number + 60 * 60 * 1, # refresh + 60 * 60 * 3, # retry + 60 * 60 * 24, # expire + 60 * 60 * 1, # minimum + ),) + rdata = rdata_type(*args) + + if rtype: + self._rtype = rtype + self._rname = rname + self.kwargs = dict( + rdata=rdata, + ttl=self.sensible_ttl() if ttl is None else ttl, + **kwargs, + ) + + def try_rr(self, q): + if q.qtype == QTYPE.ANY or q.qtype == self._rtype: + return self.as_rr(q.qname) + + def as_rr(self, alt_rname): + return RR(rname=self._rname or alt_rname, rtype=self._rtype, **self.kwargs) + + def sensible_ttl(self): + if self._rtype in (QTYPE.NS, QTYPE.SOA): + return 60 * 60 * 24 + else: + return 300 + + @property + def is_soa(self): + return self._rtype == QTYPE.SOA + + def __str__(self): + return '{} {}'.format(QTYPE[self._rtype], self.kwargs) + + +ZONES = {} + +try: + get_zones = requests.get("https://www.sudomemo.net/api/dns_zones.json", headers={'User-Agent': 'SudomemoDNS/' + SUDOMEMODNS_VERSION + ' (' + get_platform() + ')'}) +except requests.exceptions.Timeout: + print("[ERROR] Couldn't load DNS data: connection to Sudomemo timed out.") + print("[ERROR] Are you connected to the Internet?") +except requests.exceptions.RequestException as e: + print("[ERROR] Couldn't load DNS data.") + print("[ERROR] Exception: ",e) + sys.exit(1) +try: + zones = json.loads(get_zones.text) +except ValueError as e: + print("[ERROR] Couldn't load DNS data: invalid response from server") + print("[ERROR] Check that you can visit sudomemo.net") + +for zone in zones: + if zone["type"] == "a": + ZONES[zone["name"]] = [ Record(A, zone["value"]) ] + elif zone["type"] == "p": + ZONES[zone["name"]] = [ Record(A, socket.gethostbyname(zone["value"])) ] + +print("[INFO] DNS information loaded successfully.") + +class Resolver: + def __init__(self): + self.zones = {DNSLabel(k): v for k, v in ZONES.items()} + + def resolve(self, request, handler): + reply = request.reply() + zone = self.zones.get(request.q.qname) + if zone is not None: + for zone_records in zone: + rr = zone_records.try_rr(request.q) + rr and reply.add_answer(rr) + else: + # no direct zone so look for an SOA record for a higher level zone + for zone_label, zone_records in self.zones.items(): + if request.q.qname.matchSuffix(zone_label): + try: + soa_record = next(r for r in zone_records if r.is_soa) + except StopIteration: + continue + else: + reply.add_answer(soa_record.as_rr(zone_label)) + break + + return reply + + +resolver = Resolver() +dnsLogger = SudomemoDNSLogger() +if get_platform() == 'linux': + print("[INFO] Please note that you will have to run this as root or with permissions to bind to UDP port 53.") + print("[INFO] If you aren't seeing any requests, check that this is the case first with lsof -i:53 (requires lsof)") + print("[INFO] To run as root, prefix the command with 'sudo'") +elif get_platform() == 'OS X': + print("[INFO] Please note that you will have to run this as root or with permissions to bind to UDP port 53.") + print("[INFO] If you aren't seeing any requests, check that this is the case first with lsof -i:53 (requires lsof)") + print("[INFO] To run as root, prefix the command with 'sudo'") +elif get_platform() == 'Windows': + print("[INFO] Please note: On Windows, you will have to run this as Administrator.") + print("[INFO] Right click on the .exe and select 'Run as administrator' to do so.") + print("[INFO] If you aren't seeing any requests, make sure you have done so first.") + print("[INFO] Disregard this message if you have already done so; as the message will always show") + + +try: + servers = [ + DNSServer(resolver=resolver, port=53, address=MY_IP, tcp=True, logger=dnsLogger), + DNSServer(resolver=resolver, port=53, address=MY_IP, tcp=False, logger=dnsLogger), + ] +except PermissionError: + print("[ERROR] Permission error: check that you are running this as Administrator or root") + sys.exit(1) + + +print("[INFO] Starting Sudomemo DNS server.") + +print("[INFO] Now waiting for DNS requests from your Nintendo DSi System.") + +if __name__ == '__main__': + for s in servers: + s.start_thread() + + try: + while 1: + sleep(0.1) + except KeyboardInterrupt: + pass + finally: + for s in servers: + s.stop()