diff options
Diffstat (limited to 'tools/gbhub')
| -rwxr-xr-x | tools/gbhub | 166 |
1 files changed, 166 insertions, 0 deletions
diff --git a/tools/gbhub b/tools/gbhub new file mode 100755 index 0000000..5f48bfc --- /dev/null +++ b/tools/gbhub @@ -0,0 +1,166 @@ +#!/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() |
