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