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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
|
#!/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)
# ---- built-in DHCP server: the bridge leases the GB 10.0.0.2 ----------------
GB_IP = b"\x0a\x00\x00\x02"; GW = b"\x0a\x00\x00\x01"
def _cksum(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 _dhcp_mtype(dhcp):
p = 240
while p < len(dhcp) and dhcp[p] != 255:
if dhcp[p] == 0: p += 1; continue
if dhcp[p] == 53: return dhcp[p + 2]
p += 2 + dhcp[p + 1]
return 0
def _dhcp_reply(req, mt):
d = bytearray(240)
d[0] = 2; d[1] = 1; d[2] = 6
d[4:8] = req[4:8] # xid
d[16:20] = GB_IP # yiaddr
d[20:24] = GW # siaddr
d[28:44] = req[28:44] # chaddr
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 = _cksum(ip); ip[10] = ck >> 8; ip[11] = ck & 0xFF
return bytes(ip) + bytes(udp)
def dhcp_intercept(frame, send_raw):
"""If frame is a DHCP request (UDP -> :67), answer it and return True."""
if len(frame) < 28 or frame[9] != 17: return False
ihl = (frame[0] & 0x0f) * 4
if len(frame) < ihl + 4 or ((frame[ihl + 2] << 8) | frame[ihl + 3]) != 67: return False
dhcp = frame[ihl + 8:]
mt = _dhcp_mtype(dhcp)
if mt == 1: send_raw(_dhcp_reply(dhcp, 2)) # DISCOVER -> OFFER
elif mt == 3: send_raw(_dhcp_reply(dhcp, 5)) # REQUEST -> ACK
return True
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:
fr = bytes(buf)
if not dhcp_intercept(fr, lambda pk: (p.stdin.write(slip_encode(pk)), p.stdin.flush())):
try: os.write(tun, fr)
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()
|