| // 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; |
| } |