aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-07-17 13:22:01 +0200
committeruser <user@clank>2026-07-17 13:22:01 +0200
commit6c1c49d676ca6fa9ab3d8cf1b80c98e45b85fbe0 (patch)
tree87049ef04a42fd9b58d7f04eeb91653c276383d1
parentnet: wget-by-hostname + shared DNS resolver (a Game Boy fetches example.com) (diff)
downloadgbos-6c1c49d676ca6fa9ab3d8cf1b80c98e45b85fbe0.tar.gz
gbos-6c1c49d676ca6fa9ab3d8cf1b80c98e45b85fbe0.tar.xz
gbos-6c1c49d676ca6fa9ab3d8cf1b80c98e45b85fbe0.zip
tools: netboot - interactive networked emulator (TUN + NAT + --serial-sock)
Brings up gbtun0 (10.0.0.1/24) + NAT, then launches sl0pboy with your own args and the link port bridged to the TUN over a unix socket (the emulator's new --serial-sock). Unlike headless tunbridge, the LCD + on-screen keyboard stay live, so it's a fully interactive networked Game Boy: the OSK is the console, the link port is the network. sudo tools/netboot --sixel --chrome gbos.gb # on the GB: SELECT for the OSK, then type: wget example.com / ping 1.1.1.1 GB is 10.0.0.2, host is 10.0.0.1. Requires /dev/net/tun + root. Ctrl-C or quit the emulator to tear down. tunbridge stays for headless scripted runs.
-rwxr-xr-xtools/netboot128
1 files changed, 128 insertions, 0 deletions
diff --git a/tools/netboot b/tools/netboot
new file mode 100755
index 0000000..ccc7116
--- /dev/null
+++ b/tools/netboot
@@ -0,0 +1,128 @@
+#!/usr/bin/env python3
+"""netboot - bring up real networking (TUN + NAT) and launch sl0pboy with your
+own arguments, link port bridged to the TUN over a unix socket.
+
+Unlike the headless `tunbridge`, this keeps the LCD + on-screen keyboard, so you
+get a fully interactive, networked Game Boy: the console is the OSK, the link
+port is the network - no sharing, no confusion.
+
+ sudo tools/netboot --sixel --chrome gbos.gb
+ sudo tools/netboot gbos.gb
+
+Then, on the Game Boy: press SELECT for the OSK and type e.g.
+ wget example.com
+ ping 1.1.1.1
+ nslookup sl0p.foo
+The GB is 10.0.0.2; the host/gateway is 10.0.0.1. Ctrl-C (or quit the emu) to
+tear everything down. Requires /dev/net/tun and root.
+"""
+import os, sys, fcntl, struct, subprocess, socket, threading, atexit
+
+HOME = os.path.expanduser("~" + (os.environ.get("SUDO_USER") or ""))
+EMU = os.path.join(HOME, "dev/gbc/build/sl0pboy")
+SOCKPATH = "/tmp/sl0pboy-net.sock"
+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):
+ o = bytearray([END])
+ for c in pkt:
+ if c == END: o += bytes([ESC, ESC_END])
+ elif c == ESC: o += bytes([ESC, ESC_ESC])
+ else: o.append(c)
+ o.append(END)
+ return bytes(o)
+
+def sh(cmd):
+ subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+
+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("netboot: must run as root (sudo)")
+ args = sys.argv[1:]
+ if not args:
+ sys.exit("usage: sudo netboot [emulator options] <rom>")
+ rom, opts = os.path.abspath(args[-1]), args[:-1]
+ if not os.path.exists(EMU):
+ sys.exit("netboot: emulator not found at " + EMU)
+
+ 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))
+
+ 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)
+ try: os.unlink(SOCKPATH)
+ except Exception: pass
+ atexit.register(cleanup)
+
+ try: os.unlink(SOCKPATH)
+ except Exception: pass
+ srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ srv.bind(SOCKPATH); srv.listen(1)
+
+ sys.stderr.write("[netboot] %s up %s/24, NAT via %s. GB=10.0.0.2. "
+ "OSK (SELECT) -> wget example.com / ping 1.1.1.1\n" % (IFACE, GW_IP, wan))
+ emu = subprocess.Popen([EMU] + opts + ["--serial-sock", SOCKPATH, rom])
+
+ srv.settimeout(10)
+ try:
+ conn, _ = srv.accept()
+ except socket.timeout:
+ sys.stderr.write("[netboot] emulator never connected the link socket\n")
+ emu.kill(); return
+ conn.setblocking(True)
+
+ def sock_to_tun(): # Game Boy -> host
+ buf, in_frame, esc = bytearray(), False, False
+ while True:
+ try: chunk = conn.recv(2048)
+ except Exception: return
+ if not chunk: return
+ for c in chunk:
+ 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)
+ # non-frame bytes are the console mirror; it's on the LCD, ignore
+ def tun_to_sock(): # host -> Game Boy
+ while True:
+ try: pkt = os.read(tun, 2048)
+ except Exception: return
+ if not pkt: return
+ try: conn.sendall(slip_encode(pkt))
+ except Exception: return
+ threading.Thread(target=sock_to_tun, daemon=True).start()
+ threading.Thread(target=tun_to_sock, daemon=True).start()
+
+ try:
+ emu.wait()
+ except KeyboardInterrupt:
+ emu.terminate()
+
+if __name__ == "__main__":
+ main()