commit c484f4fe3bd08fe03a55ae70f7091020018d6def
parent 600bb479fe6e810286dfc133040072265f03d97a
Author: Brian Swetland <swetland@frotz.net>
Date:   Mon, 23 May 2022 01:38:45 -0700
netboot: add ipv6 netboot utility
Diffstat:
3 files changed, 347 insertions(+), 1 deletion(-)
diff --git a/Makefile b/Makefile
@@ -44,7 +44,7 @@ BUILD := out
 CFLAGS := -g -Wall -Ihw/inc
 CFLAGS += -O2
 
-ALL :=
+ALL := out/netboot
 
 LIBC_SRC := libc/src/printf.c $(wildcard libc/src/string/*.c)
 
@@ -55,3 +55,7 @@ build-all: $(ALL)
 
 clean::
 	rm -rf $(BUILD)
+
+out/netboot: tools/netboot.c tools/netboot.h
+	$(CC) -Wall -O2 -g -o out/netboot tools/netboot.c
+
diff --git a/tools/netboot.c b/tools/netboot.c
@@ -0,0 +1,297 @@
+// Copyright 2022, Brian Swetland <swetland@frotz.net>
+// Licensed under the Apache License, Version 2.0.
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <poll.h>
+#include <time.h>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+#include <net/if.h>
+#include <fcntl.h>
+
+#include "netboot.h"
+
+uint64_t get_time_ms(void) {
+	struct timespec ts;
+	if (clock_gettime(CLOCK_MONOTONIC, &ts)) {
+		exit(-1);
+	}
+	return (((uint64_t)ts.tv_nsec) / 1000000ULL) + (((uint64_t)ts.tv_sec) * 1000ULL);
+}
+
+int open_udp6_socket(struct sockaddr_in6 *addr, int con) {
+	int s, n = 1;
+	if ((s = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
+		perror("open_udp6_socket: open socket");
+		return -1;
+	}
+	if (setsockopt(s, SOL_SOCKET, SO_REUSEPORT, &n, sizeof(n)) < 0) {
+		perror("open_udp6_socket: SO_REUSEPORT");
+		goto fail;
+	}
+	if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &n, sizeof(n)) < 0) {
+		perror("open_udp6_socket: SO_REUSEADDR");
+		goto fail;
+	}
+	if (fcntl(s, F_SETFL, O_NONBLOCK) < 0) {
+		perror("open_ud6_socket: set O_NONBLOCK");
+		goto fail;
+	}
+	if (con) {
+		if (connect(s, (struct sockaddr*) addr, sizeof(struct sockaddr_in6)) < 0) {
+			perror("open_udp6_socket: connect");
+			goto fail;
+		}
+	} else {
+		if (bind(s, (struct sockaddr*) addr, sizeof(struct sockaddr_in6)) < 0) {
+			perror("open_udp6_socket: bind");
+			goto fail;
+		}
+	}
+	return s;
+fail:
+	close(s);
+	return -1;
+}
+
+static uint32_t seq = 1;
+
+int open_node(int idx, const char *nodename) {
+	int s, r;
+
+	struct sockaddr_in6 addr = {
+		.sin6_family = AF_INET6,
+		.sin6_scope_id = idx,
+	};
+	if ((s = open_udp6_socket(&addr, 0)) < 0) {
+		return -1;
+	}
+
+	struct sockaddr_in6 mcast = {
+		.sin6_family = AF_INET6,
+		.sin6_port = htons(NB_PORT_QUERY),
+		.sin6_scope_id = idx,
+	};
+	inet_pton(AF_INET6, "ff02::1", &mcast.sin6_addr);
+
+	netboot_msg_t msg = {
+		.magic = NB_MAGIC,
+		.cmd = NB_CMD_QUERY,
+		.arg = 0,
+	};
+	uint32_t msglen = strlen(nodename);
+	if (msglen > NB_MSG_MAX) {
+		return -1;
+	}
+	memcpy(msg.db, nodename, msglen);
+	msglen += NB_MSG_MIN;
+
+	uint64_t next = get_time_ms() + 500;
+	for (;;) {
+		uint64_t now = get_time_ms();
+		if (now >= next) {
+			next = now + 500;
+			msg.seq = ++seq;
+			r = sendto(s, &msg, msglen, 0, (struct sockaddr*) &mcast, sizeof(mcast));
+			if (r != msglen) {
+				perror("write");
+				return -1;
+			}
+		}
+		struct pollfd pfd = {
+			.fd = s,
+			.events = POLLIN,
+		};
+		if (poll(&pfd, 1, next - now) == 1) {
+			uint8_t rxb[2048];
+			struct sockaddr_in6 rxaddr;
+			socklen_t rxalen = sizeof(rxaddr);
+			if ((r = recvfrom(s, rxb, sizeof(rxb), 1, (void*)&rxaddr, &rxalen)) > 0) {
+				netboot_msg_t *msg = (void*)rxb;
+				if (r < NB_MSG_MIN) {
+					continue;
+				}
+				if ((msg->magic != NB_MAGIC) ||
+					(msg->cmd != NB_CMD_STATUS) ||
+					(msg->seq != seq)) {
+					continue;
+				}
+				char _tmp[128];
+				const char *tmp = inet_ntop(AF_INET6, &rxaddr.sin6_addr, _tmp, sizeof(_tmp));
+				fprintf(stderr, "[ located '%s' at %s/%u ]\n", nodename,
+					tmp ? tmp : "unknown", rxaddr.sin6_scope_id);
+
+				int s0 = open_udp6_socket(&rxaddr, 1);
+				if (s0 >= 0) {
+					close(s);
+					return s0;
+				}
+			}
+		}
+	}
+}
+
+
+int send_msg(int s, netboot_msg_t *msg, unsigned len) {
+	int r;
+	for (int retry = 0; retry < 5; retry++) {
+		if ((r = write(s, msg, len)) != len) {
+			return -1;
+		}
+		struct pollfd pfd = {
+			.fd = s,
+			.events = POLLIN,
+		};
+		if (poll(&pfd, 1, 250) != 1) {
+			fprintf(stderr, "T");
+			continue;
+		}
+		netboot_msg_t rsp;
+		r = read(s, &rsp, sizeof(rsp));
+		if ((r < NB_MSG_MIN) || (rsp.magic != NB_MAGIC) || (rsp.cmd != NB_CMD_STATUS)) {
+			fprintf(stderr, "X");
+			continue;
+		}
+		if (rsp.arg != NB_OK) {
+			fprintf(stderr, "E");
+			return -1;
+		}
+		fprintf(stderr, ".");
+		return 0;
+	}
+	return -1;
+}
+
+int send_file(int s, const char* fn, uint32_t addr) {
+	int r;
+
+	netboot_msg_t msg;
+	msg.magic = NB_MAGIC;
+	msg.cmd = NB_CMD_WRITE;
+
+	int fd = open(fn, O_RDONLY);
+	if (fd < 0) {
+		fprintf(stderr, "error: cannot open '%s'\n", fn);
+		return -1;
+	}
+	fprintf(stderr, "[ sending '%s' to 0x%08x ]\n", fn, addr);
+	for (;;) {
+		if ((r = read(fd, msg.db, NB_DATA_MAX)) <= 0) {
+			close(fd);
+			return r;
+		}
+		msg.arg = addr;
+		addr += r;
+		if (send_msg(s, &msg, r + NB_MSG_MIN)) {
+			close(fd);
+			return -1;
+		}
+	}
+}
+
+typedef struct {
+	const char* name;
+	uint32_t addr;
+} image_t;
+
+#define IMGMAX 8
+
+#define consume() \
+	do { argc--; argv++; if (argc == 1) { \
+		fprintf(stderr, "error: missing argument\n"); \
+		return -1; } \
+	} while (0)
+
+void usage(void) {
+	fprintf(stderr, "\n"
+	"usage:    netboot <option>* <image>+\n"
+	"\n"
+	"option:   -i <interface-name>\n"
+	"          -n <node-name>\n"
+	"\n"
+	"image:    <filename>@<hex-load-address>\n"
+	"\n"
+	"Send one or more images to <node-name> on <interface-name>.\n"
+	"Start execution at the address of the first image.\n\n");
+}
+
+int main(int argc, char **argv) {
+	int s, idx;
+
+	const char *ifname = "qemu0";
+	const char *nodename = "device";
+	image_t imagelist[IMGMAX];
+	int imagecount = 0;
+
+	char *tmp;
+	while (argc > 1) {
+		if ((tmp = strchr(argv[1], '@'))) {
+			if (imagecount == IMGMAX) {
+				fprintf(stderr, "error: too many images\n");
+				return -1;
+			}
+			*tmp++ = 0;
+			imagelist[imagecount].name = argv[1];
+			imagelist[imagecount].addr = strtoul(tmp, NULL, 16);
+			imagecount++;
+		} else if (!strcmp("-i", argv[1])) {
+			consume();
+			ifname = argv[1];
+		} else if (!strcmp("-n", argv[1])) {
+			consume();
+			nodename = argv[1];
+		} else if (!strcmp("-h", argv[1])) {
+			usage();
+			return 0;
+		} else {
+			fprintf(stderr, "error: unknown argument '%s'\n", argv[1]);
+			return -1;
+		} 
+		argc--;
+		argv++;
+	}
+
+	if (imagecount == 0) {
+		fprintf(stderr, "error: no files to send?\n");
+		return -1;
+	}
+	if ((idx = if_nametoindex(ifname)) == 0) {
+		fprintf(stderr, "unknown network interface: '%s'\n", argv[1]);
+		return -1;
+	}
+
+	fprintf(stderr, "\n[ netboot v0.1 ]\n");
+	fprintf(stderr, "[ netifc '%s' ]\n", ifname);
+	fprintf(stderr, "[ nodename '%s' ]\n", nodename);
+
+	for (;;) {
+again:
+		if ((s = open_node(idx, "device")) < 0) {
+			return -1;
+		}
+
+		for (unsigned n = 0; n < imagecount; n++) {
+			if (send_file(s, imagelist[n].name, imagelist[n].addr)) {
+				fprintf(stderr, "\n[ send failure ]\n");
+				close(s);
+				goto again;
+			}
+		}
+		fprintf(stderr, "\n[ start execution at 0x%08x ]\n", imagelist[0].addr);
+
+		netboot_msg_t msg;
+		msg.magic = NB_MAGIC;
+		msg.cmd = NB_CMD_EXEC;
+		msg.seq = ++seq;
+		msg.arg = imagelist[0].addr;
+		send_msg(s, &msg, NB_MSG_MIN);
+		fprintf(stderr, "\n");
+	}
+
+	return 0;
+}
diff --git a/tools/netboot.h b/tools/netboot.h
@@ -0,0 +1,45 @@
+// Copyright 2022, Brian Swetland <swetland@frotz.net>
+// Licensed under the Apache License, Version 2.0
+
+#pragma once
+
+#include <stdint.h>
+
+#define NB_PORT_QUERY  32320
+#define NB_PORT_CTRL   31310
+
+// echo -n netboot.magic | sha256sum | cut -c1-8
+// commands are similar (netboot.cmd.name)
+#define NB_MAGIC       0x1e5dae11U
+
+// host to target commands
+#define NB_CMD_QUERY   0x56a938e6U // arg=0, db=targetname.ascii
+#define NB_CMD_WRITE   0x7b639621U // arg=addr, db=bytes
+#define NB_CMD_READ    0x1c1d1ecfU // arg=addr, reply.db=bytes
+#define NB_CMD_EXEC    0xbdb9a844U // arg=addr
+
+// target to host responses
+#define NB_CMD_STATUS  0x9e68e03bU // arg=status (0=OK)
+
+// target to multicast messages
+#define NB_CMD_SYSLOG  0x5a374503U // arg=0, db=logmsgs.ascii
+
+#define NB_OK          0
+#define NB_ERR_BADCMD  1
+#define NB_ERR_PARAM   2
+
+#define NB_MSG_MIN     16
+#define NB_MSG_MAX     (16 + 1024)
+#define NB_DATA_MAX    1024
+
+typedef struct {
+	uint32_t magic;
+	uint32_t cmd;
+	uint32_t seq;
+	uint32_t arg;
+	union {
+		uint8_t db[1024];
+		uint32_t dw[1024 / 4];
+	};
+} netboot_msg_t;
+