From 6c1c49d676ca6fa9ab3d8cf1b80c98e45b85fbe0 Mon Sep 17 00:00:00 2001 From: user Date: Fri, 17 Jul 2026 13:22:01 +0200 Subject: 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. --- tools/netboot | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100755 tools/netboot (limited to 'tools/netboot') 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, 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() -- cgit v1.3.1-sl0p