| // Copyright 2016 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #define _POSIX_C_SOURCE 200809L |
| #define _GNU_SOURCE |
| #define _DARWIN_C_SOURCE |
| |
| #include <inttypes.h> |
| #include <stdbool.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| |
| #include <arpa/inet.h> |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <netinet/in.h> |
| #include <sys/socket.h> |
| #include <sys/stat.h> |
| #include <sys/time.h> |
| #include <unistd.h> |
| |
| #include <zircon/boot/netboot.h> |
| |
| #include "bootserver.h" |
| |
| char* appname; |
| int64_t us_between_packets = DEFAULT_US_BETWEEN_PACKETS; |
| bool use_filename_prefix = true; |
| |
| static bool use_tftp = false; |
| static size_t total_file_size; |
| static int progress_reported; |
| static int packets_sent; |
| static struct timeval start_time, end_time; |
| static bool is_redirected; |
| static const char spinner[] = {'|', '/', '-', '\\'}; |
| |
| void initialize_status(const char* name, size_t size) { |
| total_file_size = size; |
| progress_reported = 0; |
| packets_sent = 0; |
| size_t prefix_len = strlen(NB_FILENAME_PREFIX); |
| const char* base_name; |
| if (!strncmp(name, NB_FILENAME_PREFIX, prefix_len)) { |
| base_name = &name[prefix_len]; |
| } else { |
| base_name = name; |
| } |
| fprintf(stderr, "Sending %s [%lu bytes]:\n", base_name, (unsigned long)size); |
| } |
| |
| void update_status(size_t bytes_so_far) { |
| packets_sent++; |
| if (total_file_size == 0) { |
| return; |
| } |
| if (is_redirected) { |
| int percent_sent = (bytes_so_far / (total_file_size / 100)); |
| if (percent_sent - progress_reported >= 5) { |
| fprintf(stderr, "%d%%...", percent_sent); |
| progress_reported = percent_sent; |
| } |
| } else { |
| if (packets_sent > 1024) { |
| packets_sent = 0; |
| float bw = 0; |
| static int spin = 0; |
| |
| struct timeval now; |
| gettimeofday(&now, NULL); |
| int64_t us_since_begin = ((int64_t)(now.tv_sec - start_time.tv_sec) * 1000000) + |
| ((int64_t)now.tv_usec - start_time.tv_usec); |
| if (us_since_begin >= 1000000) { |
| bw = (float)bytes_so_far / (1024.0 * 1024.0 * ((float)us_since_begin / 1000000)); |
| } |
| |
| fprintf(stderr, "\33[2K\r"); |
| if (total_file_size > 0) { |
| fprintf(stderr, "%c %.01f%%", spinner[(spin++) % 4], |
| 100.0 * (float)bytes_so_far / (float)total_file_size); |
| } else { |
| fprintf(stderr, "%c", spinner[(spin++) % 4]); |
| } |
| if (bw > 0.1) { |
| fprintf(stderr, " %.01fMB/s", bw); |
| } |
| } |
| } |
| } |
| |
| static int xfer(struct sockaddr_in6* addr, const char* local_name, const char* remote_name) { |
| int result; |
| is_redirected = !isatty(fileno(stdout)); |
| gettimeofday(&start_time, NULL); |
| if (use_tftp) { |
| result = tftp_xfer(addr, local_name, remote_name); |
| } else { |
| result = netboot_xfer(addr, local_name, remote_name); |
| } |
| gettimeofday(&end_time, NULL); |
| if (end_time.tv_usec < start_time.tv_usec) { |
| end_time.tv_sec -= 1; |
| end_time.tv_usec += 1000000; |
| } |
| if (result == 0) { |
| fprintf(stderr, "\nTransfer completed in %d.%06d sec\n", |
| (int)(end_time.tv_sec - start_time.tv_sec), |
| (int)(end_time.tv_usec - start_time.tv_usec)); |
| } |
| return result; |
| } |
| |
| void usage(void) { |
| fprintf(stderr, |
| "usage: %s [ <option> ]* <kernel> [ <ramdisk> ] [ -- [ <kerneloption> ]* ]\n" |
| "\n" |
| "options:\n" |
| " -1 only boot once, then exit\n" |
| " -a only boot device with this IPv6 address\n" |
| " -b <sz> tftp block size (default=%d, ignored with --netboot)\n" |
| " -i <NN> number of microseconds between packets\n" |
| " set between 50-500 to deal with poor bootloader network stacks (default=%d)\n" |
| " (ignored with --tftp)\n" |
| " -n only boot device with this nodename\n" |
| " -w <sz> tftp window size (default=%d, ignored with --netboot)\n" |
| " --netboot use the netboot protocol (default)\n" |
| " --tftp use the tftp protocol\n", |
| appname, DEFAULT_TFTP_BLOCK_SZ, DEFAULT_US_BETWEEN_PACKETS, DEFAULT_TFTP_WIN_SZ); |
| exit(1); |
| } |
| |
| void drain(int fd) { |
| char buf[4096]; |
| if (fcntl(fd, F_SETFL, O_NONBLOCK) == 0) { |
| while (read(fd, buf, sizeof(buf)) > 0) |
| ; |
| fcntl(fd, F_SETFL, 0); |
| } |
| } |
| |
| int send_boot_command(struct sockaddr_in6* ra) { |
| // Construct message |
| nbmsg msg; |
| static int cookie = 0; |
| msg.magic = NB_MAGIC; |
| msg.cookie = cookie++; |
| msg.cmd = NB_BOOT; |
| msg.arg = 0; |
| |
| // Send to NB_SERVER_PORT |
| struct sockaddr_in6 target_addr; |
| memcpy(&target_addr, ra, sizeof(struct sockaddr_in6)); |
| target_addr.sin6_port = htons(NB_SERVER_PORT); |
| int s = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP); |
| if (s < 0) { |
| fprintf(stderr, "%s: cannot create socket %d\n", appname, s); |
| return -1; |
| } |
| ssize_t send_result = sendto(s, &msg, sizeof(msg), 0, (struct sockaddr*)&target_addr, |
| sizeof(target_addr)); |
| if (send_result == sizeof(msg)) { |
| fprintf(stderr, "%s: sent boot command\n", appname); |
| return 0; |
| } |
| fprintf(stderr, "%s: failure sending boot command\n", appname); |
| return -1; |
| } |
| |
| int main(int argc, char** argv) { |
| struct in6_addr allowed_addr; |
| struct sockaddr_in6 addr; |
| char tmp[INET6_ADDRSTRLEN]; |
| char cmdline[4096]; |
| char* cmdnext = cmdline; |
| char* nodename = NULL; |
| int r, s, n = 1; |
| const char* kernel_fn = NULL; |
| const char* ramdisk_fn = NULL; |
| int once = 0; |
| int status; |
| |
| memset(&allowed_addr, 0, sizeof(allowed_addr)); |
| cmdline[0] = 0; |
| if ((appname = strrchr(argv[0], '/')) != NULL) { |
| appname++; |
| } else { |
| appname = argv[0]; |
| } |
| |
| while (argc > 1) { |
| if (argv[1][0] != '-') { |
| if (kernel_fn == NULL) { |
| kernel_fn = argv[1]; |
| } else if (ramdisk_fn == NULL) { |
| ramdisk_fn = argv[1]; |
| } else { |
| usage(); |
| } |
| } else if (!strcmp(argv[1], "-1")) { |
| once = 1; |
| } else if (!strcmp(argv[1], "-b")) { |
| if (argc <= 1) { |
| fprintf(stderr, "'-b' option requires an argument (tftp block size)\n"); |
| return -1; |
| } |
| errno = 0; |
| static uint16_t block_size; |
| block_size = strtoll(argv[2], NULL, 10); |
| if (errno != 0 || block_size <= 0) { |
| fprintf(stderr, "invalid arg for -b: %s\n", argv[2]); |
| return -1; |
| } |
| tftp_block_size = &block_size; |
| argc--; |
| argv++; |
| } else if (!strcmp(argv[1], "-w")) { |
| if (argc <= 1) { |
| fprintf(stderr, "'-w' option requires an argument (tftp window size)\n"); |
| return -1; |
| } |
| errno = 0; |
| static uint16_t window_size; |
| window_size = strtoll(argv[2], NULL, 10); |
| if (errno != 0 || window_size <= 0) { |
| fprintf(stderr, "invalid arg for -w: %s\n", argv[2]); |
| return -1; |
| } |
| tftp_window_size = &window_size; |
| argc--; |
| argv++; |
| } else if (!strcmp(argv[1], "-i")) { |
| if (argc <= 1) { |
| fprintf(stderr, "'-i' option requires an argument (micros between packets)\n"); |
| return -1; |
| } |
| errno = 0; |
| us_between_packets = strtoll(argv[2], NULL, 10); |
| if (errno != 0 || us_between_packets <= 0) { |
| fprintf(stderr, "invalid arg for -i: %s\n", argv[2]); |
| return -1; |
| } |
| fprintf(stderr, "packet spacing set to %" PRId64 " microseconds\n", us_between_packets); |
| argc--; |
| argv++; |
| } else if (!strcmp(argv[1], "-a")) { |
| if (argc <= 1) { |
| fprintf(stderr, "'-a' option requires a valid ipv6 address\n"); |
| return -1; |
| } |
| if (inet_pton(AF_INET6, argv[2], &allowed_addr) != 1) { |
| fprintf(stderr, "%s: invalid ipv6 address specified\n", argv[2]); |
| return -1; |
| } |
| argc--; |
| argv++; |
| } else if (!strcmp(argv[1], "-n")) { |
| if (argc <= 1) { |
| fprintf(stderr, "'-n' option requires a valid nodename\n"); |
| return -1; |
| } |
| nodename = argv[2]; |
| argc--; |
| argv++; |
| } else if (!strcmp(argv[1], "--netboot")) { |
| use_tftp = false; |
| } else if (!strcmp(argv[1], "--tftp")) { |
| use_tftp = true; |
| } else if (!strcmp(argv[1], "--")) { |
| while (argc > 2) { |
| size_t len = strlen(argv[2]); |
| if (len > (sizeof(cmdline) - 2 - (cmdnext - cmdline))) { |
| fprintf(stderr, "%s: commandline too large\n", appname); |
| return -1; |
| } |
| if (cmdnext != cmdline) { |
| *cmdnext++ = ' '; |
| } |
| memcpy(cmdnext, argv[2], len + 1); |
| cmdnext += len; |
| argc--; |
| argv++; |
| } |
| break; |
| } else { |
| usage(); |
| } |
| argc--; |
| argv++; |
| } |
| if (kernel_fn == NULL) { |
| usage(); |
| } |
| if (!nodename) { |
| nodename = getenv("ZIRCON_NODENAME"); |
| } |
| if (nodename) { |
| fprintf(stderr, "%s: Will only boot nodename '%s'\n", appname, nodename); |
| } |
| |
| // compute the default ramdisk fn to use if |
| // ramdisk is not specified and such a ramdisk |
| // file actually exists |
| char* auto_ramdisk_fn = NULL; |
| if (ramdisk_fn == NULL) { |
| char* bootdata_fn = "bootdata.bin"; |
| char *end = strrchr(kernel_fn, '/'); |
| if (end == NULL) { |
| auto_ramdisk_fn = bootdata_fn; |
| } else { |
| size_t prefix_len = (end - kernel_fn) + 1; |
| size_t len = prefix_len + strlen(bootdata_fn) + 1; |
| if ((auto_ramdisk_fn = malloc(len)) != NULL) { |
| memcpy(auto_ramdisk_fn, kernel_fn, prefix_len); |
| memcpy(auto_ramdisk_fn + prefix_len, bootdata_fn, strlen(bootdata_fn) + 1); |
| } |
| } |
| } |
| |
| memset(&addr, 0, sizeof(addr)); |
| addr.sin6_family = AF_INET6; |
| addr.sin6_port = htons(NB_ADVERT_PORT); |
| |
| s = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP); |
| if (s < 0) { |
| fprintf(stderr, "%s: cannot create socket %d\n", appname, s); |
| return -1; |
| } |
| setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &n, sizeof(n)); |
| if ((r = bind(s, (void*)&addr, sizeof(addr))) < 0) { |
| fprintf(stderr, "%s: cannot bind to [%s]%d %d: %s\n", appname, |
| inet_ntop(AF_INET6, &addr.sin6_addr, tmp, sizeof(tmp)), |
| ntohs(addr.sin6_port), errno, strerror(errno)); |
| return -1; |
| } |
| |
| fprintf(stderr, "%s: listening on [%s]%d\n", appname, |
| inet_ntop(AF_INET6, &addr.sin6_addr, tmp, sizeof(tmp)), |
| ntohs(addr.sin6_port)); |
| |
| for (;;) { |
| struct sockaddr_in6 ra; |
| socklen_t rlen; |
| char buf[4096]; |
| nbmsg* msg = (void*)buf; |
| rlen = sizeof(ra); |
| r = recvfrom(s, buf, sizeof(buf) - 1, 0, (void*)&ra, &rlen); |
| if (r < 0) { |
| fprintf(stderr, "%s: socket read error %d\n", appname, r); |
| break; |
| } |
| if (r < sizeof(nbmsg)) |
| continue; |
| if (!IN6_IS_ADDR_LINKLOCAL(&ra.sin6_addr)) { |
| fprintf(stderr, "%s: ignoring non-link-local message\n", appname); |
| continue; |
| } |
| if (!IN6_IS_ADDR_UNSPECIFIED(&allowed_addr) && |
| !IN6_ARE_ADDR_EQUAL(&allowed_addr, &ra.sin6_addr)) { |
| fprintf(stderr, "%s: ignoring message not from allowed address '%s'\n", |
| appname, inet_ntop(AF_INET6, &allowed_addr, tmp, sizeof(tmp))); |
| continue; |
| } |
| if (msg->magic != NB_MAGIC) |
| continue; |
| if (msg->cmd != NB_ADVERTISE) |
| continue; |
| if ((use_tftp && (msg->arg < NB_VERSION_1_2)) || |
| (!use_tftp && (msg->arg < NB_VERSION_1_1))) { |
| fprintf(stderr, "%s: Incompatible version 0x%08X of bootloader detected from [%s]%d, " |
| "please upgrade your bootloader\n", |
| appname, msg->arg, inet_ntop(AF_INET6, &ra.sin6_addr, tmp, sizeof(tmp)), |
| ntohs(ra.sin6_port)); |
| if (once) { |
| break; |
| } |
| continue; |
| } |
| fprintf(stderr, "%s: got beacon from [%s]%d\n", appname, |
| inet_ntop(AF_INET6, &ra.sin6_addr, tmp, sizeof(tmp)), |
| ntohs(ra.sin6_port)); |
| |
| // ensure any payload is null-terminated |
| buf[r] = 0; |
| |
| |
| char* save = NULL; |
| char* adv_nodename = NULL; |
| char* adv_version = "unknown"; |
| for (char* var = strtok_r((char*)msg->data, ";", &save); |
| var; |
| var = strtok_r(NULL, ";", &save)) { |
| if (!strncmp(var, "nodename=", 9)) { |
| adv_nodename = var + 9; |
| } else if(!strncmp(var, "version=", 8)) { |
| adv_version = var + 8; |
| } |
| } |
| |
| if (nodename) { |
| if (adv_nodename == NULL) { |
| fprintf(stderr, "%s: ignoring unknown nodename (expecting %s)\n", |
| appname, nodename); |
| } else if (strcmp(adv_nodename, nodename)) { |
| fprintf(stderr, "%s: ignoring nodename %s (expecting %s)\n", |
| appname, adv_nodename, nodename); |
| continue; |
| } |
| } |
| |
| if (strcmp(BOOTLOADER_VERSION, adv_version)) { |
| fprintf(stderr, |
| "%s: WARNING:\n" |
| "%s: WARNING: Bootloader version '%s' != '%s'. Please Upgrade.\n" |
| "%s: WARNING:\n", |
| appname, appname, adv_version, BOOTLOADER_VERSION, appname); |
| if (!strcmp(adv_version, "0.5.5")) { |
| use_filename_prefix = false; |
| } |
| } else { |
| use_filename_prefix = true; |
| } |
| |
| if (cmdline[0]) { |
| status = xfer(&ra, "(cmdline)", cmdline); |
| } else { |
| status = 0; |
| } |
| if (status == 0) { |
| struct stat s; |
| if (ramdisk_fn) { |
| status = xfer(&ra, ramdisk_fn, |
| use_filename_prefix ? NB_RAMDISK_FILENAME : "ramdisk.bin"); |
| } else if (auto_ramdisk_fn && (stat(auto_ramdisk_fn, &s) == 0)) { |
| status = xfer(&ra, auto_ramdisk_fn, |
| use_filename_prefix ? NB_RAMDISK_FILENAME : "ramdisk.bin"); |
| } |
| } |
| if (status == 0) { |
| status = xfer(&ra, kernel_fn, use_filename_prefix ? NB_KERNEL_FILENAME : "kernel.bin"); |
| if (status == 0) { |
| send_boot_command(&ra); |
| } |
| } |
| if (once) { |
| break; |
| } |
| drain(s); |
| } |
| |
| return 0; |
| } |