blob: 9cd0e4c0a2f438fa204d9685959414f882e38630 [file] [log] [blame]
// 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;
}