aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-07-17 03:01:58 +0200
committeruser <user@clank>2026-07-17 03:01:58 +0200
commit0e5889c7dd431d7cb4cdaacea2702065fd96839f (patch)
treec6cf8b15ad4529511f1574553ee7bcd42c101858
parentnet: IP stack milestone B - UDP echo (pseudo-header checksum) (diff)
downloadgbos-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.py127
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()