aboutsummaryrefslogtreecommitdiffstats
path: root/tools/netboot
blob: bca93ee6496b6687ab3f8dc2b15a9bff87f28269 (plain) (blame)
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()