aboutsummaryrefslogtreecommitdiffstats
path: root/tools/gbhub
diff options
context:
space:
mode:
authoruser <user@clank>2026-07-17 14:26:06 +0200
committeruser <user@clank>2026-07-17 14:26:06 +0200
commit584b19508b31f444ae02a974b96d1a50b8e650a2 (patch)
treeff8698446e6337b235482b5dcb9f19ae2fb5091d /tools/gbhub
parenttools: gbhub - many Game Boys on one network (DHCP pool + inter-GB routing) (diff)
downloadgbos-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-xtools/gbhub77
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)