diff --git a/usr.sbin/bhyve/bhyve.8 b/usr.sbin/bhyve/bhyve.8 --- a/usr.sbin/bhyve/bhyve.8 +++ b/usr.sbin/bhyve/bhyve.8 @@ -620,6 +620,10 @@ process. .It Ar /dev/xxx Use the host TTY device for serial port I/O. +.It Ar tcp=ip:port +Use the TCP server for serial port I/O. +It will start a tcp server and wait for connection. +Note that this feature allows unprivileged users to access the guest console, ensure that access is restricted appropriately. .El .Ss TPM device backends .Bl -bullet @@ -1118,7 +1122,7 @@ .Ed .Pp Run a UEFI virtual machine with a display resolution of 800 by 600 pixels -that can be accessed via VNC at: 0.0.0.0:5900. +that can be accessed via VNC at: 0.0.0.0:5900 or via TCP at: 0.0.0.0:1234. .Bd -literal -offset indent bhyve -c 2 -m 4G -w -H \\ -s 0,hostbridge \\ @@ -1127,13 +1131,13 @@ -s 5,virtio-net,tap0 \\ -s 29,fbuf,tcp=0.0.0.0:5900,w=800,h=600,wait \\ -s 30,xhci,tablet \\ - -s 31,lpc -l com1,stdio \\ + -s 31,lpc -l com1,tcp=0.0.0.0:1234 \\ -l bootrom,/usr/local/share/uefi-firmware/BHYVE_UEFI.fd \\ uefivm .Ed .Pp Run a UEFI virtual machine with a VNC display that is bound to all IPv6 -addresses on port 5900. +addresses on port 5900 and a serial I/O port bound to TCP port 1234. .Bd -literal -offset indent bhyve -c 2 -m 4G -w -H \\ -s 0,hostbridge \\ @@ -1141,7 +1145,7 @@ -s 5,virtio-net,tap0 \\ -s 29,fbuf,tcp=[::]:5900,w=800,h=600 \\ -s 30,xhci,tablet \\ - -s 31,lpc -l com1,stdio \\ + -s 31,lpc -l com1,tcp=[::]:1234 \\ -l bootrom,/usr/local/share/uefi-firmware/BHYVE_UEFI.fd \\ uefivm .Ed diff --git a/usr.sbin/bhyve/bhyve_config.5 b/usr.sbin/bhyve/bhyve_config.5 --- a/usr.sbin/bhyve/bhyve_config.5 +++ b/usr.sbin/bhyve/bhyve_config.5 @@ -23,7 +23,7 @@ .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF .\" SUCH DAMAGE. .\" -.Dd November 20, 2023 +.Dd August 21, 2024 .Dt BHYVE_CONFIG 5 .Os .Sh NAME @@ -446,6 +446,12 @@ to use standard input and output of the .Xr bhyve 8 process. +.It Va tcp Ta Oo Ar IP Ns : Oc Ns Ar port Ta Ta +TCP address to listen on for remote connections. +The IP address must be given as a numeric address. +IPv6 addresses must be enclosed in square brackets and +support scoped identifiers as described in +.Xr getaddrinfo 3 . .El .Ss Host Bridge Settings .Bl -column "pcireg.*" "integer" "Default" diff --git a/usr.sbin/bhyve/uart_backend.c b/usr.sbin/bhyve/uart_backend.c --- a/usr.sbin/bhyve/uart_backend.c +++ b/usr.sbin/bhyve/uart_backend.c @@ -28,13 +28,18 @@ */ #include +#include #include #include +#include + +#include #include #include #include +#include #include #include #include @@ -49,6 +54,7 @@ struct ttyfd { bool opened; + bool is_socket; int rfd; /* fd for reading */ int wfd; /* fd for writing, may be == rfd */ }; @@ -70,9 +76,17 @@ pthread_mutex_t mtx; }; +struct uart_socket_softc { + struct uart_softc *softc; + void (*drain)(int, enum ev_type, void *); + void *arg; +}; + static bool uart_stdio; /* stdio in use for i/o */ static struct termios tio_stdio_orig; +static void uart_tcp_disconnect(struct uart_softc *); + static void ttyclose(void) { @@ -97,20 +111,22 @@ } static int -ttyread(struct ttyfd *tf) +ttyread(struct ttyfd *tf, uint8_t *ret) { - unsigned char rb; + uint8_t rb; + int len; - if (read(tf->rfd, &rb, 1) == 1) - return (rb); - else - return (-1); + len = read(tf->rfd, &rb, 1); + if (ret && len == 1) + *ret = rb; + + return (len); } -static void +static int ttywrite(struct ttyfd *tf, unsigned char wb) { - (void)write(tf->wfd, &wb, 1); + return (write(tf->wfd, &wb, 1)); } static bool @@ -179,14 +195,27 @@ void uart_rxfifo_drain(struct uart_softc *sc, bool loopback) { - int ch; + uint8_t ch; + int len; if (loopback) { - (void)ttyread(&sc->tty); + if (ttyread(&sc->tty, &ch) == 0 && sc->tty.is_socket) + uart_tcp_disconnect(sc); } else { - while (rxfifo_available(sc) && - ((ch = ttyread(&sc->tty)) != -1)) + while (rxfifo_available(sc)) { + len = ttyread(&sc->tty, &ch); + if (len <= 0) { + /* + * If return value of read is 0, it means + * disconnected + */ + if (len == 0 && sc->tty.is_socket) + uart_tcp_disconnect(sc); + break; + } + rxfifo_putchar(sc, ch); + } } } @@ -196,7 +225,11 @@ if (loopback) { return (rxfifo_putchar(sc, ch)); } else if (sc->tty.opened) { - ttywrite(&sc->tty, ch); + /* + * We the return value of write is -1, it means disconnected + */ + if (ttywrite(&sc->tty, ch) == -1 && sc->tty.is_socket) + uart_tcp_disconnect(sc); return (0); } else { /* Drop on the floor. */ @@ -259,6 +292,62 @@ } #endif +/* + * Listen to the tcp port, wait connection, then accept it + */ +static void +uart_tcp_listener(int fd, enum ev_type type __unused, void *arg) +{ + const static char tcp_error_msg[] = "Socket already connected\n"; + struct uart_socket_softc *socket_softc = (struct uart_socket_softc *) + arg; + struct uart_softc *sc = socket_softc->softc; + int conn_fd; + + conn_fd = accept(fd, NULL, NULL); + if (conn_fd == -1) + goto clean; + + if (fcntl(conn_fd, F_SETFL, O_NONBLOCK) != 0) + goto clean; + + pthread_mutex_lock(&sc->mtx); + + if (sc->tty.opened) { + (void)send(conn_fd, tcp_error_msg, sizeof(tcp_error_msg), 0); + pthread_mutex_unlock(&sc->mtx); + goto clean; + } else { + sc->tty.rfd = sc->tty.wfd = conn_fd; + sc->tty.opened = true; + sc->mev = mevent_add(sc->tty.rfd, EVF_READ, socket_softc->drain, + socket_softc->arg); + } + + pthread_mutex_unlock(&sc->mtx); + return; + +clean: + if (conn_fd != -1) + close(conn_fd); +} + +/* + * When connected based protocal disconnected, we use this handler to clean it + * Notice that this function is a helper function thus the caller should + * take the responsibilty to lock the softc + */ +static void +uart_tcp_disconnect(struct uart_softc *sc) +{ + mevent_delete_close(sc->mev); + sc->mev = NULL; + + close(sc->tty.rfd); + sc->tty.opened = false; + sc->tty.rfd = sc->tty.wfd = -1; +} + static int uart_stdio_backend(struct uart_softc *sc) { @@ -324,6 +413,105 @@ return (0); } +/* + * Listen to address and add to kqueue. + * If connected (e.g. tcp_handler is triggered), we replace the handler to the + * connected handler + */ +static int +uart_tcp_backend(struct uart_softc *sc, const char *path, + void (*drain)(int, enum ev_type, void *), void *arg) +{ +#ifndef WITHOUT_CAPSICUM + cap_rights_t rights; + cap_ioctl_t cmds[] = { TIOCGETA, TIOCSETA, TIOCGWINSZ }; +#endif + int bind_fd = -1; + char addr[256], port[6]; + int domain; + struct addrinfo hints, *src_addr = NULL; + struct uart_socket_softc *socket_softc = NULL; + + if (sscanf(path, "tcp=[%255[^]]]:%5s", addr, port) == 2) { + domain = AF_INET6; + } else if (sscanf(path, "tcp=%255[^:]:%5s", addr, port) == 2) { + domain = AF_INET; + } else { + warnx("Invalid number of parameter"); + goto clean; + } + + bind_fd = socket(domain, SOCK_STREAM, 0); + if (bind_fd < 0) + goto clean; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = domain; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV | AI_PASSIVE; + + if (getaddrinfo(addr, port, &hints, &src_addr) != 0) { + warnx("Invalid address %s:%s", addr, port); + goto clean; + } + + if (bind(bind_fd, src_addr->ai_addr, src_addr->ai_addrlen) == -1) { + warnx( + "Unable to bind to address %s:%s. Maybe the address is used?", + addr, port); + goto clean; + } + + freeaddrinfo(src_addr); + src_addr = NULL; + + if (fcntl(bind_fd, F_SETFL, O_NONBLOCK) == -1) + goto clean; + + if (listen(bind_fd, 1) == -1) { + warnx("Unable to listen to address %s:%s", addr, port); + goto clean; + } + + /* + * Set connection softc, this structure includes both softc and drain + * function provided by the frontend + */ + if ((socket_softc = calloc(sizeof(struct uart_socket_softc), 1)) == + NULL) + goto clean; + + socket_softc->softc = sc; + socket_softc->drain = drain; + socket_softc->arg = arg; + + if ((sc->mev = mevent_add(bind_fd, EVF_READ, uart_tcp_listener, + socket_softc)) == NULL) + goto clean; + +#ifndef WITHOUT_CAPSICUM + cap_rights_init(&rights, CAP_EVENT, CAP_ACCEPT, CAP_RECV, CAP_SEND, + CAP_FCNTL, CAP_IOCTL); + if (caph_rights_limit(bind_fd, &rights) == -1) + errx(EX_OSERR, "Unable to apply rights for sandbox"); + if (caph_ioctls_limit(bind_fd, cmds, nitems(cmds)) == -1) + errx(EX_OSERR, "Unable to apply ioctls for sandbox"); + if (caph_fcntls_limit(bind_fd, CAP_FCNTL_SETFL) == -1) + errx(EX_OSERR, "Unable to apply fcntls for sandbox"); +#endif + + return (0); + +clean: + if (bind_fd != -1) + close(bind_fd); + if (socket_softc != NULL) + free(socket_softc); + if (src_addr) + freeaddrinfo(src_addr); + return (-1); +} + struct uart_softc * uart_init(void) { @@ -342,11 +530,23 @@ { int retval; - if (strcmp("stdio", path) == 0) + if (strcmp("stdio", path) == 0) { retval = uart_stdio_backend(sc); - else + } else if (strncmp("tcp", path, 3) == 0) { + /* + * Set this to prevent race conditions when socket connected + * very fast + */ + sc->tty.is_socket = true; + retval = uart_tcp_backend(sc, path, drain, arg); + } else { retval = uart_tty_backend(sc, path); - if (retval == 0) { + } + /* + * Connected based protocal should wait for connection + * Thus it can listen nothing when initialize + */ + if (retval == 0 && !sc->tty.is_socket) { ttyopen(&sc->tty); sc->mev = mevent_add(sc->tty.rfd, EVF_READ, drain, arg); assert(sc->mev != NULL);