diff options
| author | user <user@clank> | 2026-07-17 03:01:58 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-07-17 03:01:58 +0200 |
| commit | 0e5889c7dd431d7cb4cdaacea2702065fd96839f (patch) | |
| tree | c6cf8b15ad4529511f1574553ee7bcd42c101858 | |
| parent | net: IP stack milestone B - UDP echo (pseudo-header checksum) (diff) | |
| download | gbos-0e5889c7dd431d7cb4cdaacea2702065fd96839f.tar.gz gbos-0e5889c7dd431d7cb4cdaacea2702065fd96839f.tar.xz gbos-0e5889c7dd431d7cb4cdaacea2702065fd96839f.zip | |
net: real routed IP via SLIP<->TUN bridge (you can *ping* a Game Boy)
tunbridge.py opens a TUN device (gbtun0, 10.0.0.1/24), NATs 10.0.0.0/24 out
to the internet, spawns the emulator, and bridges raw IP packets on the TUN
to/from SLIP frames on the link serial. The Game Boy (10.0.0.2) becomes a
genuine routed IP host: the host kernel routes its packets for real, so the
host's own `ping` command reaches it and netd answers.
Verified: `sudo python3 tools/tunbridge.py netd --test`
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=1.61 ms
4 packets transmitted, 4 received, 0% packet loss
Two hard-won gotchas baked in:
- Under sudo, ~ expands to /root, so resolve EMU/ROM via $SUDO_USER's home.
- Only start feeding host->GB packets AFTER netd is running; raw packet
bytes delivered to gbos's shell (before netd claims the serial) corrupt
the shell and crash the emulator. gb->host draining starts immediately.
Requires /dev/net/tun (on Proxmox LXC: allow cgroup2 device c 10:200 rwm +
bind-mount /dev/net/tun into the container).
| -rw-r--r-- | tools/tunbridge.py | 127 |
1 files changed, 127 insertions, 0 deletions
diff --git a/tools/tunbridge.py b/tools/tunbridge.py new file mode 100644 index 0000000..b462c1b --- /dev/null +++ b/tools/tunbridge.py @@ -0,0 +1,127 @@ +#!/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() |
