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
|
#!/usr/bin/env python3
"""netboot - bring up real networking (TUN + NAT) and launch sl0pboy with your
own arguments, link port bridged to the TUN over a unix socket.
Unlike the headless `tunbridge`, this keeps the LCD + on-screen keyboard, so you
get a fully interactive, networked Game Boy: the console is the OSK, the link
port is the network - no sharing, no confusion.
sudo tools/netboot --sixel --chrome gbos.gb
sudo tools/netboot gbos.gb
Then, on the Game Boy: press SELECT for the OSK and type e.g.
wget example.com
ping 1.1.1.1
nslookup sl0p.foo
The GB is 10.0.0.2; the host/gateway is 10.0.0.1. Ctrl-C (or quit the emu) to
tear everything down. Requires /dev/net/tun and root.
"""
import os, sys, fcntl, struct, subprocess, socket, threading, atexit
HOME = os.path.expanduser("~" + (os.environ.get("SUDO_USER") or ""))
EMU = os.path.join(HOME, "dev/gbc/build/sl0pboy")
SOCKPATH = "/tmp/sl0pboy-net.sock"
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):
o = bytearray([END])
for c in pkt:
if c == END: o += bytes([ESC, ESC_END])
elif c == ESC: o += bytes([ESC, ESC_ESC])
else: o.append(c)
o.append(END)
return bytes(o)
# ---- built-in DHCP server: lease 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]; d[16:20] = GB_IP; 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 = _cksum(ip); ip[10] = ck >> 8; ip[11] = ck & 0xFF
return bytes(ip) + bytes(udp)
def dhcp_intercept(frame, send_raw):
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))
elif mt == 3: send_raw(_dhcp_reply(dhcp, 5))
return True
def sh(cmd):
subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
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("netboot: must run as root (sudo)")
args = sys.argv[1:]
if not args:
sys.exit("usage: sudo netboot [emulator options] <rom>")
rom, opts = os.path.abspath(args[-1]), args[:-1]
if not os.path.exists(EMU):
sys.exit("netboot: emulator not found at " + EMU)
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))
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)
try: os.unlink(SOCKPATH)
except Exception: pass
atexit.register(cleanup)
try: os.unlink(SOCKPATH)
except Exception: pass
srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
srv.bind(SOCKPATH); srv.listen(1)
sys.stderr.write("[netboot] %s up %s/24, NAT via %s. GB=10.0.0.2. "
"OSK (SELECT) -> wget example.com / ping 1.1.1.1\n" % (IFACE, GW_IP, wan))
emu = subprocess.Popen([EMU] + opts + ["--serial-sock", SOCKPATH, rom])
srv.settimeout(10)
try:
conn, _ = srv.accept()
except socket.timeout:
sys.stderr.write("[netboot] emulator never connected the link socket\n")
emu.kill(); return
conn.setblocking(True)
def sock_to_tun(): # Game Boy -> host
buf, in_frame, esc = bytearray(), False, False
while True:
try: chunk = conn.recv(2048)
except Exception: return
if not chunk: return
for c in chunk:
if c == END:
if in_frame and buf:
fr = bytes(buf)
if not dhcp_intercept(fr, lambda pk: conn.sendall(slip_encode(pk))):
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)
# non-frame bytes are the console mirror; it's on the LCD, ignore
def tun_to_sock(): # host -> Game Boy
while True:
try: pkt = os.read(tun, 2048)
except Exception: return
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: conn.sendall(slip_encode(pkt))
except Exception: return
threading.Thread(target=sock_to_tun, daemon=True).start()
threading.Thread(target=tun_to_sock, daemon=True).start()
try:
emu.wait()
except KeyboardInterrupt:
emu.terminate()
if __name__ == "__main__":
main()
|