#!/usr/bin/env python3 """tunbridge.py - give gbos a real routed IP over the link port. Opens a TUN device (gbtun0, 10.0.0.1/24), NATs it to the internet, spawns the emulator, and bridges: SLIP frames on the link serial <-> raw IP packets on the TUN. The Game Boy (10.0.0.2) becomes a genuine IP host - the host kernel routes its packets for real. Run as root: sudo python3 tools/tunbridge.py TLS is still not the GB's problem; plaintext services work end to end. """ import os, sys, fcntl, struct, subprocess, threading, time, signal, atexit # resolve paths against the invoking (non-root) user's home, since sudo makes ~ = /root _HOME = os.path.expanduser("~" + (os.environ.get("SUDO_USER") or "")) EMU = os.path.join(_HOME, "dev/gbc/build/sl0pboy") ROM = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "gbos.gb")) IFACE, GW_IP, GB_NET, MTU = "gbtun0", "10.0.0.1", "10.0.0.0/24", 256 END, ESC, ESC_END, ESC_ESC = 0xC0, 0xDB, 0xDC, 0xDD TUNSETIFF, IFF_TUN, IFF_NO_PI = 0x400454ca, 0x0001, 0x1000 def slip_encode(pkt): out = bytearray([END]) for c in pkt: if c == END: out += bytes([ESC, ESC_END]) elif c == ESC: out += bytes([ESC, ESC_ESC]) else: out.append(c) out.append(END) return bytes(out) def sh(cmd, quiet=True): subprocess.run(cmd, shell=True, check=False, stdout=subprocess.DEVNULL if quiet else None, stderr=subprocess.DEVNULL if quiet else None) def wan_iface(): try: return subprocess.check_output("ip route show default | awk '{print $5; exit}'", shell=True).decode().strip() or "eth0" except Exception: return "eth0" def main(): if os.geteuid() != 0: sys.exit("must run as root (sudo)") tun = os.open("/dev/net/tun", os.O_RDWR) fcntl.ioctl(tun, TUNSETIFF, struct.pack("16sH", IFACE.encode(), IFF_TUN | IFF_NO_PI)) wan = wan_iface() sh("ip addr add %s/24 dev %s" % (GW_IP, IFACE)) sh("ip link set %s mtu %d up" % (IFACE, MTU)) sh("sysctl -w net.ipv4.ip_forward=1") sh("iptables -t nat -C POSTROUTING -s %s -o %s -j MASQUERADE || " "iptables -t nat -A POSTROUTING -s %s -o %s -j MASQUERADE" % (GB_NET, wan, GB_NET, wan)) sh("iptables -C FORWARD -i %s -j ACCEPT || iptables -I FORWARD -i %s -j ACCEPT" % (IFACE, IFACE)) sh("iptables -C FORWARD -o %s -j ACCEPT || iptables -I FORWARD -o %s -j ACCEPT" % (IFACE, IFACE)) sys.stderr.write("[tun] %s up %s/24, NAT via %s, GB is 10.0.0.2\n" % (IFACE, GW_IP, wan)) def cleanup(): sh("iptables -t nat -D POSTROUTING -s %s -o %s -j MASQUERADE" % (GB_NET, wan)) sh("iptables -D FORWARD -i %s -j ACCEPT" % IFACE) sh("iptables -D FORWARD -o %s -j ACCEPT" % IFACE) atexit.register(cleanup) p = subprocess.Popen([EMU, "--headless", "--uncapped", ROM], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=0, start_new_session=True) def tun_to_gb(): # host -> Game Boy while True: pkt = os.read(tun, 2048) if not pkt: return try: p.stdin.write(slip_encode(pkt)); p.stdin.flush() except Exception: return def gb_to_tun(): # Game Boy -> host buf, in_frame, esc = bytearray(), False, False while True: b = p.stdout.read(1) if not b: return c = b[0] if c == END: if in_frame and buf: try: os.write(tun, bytes(buf)) except Exception: pass buf, in_frame, esc = bytearray(), False, False else: buf, in_frame, esc = bytearray(), True, False elif in_frame: if esc: c = {ESC_END: END, ESC_ESC: ESC}.get(c, c); esc = False; buf.append(c) elif c == ESC: esc = True else: buf.append(c) else: sys.stderr.write(chr(c) if 32 <= c < 127 else ("\n" if c == 10 else "")) # Drain the GB->host direction from the start, but only start feeding host->GB # packets AFTER netd is running: raw packet bytes fed to gbos's shell (before # netd claims the serial) corrupt the shell and crash the emulator. threading.Thread(target=gb_to_tun, daemon=True).start() args = sys.argv[1:] testmode = "--test" in args if testmode: args.remove("--test") cmd = args[0] if args else "netd" time.sleep(1.0) p.stdin.write(cmd.encode() + b"\n"); p.stdin.flush() time.sleep(0.8) threading.Thread(target=tun_to_gb, daemon=True).start() sys.stderr.write("[tun] running %s on the GB\n" % cmd) if testmode: time.sleep(3.0) r = subprocess.run("ping -c 4 -W 2 10.0.0.2", shell=True, capture_output=True, text=True) sys.stderr.write("\n=== ping 10.0.0.2 (host -> Game Boy, real routing) ===\n") sys.stderr.write(r.stdout + r.stderr + "\n") p.kill(); return sys.stderr.write("[tun] try: ping 10.0.0.2 (Ctrl-C to stop)\n") signal.signal(signal.SIGTERM, lambda *a: sys.exit(0)) try: while p.poll() is None: time.sleep(0.5) except KeyboardInterrupt: pass finally: p.kill() if __name__ == "__main__": main()