diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 0000000..69e8623 --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,8 @@ +# Attribution + +reximemoDNS is based on the open-source sudomemoDNS project by Austin Burk / Team Sudomemo. +The original project is distributed under the MIT License; see `LICENSE`. + +RexiMemo-specific changes include the project branding, default DNS targets, RexiMemo host interception, upstream DNS forwarding, and configuration options. + +RexiMemo is not affiliated with or endorsed by Team Sudomemo, Hatena Co., Ltd., or Nintendo. diff --git a/README.md b/README.md index 8b6211c..48e42aa 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,103 @@ -![sudomemoDNS Logo](/sudomemoDNS_banner.png) -### This server will allow you to connect to Sudomemo when your Internet Service Provider does not work with custom DNS. +# reximemoDNS + +A small local DNS helper for connecting Flipnote Studio to **RexiMemo** when a network or ISP does not reliably allow the console to use RexiMemo's public DNS directly. + +It runs on a computer on the same local network as the Nintendo DSi. Flipnote Studio's Nintendo/Hatena hostnames are redirected to RexiMemo, while unrelated DNS lookups are forwarded to a normal upstream resolver. + +## Default RexiMemo routing + +reximemoDNS redirects these hostnames to RexiMemo's legacy service address: + +- `nas.nintendowifi.net` +- `flipnote.hatena.com` +- `ugomemo.hatena.ne.jp` + +The default RexiMemo server IP is **141.147.76.115**. All other DNS queries are forwarded to **8.8.8.8** by default. + +`conntest.nintendowifi.net` is deliberately not intercepted, matching RexiMemo's server-side DNS configuration. ## Setup -The setup process does not differ from what is shown at https://flipnot.es/setup except for the values to enter in your custom DNS settings. +1. Put the computer running reximemoDNS and the DSi on the same local network. +2. Install the requirements. +3. Run reximemoDNS as Administrator/root because DNS uses port 53. +4. Enter the **Primary DNS** address printed by the program into the DSi connection settings. +5. Use `008.008.008.008` as Secondary DNS. +6. Save the connection and open Flipnote Studio → Flipnote Hatena. -First, make sure that your console is connected to the same network as this computer. - -Please submit any issues to Sudomemo Support. - -## Running on Windows: - -Run the .exe provided in this release. You may have to click past a warning from Windows SmartScreen (as it sometimes turns its nose up at applications bundled by PyInstaller) or your firewall (as it needs to listen for DNS requests). - -## Running on anything else: - -Required packages are installable with the following command: - - sudo python3 -m pip install -r requirements.txt - -Required Python version: 3.6+, tested with 3.9.9 - - sudo python3 sudomemoDNS.py - -Note: `python3` is assumed to be the name of your python binary, if it is not then it will need to be substituted with the name and/or path of your binary. - -## What you'll see when it starts up - - +===============================+ - | Sudomemo DNS Server | - | Version 1.2.1 | - +===============================+ - - == Welcome to sudomemoDNS! == - This server will allow you to connect to Sudomemo when your Internet Service Provider does - not work with custom DNS. - - == How To Use == - First, make sure that your console is connected to the same network as this computer. - - Then, put these settings in for DNS on your console: - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Primary DNS: XXX.XXX.XXX.XXX (NOTE: This value will be unique when you run the program) - Secondary DNS: 008.008.008.008 - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - == Getting Help == - Need help? Visit our Discord server or check out https://support.sudomemo.net - -## Building on Windows - -- Install Python 3.6+ (preferably 3.9). Make sure to install the x86 version for 32 bit targets. - -- Install all requirements via the following command as an administrator +### Install requirements +```bash +python -m pip install -r requirements.txt ``` + +### Run + +Windows, from an Administrator terminal: + +```powershell +python reximemoDNS.py +``` + +Linux/macOS: + +```bash +sudo python3 reximemoDNS.py +``` + +When it starts, it prints the LAN address to enter on the DSi. + +## Configuration + +The defaults are already set for the current RexiMemo server, but they can be changed with environment variables: + +- `REXIMEMODNS_SERVER_IP` — RexiMemo legacy-service IPv4 address. Default: `141.147.76.115` +- `REXIMEMODNS_UPSTREAM_DNS` — resolver used for all non-RexiMemo names. Default: `8.8.8.8` +- `REXIMEMODNS_BIND_IP` — local interface/address on which to listen. By default, reximemoDNS detects the computer's LAN IPv4 address. +- `REXIMEMODNS_PORT` — DNS listening port. Default: `53` + +Example: + +```bash +REXIMEMODNS_SERVER_IP=141.147.76.115 REXIMEMODNS_UPSTREAM_DNS=1.1.1.1 sudo python3 reximemoDNS.py +``` + +On Windows PowerShell: + +```powershell +$env:REXIMEMODNS_SERVER_IP="141.147.76.115" +$env:REXIMEMODNS_UPSTREAM_DNS="1.1.1.1" +python reximemoDNS.py +``` + +## Troubleshooting + +If the program cannot bind to port 53, run it as Administrator/root and make sure another DNS server is not already using that port. + +If no console requests appear in the log, check the computer firewall and confirm the DSi's Primary DNS exactly matches the address printed by reximemoDNS. + +If ordinary websites or the DSi connection test stop resolving while reximemoDNS is running, verify that `REXIMEMODNS_UPSTREAM_DNS` points to a reachable DNS resolver. + +For RexiMemo support, join the Discord server: **https://discord.gg/K5mKWvaruF** + +## Building a Windows executable + +Install Nuitka and its build dependencies: + +```powershell python -m pip install -r requirements.txt nuitka zstandard ``` -- Run the following (answer yes to any prompts): +Then build: -```shell -python -m nuitka --standalone --onefile --windows-icon-from-ico=sudomemoDNS_icon.ico -o sudomemoDNS.exe sudomemoDNS.py +```powershell +python -m nuitka --standalone --onefile -o reximemoDNS.exe reximemoDNS.py ``` -- Run `sudomemoDNS.exe` \ No newline at end of file +No RexiMemo-specific icon is bundled in this source tree, so the executable uses the default application icon unless you supply one when building. + +## License and attribution + +reximemoDNS is derived from the open-source `sudomemoDNS` project by Austin Burk / Team Sudomemo. The original MIT copyright and license are preserved in `LICENSE`. + +See `NOTICE.md` for attribution and RexiMemo-specific changes. diff --git a/requirements.txt b/requirements.txt index 1b9e8ea..48ea80a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -requests -dnslib +dnslib>=0.9.24 diff --git a/reximemoDNS.py b/reximemoDNS.py new file mode 100644 index 0000000..2824c8d --- /dev/null +++ b/reximemoDNS.py @@ -0,0 +1,244 @@ +# reximemoDNS +# +# RexiMemo adaptation of the MIT-licensed sudomemoDNS project. +# See LICENSE and NOTICE.md for attribution. + +from datetime import datetime +from ipaddress import IPv4Address, ip_address +import os +from socket import AF_INET, SOCK_DGRAM, socket +from sys import platform +from time import sleep + +from dnslib import A, DNSLabel, DNSRecord, QTYPE, RCODE, RR +from dnslib.server import DNSServer + + +REXIMEMODNS_VERSION = "1.0.0" +DEFAULT_REXIMEMO_SERVER_IP = "141.147.76.115" +DEFAULT_UPSTREAM_DNS = "8.8.8.8" + +# Keep this list aligned with RexiMemo's LOCAL_DNS_HOSTS in network_services.py. +# conntest.nintendowifi.net is intentionally not intercepted. +REXIMEMO_HOSTS = ( + "nas.nintendowifi.net", + "flipnote.hatena.com", + "ugomemo.hatena.ne.jp", +) + + +def get_platform(): + platforms = { + "linux": "Linux", + "linux1": "Linux", + "linux2": "Linux", + "darwin": "macOS", + "win32": "Windows", + } + return platforms.get(platform, platform) + + +def format_ip(address): + """Format an IPv4 address the way Nintendo connection screens display it.""" + octets = str(address).split(".") + return ".".join(f"{int(octet):03d}" for octet in octets) + + +def _ipv4_env(name, default): + value = (os.environ.get(name) or default).strip() + try: + parsed = ip_address(value) + except ValueError as exc: + raise SystemExit(f"[ERROR] {name} must be a valid IPv4 address; got {value!r}") from exc + if not isinstance(parsed, IPv4Address): + raise SystemExit(f"[ERROR] {name} must be an IPv4 address; got {value!r}") + return str(parsed) + + +def get_ip(): + configured = (os.environ.get("REXIMEMODNS_BIND_IP") or "").strip() + if configured: + return _ipv4_env("REXIMEMODNS_BIND_IP", configured) + + probe = socket(AF_INET, SOCK_DGRAM) + try: + # The destination does not need to be reachable. Connecting a UDP socket + # is only used to discover which local IPv4 address the OS would route. + probe.connect(("10.255.255.255", 1)) + return probe.getsockname()[0] + except OSError: + return "127.0.0.1" + finally: + probe.close() + + +def _port_env(): + raw = (os.environ.get("REXIMEMODNS_PORT") or "53").strip() + try: + value = int(raw) + except ValueError as exc: + raise SystemExit(f"[ERROR] REXIMEMODNS_PORT must be a number; got {raw!r}") from exc + if not 1 <= value <= 65535: + raise SystemExit("[ERROR] REXIMEMODNS_PORT must be between 1 and 65535") + return value + + +MY_IP = get_ip() +REXIMEMO_SERVER_IP = _ipv4_env("REXIMEMODNS_SERVER_IP", DEFAULT_REXIMEMO_SERVER_IP) +UPSTREAM_DNS = _ipv4_env("REXIMEMODNS_UPSTREAM_DNS", DEFAULT_UPSTREAM_DNS) +DNS_PORT = _port_env() +SERIAL = int((datetime.utcnow() - datetime(1970, 1, 1)).total_seconds()) + + +def _print_banner(): + print("+===============================+") + print("| RexiMemo DNS Server |") + print(f"| Version {REXIMEMODNS_VERSION:<13}|") + print("+===============================+\n") + + print("== Welcome to reximemoDNS! ==") + print( + "This local DNS helper routes Flipnote Studio's Nintendo/Hatena service names " + "to RexiMemo while forwarding unrelated DNS normally.\n" + ) + + print("== How To Use ==") + print("First, make sure that your console is connected to the same network as this computer.\n") + print("Then, put these settings in for DNS on your console:") + print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") + print(f"Primary DNS: {format_ip(MY_IP)}") + print(f"Secondary DNS: {format_ip(DEFAULT_UPSTREAM_DNS)}") + print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n") + + print("== RexiMemo Routing ==") + print(f"RexiMemo server: {REXIMEMO_SERVER_IP}") + for hostname in REXIMEMO_HOSTS: + print(f" {hostname} -> {REXIMEMO_SERVER_IP}") + print(f"Other DNS queries -> {UPSTREAM_DNS}\n") + + print("== Getting Help ==") + print("Join the RexiMemo Discord server: https://discord.gg/K5mKWvaruF\n") + + +class RexiMemoDNSLogger(object): + def log_recv(self, handler, data): + pass + + def log_send(self, handler, data): + pass + + def log_request(self, handler, request): + qname = str(request.q.qname).rstrip(".") + qtype = QTYPE.get(request.q.qtype, str(request.q.qtype)) + print( + "[INFO] DNS request from %s: %s %s" + % (handler.client_address[0], qtype, qname) + ) + + def log_reply(self, handler, reply): + answer_count = len(reply.rr) + print( + "[INFO] DNS response to %s: %d answer%s" + % (handler.client_address[0], answer_count, "" if answer_count == 1 else "s") + ) + + def log_error(self, handler, error): + print("[ERROR] Invalid DNS request from %s: %s" % (handler.client_address[0], error)) + + def log_truncated(self, handler, reply): + pass + + def log_data(self, dnsobj): + pass + + +class Resolver: + def __init__(self): + self.reximemo_names = {DNSLabel(name) for name in REXIMEMO_HOSTS} + + def _reximemo_reply(self, request): + reply = request.reply() + # Mirror RexiMemo's own DNS behavior: A/ANY gets the legacy-service IPv4 + # address. Other query types get an authoritative empty NOERROR response. + if request.q.qtype in (QTYPE.A, QTYPE.ANY): + reply.add_answer( + RR( + rname=request.q.qname, + rtype=QTYPE.A, + rclass=1, + ttl=0, + rdata=A(REXIMEMO_SERVER_IP), + ) + ) + return reply + + def _forward(self, request, handler): + try: + raw = request.send( + UPSTREAM_DNS, + 53, + tcp=(getattr(handler, "protocol", "udp") == "tcp"), + timeout=3, + ) + reply = DNSRecord.parse(raw) + # Never relay a mismatched DNS transaction as if it answered this query. + if reply.header.id != request.header.id: + raise ValueError("upstream DNS transaction ID mismatch") + return reply + except Exception as exc: + print(f"[ERROR] Upstream DNS lookup via {UPSTREAM_DNS} failed: {exc}") + reply = request.reply() + reply.header.rcode = RCODE.SERVFAIL + return reply + + def resolve(self, request, handler): + if request.q.qname in self.reximemo_names: + return self._reximemo_reply(request) + return self._forward(request, handler) + + +def main(): + _print_banner() + print("[INFO] Starting reximemoDNS...") + print("[INFO] Detected operating system:", get_platform()) + + current_platform = get_platform() + if current_platform in {"Linux", "macOS"}: + print("[INFO] DNS uses UDP/TCP port 53, so this normally must run as root.") + print("[INFO] If no requests appear, also check whether another service is already using port 53.") + elif current_platform == "Windows": + print("[INFO] Windows may ask you to allow reximemoDNS through the firewall.") + + resolver = Resolver() + logger = RexiMemoDNSLogger() + + try: + servers = [ + DNSServer(resolver=resolver, port=DNS_PORT, address=MY_IP, tcp=True, logger=logger), + DNSServer(resolver=resolver, port=DNS_PORT, address=MY_IP, tcp=False, logger=logger), + ] + except PermissionError: + print("[ERROR] Permission denied while opening DNS port. Run as Administrator/root.") + raise SystemExit(1) + except OSError as exc: + print(f"[ERROR] Could not bind DNS server to {MY_IP}:{DNS_PORT}: {exc}") + raise SystemExit(1) + + print(f"[INFO] reximemoDNS is ready on {MY_IP}:{DNS_PORT} (UDP/TCP).") + print("[INFO] Waiting for DNS requests from your console...") + + for server in servers: + server.start_thread() + + try: + while True: + sleep(0.1) + except KeyboardInterrupt: + print("\n[INFO] Stopping reximemoDNS...") + finally: + for server in servers: + server.stop() + + +if __name__ == "__main__": + main()