| // 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 _DARWIN_C_SOURCE | |
| #define _GNU_SOURCE | |
| #include "netprotocol.h" | |
| #include <arpa/inet.h> | |
| #include <errno.h> | |
| #include <fcntl.h> | |
| #include <getopt.h> | |
| #include <ifaddrs.h> | |
| #include <netinet/in.h> | |
| #include <poll.h> | |
| #include <stdint.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <sys/socket.h> | |
| #include <sys/time.h> | |
| #include <unistd.h> | |
| #include <zircon/boot/netboot.h> | |
| uint16_t tftp_block_size = TFTP_DEFAULT_BLOCK_SZ; | |
| uint16_t tftp_window_size = TFTP_DEFAULT_WINDOW_SZ; | |
| static uint32_t cookie = 0x12345678; | |
| static int netboot_timeout = 250; | |
| static bool netboot_wait = true; | |
| static struct timeval netboot_timeout_init(int msec) { | |
| struct timeval timeout_tv; | |
| timeout_tv.tv_sec = msec / 1000; | |
| timeout_tv.tv_usec = (msec % 1000) * 1000; | |
| struct timeval end_tv; | |
| gettimeofday(&end_tv, NULL); | |
| timeradd(&end_tv, &timeout_tv, &end_tv); | |
| return end_tv; | |
| } | |
| static int netboot_timeout_get_msec(const struct timeval* end_tv) { | |
| struct timeval wait_tv; | |
| struct timeval now_tv; | |
| gettimeofday(&now_tv, NULL); | |
| timersub(end_tv, &now_tv, &wait_tv); | |
| return wait_tv.tv_sec * 1000 + wait_tv.tv_usec / 1000; | |
| } | |
| static int netboot_bind_to_cmd_port(int socket) { | |
| struct sockaddr_in6 addr; | |
| memset(&addr, 0, sizeof(addr)); | |
| addr.sin6_family = AF_INET6; | |
| for (uint16_t port = NB_CMD_PORT_START; port <= NB_CMD_PORT_END; port++) { | |
| addr.sin6_port = htons(port); | |
| if (bind(socket, (void*)&addr, sizeof(addr)) == 0) { | |
| return 0; | |
| } | |
| } | |
| return -1; | |
| } | |
| static int netboot_send_query(int socket, unsigned port, const char* ifname) { | |
| const char* hostname = "*"; | |
| size_t hostname_len = strlen(hostname) + 1; | |
| msg m; | |
| m.hdr.magic = NB_MAGIC; | |
| m.hdr.cookie = ++cookie; | |
| m.hdr.cmd = NB_QUERY; | |
| m.hdr.arg = 0; | |
| memcpy(m.data, hostname, hostname_len); | |
| struct sockaddr_in6 addr; | |
| memset(&addr, 0, sizeof(addr)); | |
| addr.sin6_family = AF_INET6; | |
| addr.sin6_port = htons(port); | |
| inet_pton(AF_INET6, "ff02::1", &addr.sin6_addr); | |
| struct ifaddrs* ifa; | |
| if (getifaddrs(&ifa) < 0) { | |
| fprintf(stderr, "error: cannot enumerate network interfaces\n"); | |
| return -1; | |
| } | |
| bool success = false; | |
| struct ifaddrs* ifa_it = ifa; | |
| for (; ifa_it != NULL; ifa_it = ifa_it->ifa_next) { | |
| if (ifa_it->ifa_addr == NULL) { | |
| continue; | |
| } | |
| if (ifa_it->ifa_addr->sa_family != AF_INET6) { | |
| continue; | |
| } | |
| struct sockaddr_in6* in6 = (void*)ifa_it->ifa_addr; | |
| if (in6->sin6_scope_id == 0) { | |
| continue; | |
| } | |
| if (ifname && ifname[0] != 0 && strcmp(ifname, ifa_it->ifa_name)) { | |
| continue; | |
| } | |
| // printf("tx %s (sid=%d)\n", ifa_it->ifa_name, in6->sin6_scope_id); | |
| size_t sz = sizeof(nbmsg) + hostname_len; | |
| addr.sin6_scope_id = in6->sin6_scope_id; | |
| ssize_t r = sendto(socket, &m, sz, 0, (struct sockaddr*)&addr, sizeof(addr)); | |
| if ((r >= 0) && (size_t)r == sz) { | |
| success = true; | |
| } | |
| } | |
| freeifaddrs(ifa); | |
| if (!success) { | |
| fprintf(stderr, "error: failed to find interface for sending query\n"); | |
| return -1; | |
| } | |
| return 0; | |
| } | |
| static bool netboot_receive_query(int socket, on_device_cb callback, void* data) { | |
| struct sockaddr_in6 ra; | |
| socklen_t rlen = sizeof(ra); | |
| memset(&ra, 0, sizeof(ra)); | |
| msg m; | |
| ssize_t r = recvfrom(socket, &m, sizeof(m), 0, (void*)&ra, &rlen); | |
| if (r < 0) { | |
| fprintf(stderr, "error: recvfrom: %s\n", strerror(errno)); | |
| } else if ((size_t)r > sizeof(nbmsg)) { | |
| r -= sizeof(nbmsg); | |
| m.data[r] = 0; | |
| if ((m.hdr.magic == NB_MAGIC) && (m.hdr.cookie == cookie) && (m.hdr.cmd == NB_ACK)) { | |
| char tmp[INET6_ADDRSTRLEN]; | |
| if (inet_ntop(AF_INET6, &ra.sin6_addr, tmp, sizeof(tmp)) == NULL) { | |
| strcpy(tmp, "???"); | |
| } | |
| // printf("found %s at %s/%d\n", (char*)m.data, tmp, ra.sin6_scope_id); | |
| if (strncmp("::", tmp, 2)) { | |
| device_info_t info; | |
| strncpy(info.nodename, (char*)m.data, sizeof(info.nodename)); | |
| strncpy(info.inet6_addr_s, tmp, INET6_ADDRSTRLEN); | |
| memcpy(&info.inet6_addr, &ra, sizeof(ra)); | |
| info.state = DEVICE; | |
| return callback(&info, data); | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| static struct option default_opts[] = { | |
| {"help", no_argument, NULL, 'h'}, | |
| {"timeout", required_argument, NULL, 't'}, | |
| {"nowait", no_argument, NULL, 'n'}, | |
| {"block-size", required_argument, NULL, 'b'}, | |
| {"window-size", required_argument, NULL, 'w'}, | |
| {NULL, 0, NULL, 0}, | |
| }; | |
| static const struct option netboot_zero_opt = {NULL, 0, NULL, 0}; | |
| static size_t netboot_count_opts(const struct option* opts) { | |
| if (!opts) { | |
| return 0; | |
| } | |
| size_t count = 0; | |
| while (memcmp(&opts[count], &netboot_zero_opt, sizeof(netboot_zero_opt))) { | |
| count++; | |
| } | |
| return count; | |
| } | |
| static void netboot_copy_opts(struct option* dst_opts, const struct option* src_opts) { | |
| if (!src_opts) { | |
| return; | |
| } | |
| size_t i; | |
| for (i = 0; memcmp(&src_opts[i], &netboot_zero_opt, sizeof(netboot_zero_opt)); i++) { | |
| dst_opts[i] = src_opts[i]; | |
| } | |
| } | |
| int netboot_handle_custom_getopt(int argc, char* const* argv, const struct option* custom_opts, | |
| bool (*opt_callback)(int ch, int argc, char* const* argv)) { | |
| size_t num_default_opts = netboot_count_opts(default_opts); | |
| size_t num_custom_opts = netboot_count_opts(custom_opts); | |
| struct option* combined_opts; | |
| combined_opts = | |
| (struct option*)malloc(sizeof(struct option) * (num_default_opts + num_custom_opts + 1)); | |
| netboot_copy_opts(combined_opts, default_opts); | |
| netboot_copy_opts(combined_opts + num_default_opts, custom_opts); | |
| memset(&combined_opts[num_default_opts + num_custom_opts], 0x0, sizeof(struct option)); | |
| int retval = -1; | |
| int ch; | |
| while ((ch = getopt_long_only(argc, argv, "t:", combined_opts, NULL)) != -1) { | |
| switch (ch) { | |
| case 't': | |
| netboot_timeout = atoi(optarg); | |
| break; | |
| case 'n': | |
| netboot_wait = false; | |
| break; | |
| case 'b': | |
| tftp_block_size = atoi(optarg); | |
| break; | |
| case 'w': | |
| tftp_window_size = atoi(optarg); | |
| break; | |
| default: | |
| if (opt_callback && opt_callback(ch, argc, argv)) { | |
| break; | |
| } else { | |
| goto err; | |
| } | |
| } | |
| } | |
| retval = optind; | |
| err: | |
| free(combined_opts); | |
| return retval; | |
| } | |
| int netboot_handle_getopt(int argc, char* const* argv) { | |
| return netboot_handle_custom_getopt(argc, argv, NULL, NULL); | |
| } | |
| void netboot_usage(bool show_tftp_opts) { | |
| fprintf(stderr, "options:\n"); | |
| fprintf(stderr, " --help Print this message.\n"); | |
| fprintf(stderr, " --timeout=<msec> Set discovery timeout to <msec>.\n"); | |
| fprintf(stderr, " --nowait Do not wait for first packet before timing out.\n"); | |
| if (show_tftp_opts) { | |
| fprintf(stderr, " --block-size=<sz> Set tftp block size (default=%d).\n", | |
| TFTP_DEFAULT_BLOCK_SZ); | |
| fprintf(stderr, " --window-size=<sz> Set tftp window size (default=%d).\n", | |
| TFTP_DEFAULT_WINDOW_SZ); | |
| } | |
| } | |
| int netboot_discover(unsigned port, const char* ifname, on_device_cb callback, void* data) { | |
| if (!callback) { | |
| errno = EINVAL; | |
| return -1; | |
| } | |
| int s; | |
| if ((s = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP)) < 0) { | |
| fprintf(stderr, "error: cannot create socket: %s\n", strerror(errno)); | |
| return -1; | |
| } | |
| if (netboot_bind_to_cmd_port(s) < 0) { | |
| fprintf(stderr, "error: cannot bind to command port: %s\n", strerror(errno)); | |
| close(s); | |
| return -1; | |
| } | |
| if (netboot_send_query(s, port, ifname) < 0) { | |
| fprintf(stderr, "error: failed to send netboot query\n"); | |
| close(s); | |
| return -1; | |
| } | |
| struct pollfd fds; | |
| fds.fd = s; | |
| fds.events = POLLIN; | |
| bool received_packets = false; | |
| bool first_wait = netboot_wait; | |
| #if defined(__APPLE__) | |
| // macOS development hosts often have a firewall that prompts the user with a dialog box asking if | |
| // a conection should be allowed. On macOS, use a long timeout for the first wait to ensure the | |
| // user has a chance to read the dialog and respond. See also bug fxbug.dev/42296. | |
| // | |
| // TODO(maniscalco): Once macOS hosts are no longer supported for bringup development we can | |
| // remove this special case and the first_wait concept. | |
| struct timeval end_tv = netboot_timeout_init(first_wait ? 3600000 : netboot_timeout); | |
| #else | |
| struct timeval end_tv = netboot_timeout_init(netboot_timeout); | |
| #endif | |
| for (;;) { | |
| int wait_ms = netboot_timeout_get_msec(&end_tv); | |
| if (wait_ms < 0) { | |
| // Expired. | |
| break; | |
| } | |
| int r = poll(&fds, 1, wait_ms); | |
| if (r > 0 && (fds.revents & POLLIN)) { | |
| received_packets = true; | |
| if (!netboot_receive_query(s, callback, data)) { | |
| break; | |
| } | |
| } else if (r < 0 && errno != EAGAIN && errno != EINTR) { | |
| fprintf(stderr, "poll returned error: %s\n", strerror(errno)); | |
| close(s); | |
| return -1; | |
| } | |
| if (first_wait) { | |
| end_tv = netboot_timeout_init(netboot_timeout); | |
| first_wait = 0; | |
| } | |
| } | |
| close(s); | |
| if (received_packets) { | |
| return 0; | |
| } else { | |
| errno = ETIMEDOUT; | |
| return -1; | |
| } | |
| } | |
| typedef struct netboot_open_cookie { | |
| struct sockaddr_in6 addr; | |
| const char* hostname; | |
| uint32_t index; | |
| } netboot_open_cookie_t; | |
| static bool netboot_open_callback(device_info_t* device, void* data) { | |
| netboot_open_cookie_t* cookie = data; | |
| cookie->index++; | |
| if (strcmp(cookie->hostname, "*") && strcmp(cookie->hostname, device->nodename)) { | |
| return true; | |
| } | |
| memcpy(&cookie->addr, &device->inet6_addr, sizeof(device->inet6_addr)); | |
| return false; | |
| } | |
| int netboot_open(const char* hostname, const char* ifname, struct sockaddr_in6* addr, | |
| bool make_connection) { | |
| if ((hostname == NULL) || (hostname[0] == 0)) { | |
| char* envname = getenv("ZIRCON_NODENAME"); | |
| hostname = envname && envname[0] != 0 ? envname : "*"; | |
| } | |
| size_t hostname_len = strlen(hostname) + 1; | |
| if (hostname_len > MAXSIZE) { | |
| errno = EINVAL; | |
| return -1; | |
| } | |
| netboot_open_cookie_t cookie; | |
| socklen_t rlen = sizeof(cookie.addr); | |
| memset(&(cookie.addr), 0, sizeof(cookie.addr)); | |
| cookie.index = 0; | |
| cookie.hostname = hostname; | |
| if (netboot_discover(NB_SERVER_PORT, ifname, netboot_open_callback, &cookie) < 0) { | |
| return -1; | |
| } | |
| // Device not found | |
| if (cookie.index == 0) { | |
| errno = EINVAL; | |
| return -1; | |
| } | |
| int s; | |
| if ((s = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP)) < 0) { | |
| fprintf(stderr, "error: cannot create socket: %s\n", strerror(errno)); | |
| return -1; | |
| } | |
| if (netboot_bind_to_cmd_port(s) < 0) { | |
| fprintf(stderr, "cannot bind to command port: %s\n", strerror(errno)); | |
| return -1; | |
| } | |
| struct timeval tv; | |
| tv.tv_sec = 0; | |
| tv.tv_usec = 250 * 1000; | |
| setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); | |
| if (addr) { | |
| memcpy(addr, &cookie.addr, sizeof(cookie.addr)); | |
| } | |
| if (make_connection && connect(s, (void*)&cookie.addr, rlen) < 0) { | |
| fprintf(stderr, "error: cannot connect UDP port\n"); | |
| close(s); | |
| return -1; | |
| } | |
| return s; | |
| } |