diff options
| author | user <user@clank> | 2026-07-17 14:26:06 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-07-17 14:26:06 +0200 |
| commit | 584b19508b31f444ae02a974b96d1a50b8e650a2 (patch) | |
| tree | ff8698446e6337b235482b5dcb9f19ae2fb5091d /tools/gbhub | |
| parent | tools: gbhub - many Game Boys on one network (DHCP pool + inter-GB routing) (diff) | |
| download | gbos-584b19508b31f444ae02a974b96d1a50b8e650a2.tar.gz gbos-584b19508b31f444ae02a974b96d1a50b8e650a2.tar.xz gbos-584b19508b31f444ae02a974b96d1a50b8e650a2.zip | |
tools: gbhub --daemon + gbjoin - interactive multi-Game-Boy networking
gbhub gains a --daemon mode: instead of spawning headless emulators, it just
runs the switch/router/DHCP and accepts link connections, handing each a lease
from a pool (10.0.0.2..) and freeing it on disconnect. The link socket is made
world-connectable so unprivileged emulators can join.
gbjoin (no root) launches a *windowed* Game Boy and plugs its link into the
running hub, with its own ROM copy so battery saves don't clash. So you get real
interactive Game Boys on one network:
term 1: sudo tools/gbhub --daemon
term 2: tools/gbjoin # windowed GB -> 10.0.0.2
term 3: tools/gbjoin # windowed GB -> 10.0.0.3
(on each: SELECT for the OSK, then e.g. ping 10.0.0.3)
Verified two GBs join the daemon and get distinct leases; routing is the same
code path as the (already-verified) spawn-mode GB<->GB ping.
Diffstat (limited to 'tools/gbhub')
| -rwxr-xr-x | tools/gbhub | 77 |
1 files changed, 57 insertions, 20 deletions
diff --git a/tools/gbhub b/tools/gbhub index 5f48bfc..3823463 100755 --- a/tools/gbhub +++ b/tools/gbhub @@ -7,11 +7,14 @@ 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 # spawn 2 headless GBs, one network sudo tools/gbhub 2 netd "ping 10.0.0.2" # scripted: GB0 serves, GB1 pings it + sudo tools/gbhub --daemon # just the hub; join windowed GBs with + # tools/gbjoin (in other terminals) -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. +In spawn mode each positional arg after N is a console command auto-typed into +that GB once it has a lease (a demo hook; interactively you'd use the OSK). +Requires root. """ import os, sys, socket, threading, subprocess, struct, fcntl, time, signal, atexit, shutil @@ -51,12 +54,14 @@ def _dreply(req, mt, gbip): def sh(cmd): subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) conns = {} # ip(bytes) -> GB +pool = list(range(2, 40)) # free host octets (10.0.0.2 .. .39) +lock = threading.Lock() tun = None class GB: - def __init__(self, idx, conn): - self.idx, self.conn = idx, conn - self.ip = bytes([10,0,0,2+idx]) + def __init__(self, octet, conn): + self.octet, self.idx, self.conn = octet, octet - 2, conn + self.ip = bytes([10,0,0,octet]) self.wlock = threading.Lock() self.line = "" def send(self, data): @@ -89,8 +94,13 @@ 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 + except Exception: chunk = None + if not chunk: + with lock: # GB left: free its address + conns.pop(gb.ip, None) + if gb.octet not in pool: pool.append(gb.octet); pool.sort() + sys.stderr.write("[gbhub] GB%d (10.0.0.%d) left\n" % (gb.idx, gb.octet)) + return for c in chunk: if c == END: if inf and buf: route(gb, bytes(buf)); buf=bytearray(); inf=False @@ -112,11 +122,8 @@ def tun_reader(): if len(pkt) >= 20 and pkt[16:20] in conns: conns[pkt[16:20]].frame(pkt) -def main(): +def net_setup(): 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" @@ -128,29 +135,59 @@ def main(): 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) + try: os.unlink(SOCK) + except Exception: pass atexit.register(cleanup) + return wan + +def register(conn): + with lock: + if not pool: conn.close(); return None + octet = pool.pop(0) + gb = GB(octet, conn); conns[gb.ip] = gb + threading.Thread(target=gb_reader, args=(gb,), daemon=True).start() + sys.stderr.write("[gbhub] GB%d joined -> 10.0.0.%d\n" % (gb.idx, octet)) + return gb +def main(): + if os.geteuid() != 0: sys.exit("gbhub: run as root") + args = sys.argv[1:] + daemon = bool(args) and args[0] in ("--daemon", "-d") + wan = net_setup() try: os.unlink(SOCK) except Exception: pass - srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); srv.bind(SOCK); srv.listen(n) + srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); srv.bind(SOCK); srv.listen(8) + threading.Thread(target=tun_reader, daemon=True).start() + + if daemon: + os.chmod(SOCK, 0o777) # let non-root gbjoin connect + sys.stderr.write("[gbhub] up on 10.0.0.0/24, NAT via %s. " + "join Game Boys in other terminals: tools/gbjoin\n" % wan) + try: + while True: + conn, _ = srv.accept(); register(conn) + except KeyboardInterrupt: pass + return + + # spawn mode: launch N headless GBs ourselves (self-contained demo) + n = int(args[0]) if args else 2 + cmds = args[1:1+n] emus = [] for i in range(n): - rom = "/tmp/gbhub_%d.gb" % i # per-GB ROM so battery saves don't clash + rom = "/tmp/gbhub_%d.gb" % i # per-GB ROM so 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) + srv.settimeout(12) 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() - + conn, _ = srv.accept(); register(conn) 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") + g = conns.get(bytes([10,0,0,2+i])) + if g: g.type(cmds[i] + "\n") def stop(*a): for e in emus: try: os.killpg(os.getpgid(e.pid), signal.SIGKILL) |
