1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
#!/usr/bin/env python3
"""tunbridge.py - give gbos a real routed IP over the link port.
Opens a TUN device (gbtun0, 10.0.0.1/24), NATs it to the internet, spawns the
emulator, and bridges: SLIP frames on the link serial <-> raw IP packets on the
TUN. The Game Boy (10.0.0.2) becomes a genuine IP host - the host kernel routes
its packets for real. Run as root: sudo python3 tools/tunbridge.py
TLS is still not the GB's problem; plaintext services work end to end.
"""
import os, sys, fcntl, struct, subprocess, threading, time, signal, atexit
# resolve paths against the invoking (non-root) user's home, since sudo makes ~ = /root
_HOME = os.path.expanduser("~" + (os.environ.get("SUDO_USER") or ""))
EMU = os.path.join(_HOME, "dev/gbc/build/sl0pboy")
ROM = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "gbos.gb"))
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):
out = bytearray([END])
for c in pkt:
if c == END: out += bytes([ESC, ESC_END])
elif c == ESC: out += bytes([ESC, ESC_ESC])
else: out.append(c)
out.append(END)
return bytes(out)
def sh(cmd, quiet=True):
subprocess.run(cmd, shell=True, check=False,
stdout=subprocess.DEVNULL if quiet else None,
stderr=subprocess.DEVNULL if quiet else None)
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("must run as root (sudo)")
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))
sys.stderr.write("[tun] %s up %s/24, NAT via %s, GB is 10.0.0.2\n" % (IFACE, GW_IP, wan))
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)
atexit.register(cleanup)
p = subprocess.Popen([EMU, "--headless", "--uncapped", ROM],
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, bufsize=0, start_new_session=True)
def tun_to_gb(): # host -> Game Boy
while True:
pkt = os.read(tun, 2048)
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: p.stdin.write(slip_encode(pkt)); p.stdin.flush()
except Exception: return
def gb_to_tun(): # Game Boy -> host
buf, in_frame, esc = bytearray(), False, False
while True:
b = p.stdout.read(1)
if not b: return
c = b[0]
if c == END:
if in_frame and buf:
try: os.write(tun, bytes(buf))
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)
else:
sys.stderr.write(chr(c) if 32 <= c < 127 else ("\n" if c == 10 else ""))
# Drain the GB->host direction from the start, but only start feeding host->GB
# packets AFTER netd is running: raw packet bytes fed to gbos's shell (before
# netd claims the serial) corrupt the shell and crash the emulator.
threading.Thread(target=gb_to_tun, daemon=True).start()
args = sys.argv[1:]
testmode = "--test" in args
if testmode: args.remove("--test")
cmd = args[0] if args else "netd"
time.sleep(1.0)
p.stdin.write(cmd.encode() + b"\n"); p.stdin.flush()
time.sleep(0.8)
threading.Thread(target=tun_to_gb, daemon=True).start()
sys.stderr.write("[tun] running %s on the GB\n" % cmd)
if testmode:
time.sleep(3.0)
r = subprocess.run("ping -c 4 -W 2 10.0.0.2", shell=True,
capture_output=True, text=True)
sys.stderr.write("\n=== ping 10.0.0.2 (host -> Game Boy, real routing) ===\n")
sys.stderr.write(r.stdout + r.stderr + "\n")
p.kill(); return
sys.stderr.write("[tun] try: ping 10.0.0.2 (Ctrl-C to stop)\n")
signal.signal(signal.SIGTERM, lambda *a: sys.exit(0))
try:
while p.poll() is None: time.sleep(0.5)
except KeyboardInterrupt:
pass
finally:
p.kill()
if __name__ == "__main__":
main()
|