Netboot installer: TFTP/HTTP servers, initramfs, EEPROM artifacts, plan
This commit is contained in:
150
configs/planck-tftp-server.py
Normal file
150
configs/planck-tftp-server.py
Normal file
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Minimal TFTP server (RRQ only) for planck netboot. Supports blksize and
|
||||
windowsize options per RFC 2347/2348/2349. Serves files under TFTP_ROOT."""
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
|
||||
TFTP_ROOT = "/volume1/plancknetboot/tftp"
|
||||
PORT = 69
|
||||
BLKSIZE_MAX = 1468 # avoids IP fragmentation on ethernet
|
||||
DEFAULT_BLKSIZE = 512
|
||||
DEFAULT_WINDOW = 1
|
||||
TIMEOUT = 5
|
||||
RETRIES = 6
|
||||
|
||||
OP_RRQ = 1
|
||||
OP_DATA = 3
|
||||
OP_ACK = 4
|
||||
OP_ERROR = 5
|
||||
OP_OACK = 6
|
||||
|
||||
|
||||
def parse_rrq(data):
|
||||
# opcode, filename, mode, optional opts
|
||||
parts = data[2:].split(b"\x00")
|
||||
filename = parts[0].decode(errors="replace")
|
||||
opts = {}
|
||||
rest = parts[2:]
|
||||
for i in range(0, len(rest) - 1, 2):
|
||||
try:
|
||||
opts[rest[i].decode().lower()] = rest[i + 1].decode()
|
||||
except (IndexError, UnicodeDecodeError):
|
||||
pass
|
||||
return filename, opts
|
||||
|
||||
|
||||
def safe_path(filename):
|
||||
parts = filename.split("/")
|
||||
if any(not p or p.startswith(".") or "/" in p for p in parts):
|
||||
return None
|
||||
if any(not all(c.isalnum() or c in "._-" for c in p) for p in parts):
|
||||
return None
|
||||
path = os.path.join(TFTP_ROOT, *parts)
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
return path
|
||||
|
||||
|
||||
def send_err(sock, addr, code, msg):
|
||||
sock.sendto(struct.pack("!HH", OP_ERROR, code) + msg.encode() + b"\x00", addr)
|
||||
|
||||
|
||||
def handle_client(sock, data, addr, filepath, opts):
|
||||
blksize = DEFAULT_BLKSIZE
|
||||
window = DEFAULT_WINDOW
|
||||
requested = {}
|
||||
if "blksize" in opts:
|
||||
try:
|
||||
blksize = min(BLKSIZE_MAX, max(512, int(opts["blksize"])))
|
||||
requested["blksize"] = str(blksize)
|
||||
except ValueError:
|
||||
pass
|
||||
if "windowsize" in opts:
|
||||
try:
|
||||
window = min(64, max(1, int(opts["windowsize"])))
|
||||
requested["windowsize"] = str(window)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
size = os.path.getsize(filepath)
|
||||
if requested:
|
||||
payload = b"\x00".join(
|
||||
k.encode() + b"\x00" + v.encode() for k, v in requested.items()
|
||||
)
|
||||
sock.sendto(struct.pack("!H", OP_OACK) + payload + b"\x00", addr)
|
||||
else:
|
||||
blksize = 512
|
||||
window = 1
|
||||
|
||||
with open(filepath, "rb") as f:
|
||||
block = 0
|
||||
eof = False
|
||||
while not eof:
|
||||
# send one window
|
||||
for _ in range(window):
|
||||
block = (block % 65536) + 1
|
||||
chunk = f.read(blksize)
|
||||
pkt = struct.pack("!HH", OP_DATA, block) + chunk
|
||||
sock.sendto(pkt, addr)
|
||||
if len(chunk) < blksize:
|
||||
eof = True
|
||||
# wait for the ACK of the last block of this window
|
||||
expect = block
|
||||
retries = 0
|
||||
while True:
|
||||
sock.settimeout(TIMEOUT)
|
||||
try:
|
||||
rdata, raddr = sock.recvfrom(4 + blksize)
|
||||
except socket.timeout:
|
||||
retries += 1
|
||||
if retries > RETRIES:
|
||||
return
|
||||
# retransmit window
|
||||
f.seek((expect - window) * blksize if expect >= window else 0)
|
||||
block = expect - window if expect >= window else 0
|
||||
eof = os.path.getsize(filepath) <= f.tell() + window * blksize
|
||||
break
|
||||
if len(rdata) >= 4 and struct.unpack("!H", rdata[:2])[0] == OP_ACK:
|
||||
acked = struct.unpack("!H", rdata[2:4])[0]
|
||||
if acked == expect:
|
||||
break
|
||||
elif acked < expect:
|
||||
continue # stale ack
|
||||
else:
|
||||
continue # shouldn't happen
|
||||
|
||||
|
||||
def main():
|
||||
srv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
srv.bind(("0.0.0.0", PORT))
|
||||
print(f"planck-tftp: serving {TFTP_ROOT} on :{PORT}", flush=True)
|
||||
while True:
|
||||
data, addr = srv.recvfrom(1024)
|
||||
if len(data) < 4 or struct.unpack("!H", data[:2])[0] != OP_RRQ:
|
||||
continue
|
||||
try:
|
||||
filename, opts = parse_rrq(data)
|
||||
except Exception:
|
||||
continue
|
||||
filepath = safe_path(filename)
|
||||
if filepath is None:
|
||||
errsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
send_err(errsock, addr, 1, "file not found")
|
||||
errsock.close()
|
||||
print(f"planck-tftp: RRQ denied {filename} from {addr}", flush=True)
|
||||
continue
|
||||
csock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
csock.bind(("0.0.0.0", 0))
|
||||
print(f"planck-tftp: RRQ {filename} from {addr}", flush=True)
|
||||
threading.Thread(
|
||||
target=handle_client, args=(csock, data, addr, filepath, opts),
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user