#!/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) # ---- built-in DHCP server: lease the GB 10.0.0.2 ---------------------------- GB_IP = b"\x0a\x00\x00\x02"; GW = b"\x0a\x00\x00\x01" def _cksum(d): s = 0 for i in range(0, len(d) - 1, 2): s += (d[i] << 8) | d[i + 1] if len(d) % 2: s += d[-1] << 8 while s >> 16: s = (s & 0xFFFF) + (s >> 16) return (~s) & 0xFFFF def _dhcp_mtype(dhcp): p = 240 while p < len(dhcp) and dhcp[p] != 255: if dhcp[p] == 0: p += 1; continue if dhcp[p] == 53: return dhcp[p + 2] p += 2 + dhcp[p + 1] return 0 def _dhcp_reply(req, mt): d = bytearray(240) d[0] = 2; d[1] = 1; d[2] = 6 d[4:8] = req[4:8]; d[16:20] = GB_IP; d[20:24] = GW; d[28:44] = req[28:44] d[236:240] = b"\x63\x82\x53\x63" d += bytes([53, 1, mt, 54, 4]) + GW + bytes([255]) udp = bytearray(8); udp[1] = 67; udp[3] = 68 ul = 8 + len(d); udp[4] = ul >> 8; udp[5] = ul & 0xFF udp += d tot = 20 + len(udp) ip = bytearray(20); ip[0] = 0x45; ip[2] = tot >> 8; ip[3] = tot & 0xFF ip[8] = 64; ip[9] = 17; ip[12:16] = GW; ip[16:20] = b"\xff\xff\xff\xff" ck = _cksum(ip); ip[10] = ck >> 8; ip[11] = ck & 0xFF return bytes(ip) + bytes(udp) def dhcp_intercept(frame, send_raw): if len(frame) < 28 or frame[9] != 17: return False ihl = (frame[0] & 0x0f) * 4 if len(frame) < ihl + 4 or ((frame[ihl + 2] << 8) | frame[ihl + 3]) != 67: return False dhcp = frame[ihl + 8:] mt = _dhcp_mtype(dhcp) if mt == 1: send_raw(_dhcp_reply(dhcp, 2)) elif mt == 3: send_raw(_dhcp_reply(dhcp, 5)) return True 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: fr = bytes(buf) if not dhcp_intercept(fr, lambda pk: conn.sendall(slip_encode(pk))): try: os.write(tun, fr) 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 if len(pkt) < 20 or pkt[16:20] != b"\x0a\x00\x00\x02": continue # only unicast to 10.0.0.2 (drop mcast noise) 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()