#!/usr/bin/env python3 """gbhub - a virtual switch/router + DHCP for several Game Boys on one network. Spawns N emulators, leases each a distinct IP (10.0.0.2, 10.0.0.3, ...), routes packets between them (so they can ping/connect to *each other*), floods broadcasts, and NATs external traffic out a TUN (so they also reach the internet). No kernel changes: the GB just sends to a dst IP over its link and the hub decides where it goes. sudo tools/gbhub 2 # 2 Game Boys, one network sudo tools/gbhub 2 netd "ping 10.0.0.2" # scripted: GB0 serves, GB1 pings it Each positional arg after N is a console command auto-typed into that GB once it has a lease (a test/demo hook; interactively you'd use the OSK). Requires root. """ import os, sys, socket, threading, subprocess, struct, fcntl, time, signal, atexit, shutil HOME = os.path.expanduser("~" + (os.environ.get("SUDO_USER") or "")) EMU = os.path.join(HOME, "dev/gbc/build/sl0pboy") ROM = os.path.join(HOME, "dev/gbos/gbos.gb") SOCK = "/tmp/gbhub.sock" IFACE, GW, MTU = "gbtun0", b"\x0a\x00\x00\x01", 320 END, ESC, BCAST = 0xC0, 0xDB, b"\xff\xff\xff\xff" def slip(pkt): o = bytearray([END]) for c in pkt: o += bytes([ESC,0xDC]) if c==END else bytes([ESC,0xDD]) if c==ESC else bytes([c]) o.append(END); return bytes(o) def _ck(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 _dmt(dh): p = 240 while p < len(dh) and dh[p] != 255: if dh[p]==0: p += 1; continue if dh[p]==53: return dh[p+2] p += 2 + dh[p+1] return 0 def _dreply(req, mt, gbip): d = bytearray(240); d[0]=2; d[1]=1; d[2]=6; d[4:8]=req[4:8] d[16:20]=gbip; 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=_ck(ip); ip[10]=ck>>8; ip[11]=ck&0xFF return bytes(ip)+bytes(udp) def sh(cmd): subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) conns = {} # ip(bytes) -> GB tun = None class GB: def __init__(self, idx, conn): self.idx, self.conn = idx, conn self.ip = bytes([10,0,0,2+idx]) self.wlock = threading.Lock() self.line = "" def send(self, data): with self.wlock: try: self.conn.sendall(data) except Exception: pass def frame(self, pkt): self.send(slip(pkt)) def type(self, text): self.send(text.encode()) # non-framed = console input def route(gb, fr): if len(fr) >= 28 and fr[9] == 17: # DHCP -> answer with this GB's IP ihl = (fr[0]&0x0f)*4 if ((fr[ihl+2]<<8)|fr[ihl+3]) == 67: mt = _dmt(fr[ihl+8:]) if mt == 1: gb.frame(_dreply(fr[ihl+8:], 2, gb.ip)) elif mt == 3: gb.frame(_dreply(fr[ihl+8:], 5, gb.ip)) return if len(fr) < 20: return dst = fr[16:20] if dst == BCAST: for g in conns.values(): if g is not gb: g.frame(fr) elif dst in conns: conns[dst].frame(fr) # Game Boy -> Game Boy elif tun is not None: try: os.write(tun, fr) # -> internet via NAT except Exception: pass def gb_reader(gb): buf, inf, esc = bytearray(), False, False while True: try: chunk = gb.conn.recv(2048) except Exception: return if not chunk: return for c in chunk: if c == END: if inf and buf: route(gb, bytes(buf)); buf=bytearray(); inf=False else: buf=bytearray(); inf=True; esc=False elif inf: if esc: c={0xDC:END,0xDD:ESC}.get(c,c); esc=False; buf.append(c) elif c==ESC: esc=True else: buf.append(c) else: # console output -> log per GB if c == 10: sys.stderr.write("[GB%d] %s\n" % (gb.idx, gb.line)); sys.stderr.flush(); gb.line="" elif 32 <= c < 127: gb.line += chr(c) def tun_reader(): while True: try: pkt = os.read(tun, 2048) except Exception: return if len(pkt) >= 20 and pkt[16:20] in conns: conns[pkt[16:20]].frame(pkt) def main(): global tun if os.geteuid() != 0: sys.exit("gbhub: run as root") n = int(sys.argv[1]) if len(sys.argv) > 1 else 2 cmds = sys.argv[2:2+n] tun = os.open("/dev/net/tun", os.O_RDWR) fcntl.ioctl(tun, 0x400454ca, struct.pack("16sH", IFACE.encode(), 0x0001|0x1000)) wan = subprocess.check_output("ip route show default|awk '{print $5;exit}'", shell=True).decode().strip() or "eth0" sh("ip addr add 10.0.0.1/24 dev %s" % 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 10.0.0.0/24 -o %s -j MASQUERADE || iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o %s -j MASQUERADE" % (wan, 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 10.0.0.0/24 -o %s -j MASQUERADE" % wan) sh("iptables -D FORWARD -i %s -j ACCEPT" % IFACE); sh("iptables -D FORWARD -o %s -j ACCEPT" % IFACE) atexit.register(cleanup) try: os.unlink(SOCK) except Exception: pass srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); srv.bind(SOCK); srv.listen(n) emus = [] for i in range(n): rom = "/tmp/gbhub_%d.gb" % i # per-GB ROM so battery saves don't clash shutil.copyfile(ROM, rom) emus.append(subprocess.Popen([EMU, "--headless", "--uncapped", "--serial-sock", SOCK, rom], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)) sys.stderr.write("[gbhub] %d Game Boys on 10.0.0.0/24 (10.0.0.2..%d), NAT via %s\n" % (n, 1+n, wan)) srv.settimeout(10) for i in range(n): conn, _ = srv.accept(); gb = GB(i, conn); conns[gb.ip] = gb threading.Thread(target=gb_reader, args=(gb,), daemon=True).start() threading.Thread(target=tun_reader, daemon=True).start() time.sleep(2.0) # let DHCP settle for i in range(n): if i < len(cmds) and cmds[i]: conns[bytes([10,0,0,2+i])].type(cmds[i] + "\n") def stop(*a): for e in emus: try: os.killpg(os.getpgid(e.pid), signal.SIGKILL) except Exception: pass sys.exit(0) signal.signal(signal.SIGTERM, stop) try: while all(e.poll() is None for e in emus): time.sleep(0.3) except KeyboardInterrupt: pass finally: stop() if __name__ == "__main__": main()