Changeset View
Changeset View
Standalone View
Standalone View
usr.sbin/bhyve/slirp/slirp-helper.c
- This file was added.
| /*- | |||||
| * SPDX-License-Identifier: BSD-2-Clause | |||||
| * | |||||
| * Copyright (c) 2023, 2025 Mark Johnston <markj@FreeBSD.org> | |||||
| * | |||||
| * This software was developed by the University of Cambridge Computer | |||||
| * Laboratory (Department of Computer Science and Technology) under Innovate | |||||
| * UK project 105694, "Digital Security by Design (DSbD) Technology Platform | |||||
| * Prototype". | |||||
| */ | |||||
| /* | |||||
| * A helper process which lets bhyve's libslirp-based network backend work | |||||
| * outside bhyve's Capsicum sandbox. We are started with a SOCK_SEQPACKET | |||||
| * socket through which we pass and receive packets from the guest's frontend. | |||||
| * | |||||
| * At initialization time, we receive an nvlist over the socket which describes | |||||
| * the desired slirp configuration. | |||||
| */ | |||||
| #include <sys/nv.h> | |||||
| #include <sys/socket.h> | |||||
| #include <assert.h> | |||||
| #include <capsicum_helpers.h> | |||||
| #include <dlfcn.h> | |||||
| #include <err.h> | |||||
| #include <errno.h> | |||||
| #include <fcntl.h> | |||||
| #include <poll.h> | |||||
| #include <pwd.h> | |||||
| #include <stdio.h> | |||||
| #include <stdlib.h> | |||||
| #include <string.h> | |||||
| #include <time.h> | |||||
| #include <unistd.h> | |||||
| #include "config.h" | |||||
| #include "libslirp.h" | |||||
| #define SLIRP_MTU 2048 | |||||
| struct slirp_priv { | |||||
| Slirp *slirp; /* libslirp handle */ | |||||
| int sock; /* data and control socket */ | |||||
| int wakeup[2]; /* used to wake up the pollfd thread */ | |||||
| struct pollfd *pollfds; | |||||
| size_t npollfds; | |||||
| size_t lastpollfd; | |||||
| }; | |||||
| typedef int (*slirp_add_hostxfwd_p_t)(Slirp *, | |||||
| const struct sockaddr *, socklen_t, const struct sockaddr *, socklen_t, | |||||
| int); | |||||
| typedef void (*slirp_cleanup_p_t)(Slirp *); | |||||
| typedef void (*slirp_input_p_t)(Slirp *, const uint8_t *, int); | |||||
| typedef Slirp *(*slirp_new_p_t)(const SlirpConfig *, const SlirpCb *, void *); | |||||
| typedef void (*slirp_pollfds_fill_p_t)(Slirp *, uint32_t *timeout, | |||||
| SlirpAddPollCb, void *); | |||||
| typedef void (*slirp_pollfds_poll_p_t)(Slirp *, int, SlirpGetREventsCb, void *); | |||||
| /* Function pointer table, initialized by libslirp_init(). */ | |||||
| static slirp_add_hostxfwd_p_t slirp_add_hostxfwd_p; | |||||
| static slirp_cleanup_p_t slirp_cleanup_p; | |||||
| static slirp_input_p_t slirp_input_p; | |||||
| static slirp_new_p_t slirp_new_p; | |||||
| static slirp_pollfds_fill_p_t slirp_pollfds_fill_p; | |||||
| static slirp_pollfds_poll_p_t slirp_pollfds_poll_p; | |||||
| static int64_t | |||||
| slirp_cb_clock_get_ns(void *param __unused) | |||||
| { | |||||
| struct timespec ts; | |||||
| int error; | |||||
| error = clock_gettime(CLOCK_MONOTONIC, &ts); | |||||
| assert(error == 0); | |||||
| return ((int64_t)(ts.tv_sec * 1000000000L + ts.tv_nsec)); | |||||
| } | |||||
| static void | |||||
| slirp_cb_notify(void *param) | |||||
| { | |||||
| struct slirp_priv *priv; | |||||
| /* Wake up the poll thread. We assume that priv->mtx is held here. */ | |||||
| priv = param; | |||||
| (void)write(priv->wakeup[1], "M", 1); | |||||
| } | |||||
| static void | |||||
| slirp_cb_register_poll_fd(int fd, void *param __unused) | |||||
| { | |||||
| const int one = 1; | |||||
| (void)setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(int)); | |||||
| } | |||||
| static ssize_t | |||||
| slirp_cb_send_packet(const void *buf, size_t len, void *param) | |||||
| { | |||||
| struct slirp_priv *priv; | |||||
| ssize_t n; | |||||
| priv = param; | |||||
| assert(len <= SLIRP_MTU); | |||||
| n = send(priv->sock, buf, len, MSG_EOR); | |||||
| if (n < 0) { | |||||
| warn("slirp_cb_send_packet: send"); | |||||
| return (n); | |||||
| } | |||||
| assert((size_t)n == len); | |||||
| return (n); | |||||
| } | |||||
| static void | |||||
| slirp_cb_unregister_poll_fd(int fd __unused, void *opaque __unused) | |||||
| { | |||||
| } | |||||
| /* Callbacks invoked from within libslirp. */ | |||||
| static const struct SlirpCb slirp_cbs = { | |||||
| .clock_get_ns = slirp_cb_clock_get_ns, | |||||
| .notify = slirp_cb_notify, | |||||
| .register_poll_fd = slirp_cb_register_poll_fd, | |||||
| .send_packet = slirp_cb_send_packet, | |||||
| .unregister_poll_fd = slirp_cb_unregister_poll_fd, | |||||
| }; | |||||
| static int | |||||
| slirpev2pollev(int events) | |||||
| { | |||||
| int ret; | |||||
| ret = 0; | |||||
| if (events & SLIRP_POLL_IN) | |||||
| ret |= POLLIN; | |||||
| if (events & SLIRP_POLL_OUT) | |||||
| ret |= POLLOUT; | |||||
| if (events & SLIRP_POLL_PRI) | |||||
| ret |= POLLPRI; | |||||
| if (events & SLIRP_POLL_ERR) | |||||
| ret |= POLLERR; | |||||
| if (events & SLIRP_POLL_HUP) | |||||
| ret |= POLLHUP; | |||||
| return (ret); | |||||
| } | |||||
| static int | |||||
| pollev2slirpev(int events) | |||||
| { | |||||
| int ret; | |||||
| ret = 0; | |||||
| if (events & POLLIN) | |||||
| ret |= SLIRP_POLL_IN; | |||||
| if (events & POLLOUT) | |||||
| ret |= SLIRP_POLL_OUT; | |||||
| if (events & POLLPRI) | |||||
| ret |= SLIRP_POLL_PRI; | |||||
| if (events & POLLERR) | |||||
| ret |= SLIRP_POLL_ERR; | |||||
| if (events & POLLHUP) | |||||
| ret |= SLIRP_POLL_HUP; | |||||
| return (ret); | |||||
| } | |||||
| static int | |||||
| slirp_addpoll(struct slirp_priv *priv, int fd, int events) | |||||
| { | |||||
| struct pollfd *pollfd, *pollfds; | |||||
| size_t i; | |||||
| for (i = priv->lastpollfd + 1; i < priv->npollfds; i++) | |||||
| if (priv->pollfds[i].fd == -1) | |||||
| break; | |||||
| if (i == priv->npollfds) { | |||||
| const size_t POLLFD_GROW = 4; | |||||
| priv->npollfds += POLLFD_GROW; | |||||
| pollfds = realloc(priv->pollfds, | |||||
| sizeof(*pollfds) * priv->npollfds); | |||||
| if (pollfds == NULL) | |||||
| return (-1); | |||||
| for (i = priv->npollfds - POLLFD_GROW; i < priv->npollfds; i++) | |||||
| pollfds[i].fd = -1; | |||||
| priv->pollfds = pollfds; | |||||
| i = priv->npollfds - POLLFD_GROW; | |||||
| } | |||||
| pollfd = &priv->pollfds[i]; | |||||
| pollfd->fd = fd; | |||||
| pollfd->events = slirpev2pollev(events); | |||||
| pollfd->revents = 0; | |||||
| priv->lastpollfd = i; | |||||
| return ((int)i); | |||||
| } | |||||
| static int | |||||
| slirp_addpoll_cb(int fd, int events, void *param) | |||||
| { | |||||
| struct slirp_priv *priv; | |||||
| priv = param; | |||||
| return (slirp_addpoll(priv, fd, events)); | |||||
| } | |||||
| static int | |||||
| slirp_poll_revents(int idx, void *param) | |||||
| { | |||||
| struct slirp_priv *priv; | |||||
| struct pollfd *pollfd; | |||||
| short revents; | |||||
| priv = param; | |||||
| assert(idx >= 0); | |||||
| assert((unsigned int)idx < priv->npollfds); | |||||
| pollfd = &priv->pollfds[idx]; | |||||
| assert(pollfd->fd != -1); | |||||
| /* The kernel may report POLLHUP even if we didn't ask for it. */ | |||||
| revents = pollfd->revents; | |||||
| if ((pollfd->events & POLLHUP) == 0) | |||||
| revents &= ~POLLHUP; | |||||
| return (pollev2slirpev(revents)); | |||||
| } | |||||
| /* | |||||
| * Main loop. Poll libslirp's descriptors plus a couple of our own. | |||||
| */ | |||||
| static void | |||||
| slirp_pollfd_loop(struct slirp_priv *priv) | |||||
| { | |||||
| struct pollfd *pollfds; | |||||
| size_t npollfds; | |||||
| uint32_t timeout; | |||||
| int error; | |||||
| for (;;) { | |||||
| int input, wakeup; | |||||
| for (size_t i = 0; i < priv->npollfds; i++) | |||||
| priv->pollfds[i].fd = -1; | |||||
| priv->lastpollfd = -1; | |||||
| /* Register for notifications from slirp_cb_notify(). */ | |||||
| wakeup = slirp_addpoll(priv, priv->wakeup[0], POLLIN); | |||||
| /* Register for input from our parent process. */ | |||||
| input = slirp_addpoll(priv, priv->sock, POLLIN | POLLRDHUP); | |||||
| timeout = UINT32_MAX; | |||||
| slirp_pollfds_fill_p(priv->slirp, &timeout, slirp_addpoll_cb, | |||||
| priv); | |||||
| pollfds = priv->pollfds; | |||||
| npollfds = priv->npollfds; | |||||
| error = poll(pollfds, npollfds, timeout); | |||||
| if (error == -1 && errno != EINTR) | |||||
| err(1, "poll"); | |||||
| slirp_pollfds_poll_p(priv->slirp, error == -1, | |||||
| slirp_poll_revents, priv); | |||||
| /* | |||||
| * If we were woken up by the notify callback, mask the | |||||
| * interrupt. | |||||
| */ | |||||
| if ((pollfds[wakeup].revents & POLLIN) != 0) { | |||||
| ssize_t n; | |||||
| do { | |||||
| uint8_t b; | |||||
| n = read(priv->wakeup[0], &b, 1); | |||||
| } while (n == 1); | |||||
| if (n != -1 || errno != EAGAIN) | |||||
| err(1, "read"); | |||||
| } | |||||
| /* | |||||
| * If new packets arrived from our parent, feed them to | |||||
| * libslirp. | |||||
| */ | |||||
| if ((pollfds[input].revents & (POLLHUP | POLLRDHUP)) != 0) | |||||
| errx(1, "parent process closed connection"); | |||||
| if ((pollfds[input].revents & POLLIN) != 0) { | |||||
| ssize_t n; | |||||
| do { | |||||
| uint8_t buf[SLIRP_MTU]; | |||||
| n = recv(priv->sock, buf, sizeof(buf), | |||||
| MSG_DONTWAIT); | |||||
| if (n < 0) { | |||||
| if (errno == EWOULDBLOCK) | |||||
| break; | |||||
| err(1, "recv"); | |||||
| } | |||||
| slirp_input_p(priv->slirp, buf, (int)n); | |||||
| } while (n >= 0); | |||||
| } | |||||
| } | |||||
| } | |||||
| static int | |||||
| parse_addr(char *addr, struct sockaddr_in *sinp) | |||||
| { | |||||
| char *port; | |||||
| int error, porti; | |||||
| memset(sinp, 0, sizeof(*sinp)); | |||||
| sinp->sin_family = AF_INET; | |||||
| sinp->sin_len = sizeof(struct sockaddr_in); | |||||
| port = strchr(addr, ':'); | |||||
| if (port == NULL) | |||||
| return (EINVAL); | |||||
| *port++ = '\0'; | |||||
| if (strlen(addr) > 0) { | |||||
| error = inet_pton(AF_INET, addr, &sinp->sin_addr); | |||||
| if (error != 1) | |||||
| return (error == 0 ? EPFNOSUPPORT : errno); | |||||
| } else { | |||||
| sinp->sin_addr.s_addr = htonl(INADDR_ANY); | |||||
| } | |||||
| porti = strlen(port) > 0 ? atoi(port) : 0; | |||||
| if (porti < 0 || porti > UINT16_MAX) | |||||
| return (EINVAL); | |||||
| sinp->sin_port = htons(porti); | |||||
| return (0); | |||||
| } | |||||
| static int | |||||
| parse_hostfwd_rule(const char *descr, int *is_udp, struct sockaddr *hostaddr, | |||||
| struct sockaddr *guestaddr) | |||||
| { | |||||
| struct sockaddr_in *hostaddrp, *guestaddrp; | |||||
| const char *proto; | |||||
| char *p, *host, *guest; | |||||
| int error; | |||||
| error = 0; | |||||
| *is_udp = 0; | |||||
| p = strdup(descr); | |||||
| if (p == NULL) | |||||
| return (ENOMEM); | |||||
| host = strchr(p, ':'); | |||||
| if (host == NULL) { | |||||
| error = EINVAL; | |||||
| goto out; | |||||
| } | |||||
| *host++ = '\0'; | |||||
| proto = p; | |||||
| *is_udp = strcmp(proto, "udp") == 0; | |||||
| guest = strchr(host, '-'); | |||||
| if (guest == NULL) { | |||||
| error = EINVAL; | |||||
| goto out; | |||||
| } | |||||
| *guest++ = '\0'; | |||||
| hostaddrp = (struct sockaddr_in *)(void *)hostaddr; | |||||
| error = parse_addr(host, hostaddrp); | |||||
| if (error != 0) | |||||
| goto out; | |||||
| guestaddrp = (struct sockaddr_in *)(void *)guestaddr; | |||||
| error = parse_addr(guest, guestaddrp); | |||||
| if (error != 0) | |||||
| goto out; | |||||
| out: | |||||
| free(p); | |||||
| return (error); | |||||
| } | |||||
| static void | |||||
| config_one_hostfwd(Slirp *slirp, const char *rule) | |||||
| { | |||||
| struct sockaddr hostaddr, guestaddr; | |||||
| int error, is_udp; | |||||
| error = parse_hostfwd_rule(rule, &is_udp, &hostaddr, &guestaddr); | |||||
| if (error != 0) | |||||
| errx(1, "unable to parse hostfwd rule '%s': %s", rule, | |||||
| strerror(error)); | |||||
| error = slirp_add_hostxfwd_p(slirp, &hostaddr, hostaddr.sa_len, | |||||
| &guestaddr, guestaddr.sa_len, is_udp ? SLIRP_HOSTFWD_UDP : 0); | |||||
| if (error != 0) | |||||
| errx(1, "Unable to add hostfwd rule '%s': %s", rule, | |||||
| strerror(errno)); | |||||
| } | |||||
| /* | |||||
| * Drop privileges to the "nobody" user. Ideally we'd chroot to somewhere like | |||||
| * /var/empty but libslirp might need to access /etc/resolv.conf. | |||||
| */ | |||||
| static void | |||||
| drop_privs(void) | |||||
| { | |||||
| struct passwd *pw; | |||||
| if (geteuid() != 0) | |||||
| return; | |||||
| pw = getpwnam("nobody"); | |||||
| if (pw == NULL) | |||||
| err(1, "getpwnam(nobody) failed"); | |||||
| if (initgroups(pw->pw_name, pw->pw_gid) != 0) | |||||
| err(1, "initgroups"); | |||||
| if (setgid(pw->pw_gid) != 0) | |||||
| err(1, "setgid"); | |||||
| if (setuid(pw->pw_uid) != 0) | |||||
| err(1, "setuid"); | |||||
| } | |||||
| static void | |||||
| libslirp_init(void) | |||||
| { | |||||
| void *handle; | |||||
| handle = dlopen("libslirp.so.0", RTLD_LAZY); | |||||
| if (handle == NULL) | |||||
| errx(1, "unable to open libslirp.so.0: %s", dlerror()); | |||||
| #define IMPORT_SYM(sym) do { \ | |||||
| sym##_p = (sym##_p_t)dlsym(handle, #sym); \ | |||||
| if (sym##_p == NULL) \ | |||||
| errx(1, "failed to resolve %s", #sym); \ | |||||
| } while (0) | |||||
| IMPORT_SYM(slirp_add_hostxfwd); | |||||
| IMPORT_SYM(slirp_cleanup); | |||||
| IMPORT_SYM(slirp_input); | |||||
| IMPORT_SYM(slirp_new); | |||||
| IMPORT_SYM(slirp_pollfds_fill); | |||||
| IMPORT_SYM(slirp_pollfds_poll); | |||||
| #undef IMPORT_SYM | |||||
| } | |||||
| static void | |||||
| usage(void) | |||||
| { | |||||
| fprintf(stderr, "Usage: slirp-helper -S <socket>\n"); | |||||
| exit(1); | |||||
| } | |||||
| int | |||||
| main(int argc, char **argv) | |||||
| { | |||||
| struct slirp_priv priv; | |||||
| SlirpConfig slirpconfig; | |||||
| Slirp *slirp; | |||||
| nvlist_t *config; | |||||
| const char *hostfwd, *vmname; | |||||
| int ch, fd, sd; | |||||
| bool restricted; | |||||
| sd = -1; | |||||
| while ((ch = getopt(argc, argv, "S:")) != -1) { | |||||
| switch (ch) { | |||||
| case 'S': | |||||
| sd = atoi(optarg); | |||||
| if (fcntl(sd, F_GETFD) == -1) | |||||
| err(1, "invalid socket %s", optarg); | |||||
| break; | |||||
| default: | |||||
| usage(); | |||||
| /* NOTREACHED */ | |||||
| } | |||||
| } | |||||
| argc -= optind; | |||||
| argv += optind; | |||||
| if (sd == -1) | |||||
| usage(); | |||||
| /* | |||||
| * Clean the fd space: point stdio to /dev/null and keep our socket. | |||||
| */ | |||||
| fd = open("/dev/null", O_RDWR); | |||||
| if (fd == -1) | |||||
| err(1, "open(/dev/null)"); | |||||
| if (dup2(fd, STDIN_FILENO) == -1) | |||||
| err(1, "dup2(stdin)"); | |||||
| if (dup2(fd, STDOUT_FILENO) == -1) | |||||
| err(1, "dup2(stdout)"); | |||||
| if (dup2(fd, STDERR_FILENO) == -1) | |||||
| err(1, "dup2(stderr)"); | |||||
| if (dup2(sd, 3) == -1) | |||||
| err(1, "dup2(slirp socket)"); | |||||
| sd = 3; | |||||
| closefrom(sd + 1); | |||||
| memset(&priv, 0, sizeof(priv)); | |||||
| priv.sock = sd; | |||||
| if (pipe2(priv.wakeup, O_CLOEXEC | O_NONBLOCK) != 0) | |||||
| err(1, "pipe2"); | |||||
| /* | |||||
| * Apply the configuration we received from bhyve. | |||||
| */ | |||||
| config = nvlist_recv(sd, 0); | |||||
| if (config == NULL) | |||||
| err(1, "nvlist_recv"); | |||||
| vmname = get_config_value_node(config, "vmname"); | |||||
| if (vmname != NULL) | |||||
| setproctitle("%s", vmname); | |||||
| restricted = !get_config_bool_node_default(config, "open", false); | |||||
| slirpconfig = (SlirpConfig){ | |||||
| .version = 4, | |||||
| .if_mtu = SLIRP_MTU, | |||||
| .restricted = restricted, | |||||
| .in_enabled = true, | |||||
| .vnetwork.s_addr = htonl(0x0a000200), /* 10.0.2.0/24 */ | |||||
| .vnetmask.s_addr = htonl(0xffffff00), /* 255.255.255.0 */ | |||||
| .vdhcp_start.s_addr = htonl(0x0a00020f),/* 10.0.2.15 */ | |||||
| .vhost.s_addr = htonl(0x0a000202), /* 10.0.2.2 */ | |||||
| .vnameserver.s_addr = htonl(0x0a000203),/* 10.0.2.3 */ | |||||
| .enable_emu = false, | |||||
| }; | |||||
| libslirp_init(); | |||||
| slirp = slirp_new_p(&slirpconfig, &slirp_cbs, &priv); | |||||
| hostfwd = get_config_value_node(config, "hostfwd"); | |||||
| if (hostfwd != NULL) { | |||||
| char *rules, *tofree; | |||||
| const char *rule; | |||||
| tofree = rules = strdup(hostfwd); | |||||
| if (rules == NULL) | |||||
| err(1, "strdup"); | |||||
| while ((rule = strsep(&rules, ";")) != NULL) | |||||
| config_one_hostfwd(slirp, rule); | |||||
| free(tofree); | |||||
| } | |||||
| priv.slirp = slirp; | |||||
| /* | |||||
| * In restricted mode, we can enter a Capsicum sandbox without losing | |||||
| * functionality. | |||||
| */ | |||||
| if (restricted && caph_enter() != 0) | |||||
| err(1, "caph_enter"); | |||||
| /* | |||||
| * Drop root privileges if we have them. | |||||
| */ | |||||
| drop_privs(); | |||||
| /* | |||||
| * Enter our main loop. If bhyve goes away, we should observe a hangup | |||||
| * on the socket and exit. | |||||
| */ | |||||
| slirp_pollfd_loop(&priv); | |||||
| /* NOTREACHED */ | |||||
| return (1); | |||||
| } | |||||