Skip to content

Instantly share code, notes, and snippets.

@DavidBuchanan314
Created April 22, 2019 01:36
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DavidBuchanan314/a0f0c8e2963ecf76e142e11aaabcfd86 to your computer and use it in GitHub Desktop.
Save DavidBuchanan314/a0f0c8e2963ecf76e142e11aaabcfd86 to your computer and use it in GitHub Desktop.
"""
This script sets up the bare minimum needed to get a Nintendo Switch console
to connect to a wifi network, without an actual internet connection.
Namely:
- A DNS server that points every record to the host running the server
- An HTTP server that responds with the `X-Organization: Nintendo` header.
This script must be run as root to acquire the permissions to listen on the
relevant ports.
The DNS implementation is very far from following any kind of standard...
"""
from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import BaseRequestHandler, UDPServer
from multiprocessing import Process
import socket
from ctypes import *
from ipaddress import IPv4Address
HTTPD_HOST = "0.0.0.0"
HTTPD_PORT = 80
DNS_HOST = "0.0.0.0"
DNS_PORT = 53
DNS_qtypes = {
1: 'A',
28: 'AAAA',
5: 'CNAME',
12: 'PTR',
16: 'TXT',
15: 'MX',
6: 'SOA'
}
class DNS_Header(BigEndianStructure):
_pack_ = 1
_fields_ = [
("id", c_uint16),
("flags", c_uint16), # contains packed fields: QR Opcode AA TC RD RA RCODE
("qdcount", c_uint16),
("ancount", c_uint16),
("nscount", c_uint16),
("arcount", c_uint16),
]
class DNS_Response(BigEndianStructure):
_pack_ = 1
_fields_ = [
("ttl", c_uint32),
("rdlength", c_uint16),
("rdata", c_uint32),
]
class DNS_Question(BigEndianStructure):
_pack_ = 1
_fields_ = [
("qtype", c_uint16),
("qclass", c_uint16),
]
def get_local_ip():
"""
This effectively gets the default route, which should be what you want
in most cases.
"""
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("10.255.255.255", 0))
ip = s.getsockname()[0]
s.close()
return ip
class FakePortalHeaderServer(BaseHTTPRequestHandler):
def fake_portal_response(self):
self.send_response(200)
self.send_header("X-Organization", "Nintendo") # ( ͡° ͜ʖ ͡°)
self.end_headers()
def do_HEAD(self):
self.fake_portal_response()
def do_GET(self):
self.fake_portal_response()
def log_message(self, fmt, *args):
print("[+] [httpd] {} - - [{}] {}".format(
self.address_string(),
self.log_date_time_string(),
fmt%args))
class FakeDNSHandler(BaseRequestHandler):
"""
This DNS implementation is garbage
"""
def handle(self):
data = self.request[0] + b"\0" # shitty parsing
sock = self.request[1]
if len(data) < sizeof(DNS_Header):
return
header = DNS_Header.from_buffer_copy(data)
resp_header = DNS_Header()
resp_header.id = header.id
resp_header.flags = header.flags
resp_header.flags |= 1<<15 # set the response flag
resp_header.flags &=~ 1<<(15-7) # clear recursion desired
resp_header.flags |= 1<<(15-8) # set recursion available
resp_header.flags |= 1<<(15-5) # pretend to be authoritative
resp_header.qdcount = header.qdcount
resp_header.ancount = header.qdcount
response = DNS_Response()
response.ttl = 3600
response.rdlength = 4
response.rdata = IPv4Address(HOST)
remainder = data[sizeof(header):]
answers = b""
for i in range(header.qdcount):
prev = remainder[::]
remainder, labels = self._parse_labels(remainder)
q = DNS_Question.from_buffer_copy(remainder)
remainder = remainder[sizeof(q):]
label_str = ".".join([x.decode() for x in labels])
print("[+] [dns] {} - {} {} -> {}".format(
self.client_address[0],
DNS_qtypes.get(q.qtype, "[unknown qtype]"),
label_str,
HOST))
answers += prev[:-len(remainder)]
answers += bytes(response)
queries = data[sizeof(header):-len(remainder)]
resp = bytes(resp_header) + queries + answers
sock.sendto(resp, self.client_address)
def _parse_labels(self, remainder):
labels = []
while remainder[0] != 0:
llen = remainder[0]
label, remainder = remainder[1:llen+1], remainder[llen+1:]
labels.append(label)
return remainder[1:], labels
def serve_http():
try:
httpd = HTTPServer((HTTPD_HOST, HTTPD_PORT), FakePortalHeaderServer)
except PermissionError:
print("[-] [httpd] Permission error - this script must be run as root!")
return
print("[*] [httpd] Serving HTTP on {}:{}".format(HTTPD_HOST, HTTPD_PORT))
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
print("[-] [httpd] Closing server")
httpd.server_close()
def serve_dns():
try:
dns = UDPServer((DNS_HOST, DNS_PORT), FakeDNSHandler)
except PermissionError:
print("[-] [dns] Permission error - this script must be run as root!")
return
print("[*] [dns] Serving DNS on {}:{}".format(DNS_HOST, DNS_PORT))
try:
dns.serve_forever()
except KeyboardInterrupt:
pass
print("[-] [dns] Closing server")
dns.server_close()
if __name__ == "__main__":
HOST = get_local_ip() # hardcode this if it autodetects wrong
print(f"[+] Detected local IP (enter as DNS on switch): {HOST}")
httpd = Process(target=serve_http)
dns = Process(target=serve_dns)
httpd.start()
dns.start()
try:
httpd.join()
dns.join()
except KeyboardInterrupt:
print("[*] [main thread] KeyboardInterrupt received")
@aveao
Copy link

aveao commented Apr 22, 2019

Are you sure that "X-Organization: Nintendo" alone is enough? In my testing in the past I had to specifically return ok on ctest.cdn.nintendo.net (in addition to the header)

@DavidBuchanan314
Copy link
Author

Hmm, it works for me, but I'm on 3.0.1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment