aboutsummaryrefslogtreecommitdiffstats
path: root/tools/netboot
blob: 3d27cf8fd84082f632ba11cb76661f134c78668b (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
#!/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)

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:
                        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)
                # 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()