Skip to content

Commit 3c0c27f

Browse files
committed
Implement connections to TCP-based services
Both IPv4 and IPv6 are supported. The port or both host and port can be taken from the service argument instead of the symbolic link name. Fixes: QubesOS/qubes-issues#9037
1 parent 9dcf00e commit 3c0c27f

File tree

4 files changed

+374
-11
lines changed

4 files changed

+374
-11
lines changed

libqrexec/exec.c

+194-8
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@
2727
#include <stddef.h>
2828
#include <limits.h>
2929

30-
#include <sys/socket.h>
3130
#include <sys/types.h>
31+
#include <sys/socket.h>
3232
#include <sys/stat.h>
3333
#include <sys/un.h>
3434
#include <sys/wait.h>
35+
#include <netdb.h>
3536
#include <unistd.h>
3637
#include <fcntl.h>
3738
#include "qrexec.h"
@@ -206,7 +207,7 @@ static int qubes_connect(int s, const char *connect_path, const size_t total_pat
206207
}
207208

208209
static int execute_qrexec_service(
209-
const struct qrexec_parsed_command *cmd,
210+
struct qrexec_parsed_command *cmd,
210211
int *pid, int *stdin_fd, int *stdout_fd, int *stderr_fd,
211212
struct buffer *stdin_buffer);
212213

@@ -405,7 +406,7 @@ struct qrexec_parsed_command *parse_qubes_rpc_command(
405406
/* Parse service name ("qubes.Service") */
406407

407408
const char *const plus = memchr(start, '+', descriptor_len);
408-
size_t const name_len = plus != NULL ? (size_t)(plus - start) : descriptor_len;
409+
size_t name_len = plus != NULL ? (size_t)(plus - start) : descriptor_len;
409410
if (name_len > NAME_MAX) {
410411
LOG(ERROR, "Service name too long to execute (length %zu)", name_len);
411412
goto err;
@@ -421,6 +422,7 @@ struct qrexec_parsed_command *parse_qubes_rpc_command(
421422
}
422423
memcpy(cmd->service_name, start, name_len);
423424
cmd->service_name[name_len] = '\0';
425+
cmd->arg = plus != NULL ? plus + 1 : NULL;
424426

425427
/* If there is no service argument, add a trailing "+" to the descriptor */
426428
size_t const descriptor_buffer_size = descriptor_len + 1 + (plus == NULL);
@@ -488,7 +490,7 @@ int execute_qubes_rpc_command(const char *cmdline, int *pid, int *stdin_fd,
488490
}
489491

490492
int execute_parsed_qubes_rpc_command(
491-
const struct qrexec_parsed_command *cmd, int *pid, int *stdin_fd,
493+
struct qrexec_parsed_command *cmd, int *pid, int *stdin_fd,
492494
int *stdout_fd, int *stderr_fd, struct buffer *stdin_buffer) {
493495
if (cmd->service_descriptor) {
494496
// Proper Qubes RPC call
@@ -501,8 +503,145 @@ int execute_parsed_qubes_rpc_command(
501503
}
502504
}
503505

506+
static int qubes_tcp_connect(const char *host, const char *port)
507+
{
508+
// Work around a glibc bug: overly-large port numbers not rejected
509+
{
510+
if (*port < '1' || *port > '9')
511+
goto invalid_port;
512+
const char *end = port;
513+
errno = 0;
514+
long r = strtol(port, (char **)&end, 10);
515+
if (r < 1 || r > 65535 || *end != 0 || errno)
516+
goto invalid_port;
517+
}
518+
/* If there is ':' or '%' in the host, then this must be an IPv6 address, not
519+
* a hostname. */
520+
bool const must_be_ipv6_addr = strchr(host, ':') != NULL || strchr(host, '%') != NULL;
521+
LOG(DEBUG, "Connecting to %s%s%s:%s",
522+
must_be_ipv6_addr ? "[" : "",
523+
host,
524+
must_be_ipv6_addr ? "]" : "",
525+
port);
526+
struct addrinfo hints = {
527+
.ai_flags = AI_NUMERICSERV | (must_be_ipv6_addr ? AI_NUMERICHOST : 0),
528+
.ai_family = must_be_ipv6_addr ? AF_INET6 : AF_UNSPEC,
529+
.ai_socktype = SOCK_STREAM,
530+
.ai_protocol = IPPROTO_TCP,
531+
}, *addrs;
532+
int rc = getaddrinfo(host, port, &hints, &addrs);
533+
if (rc != 0) {
534+
/* data comes from symlink or from qrexec service argument, which has already
535+
* been sanitized */
536+
LOG(ERROR, "getaddrinfo(%s, %s) failed: %s", host, port, gai_strerror(rc));
537+
return -1;
538+
}
539+
rc = -1;
540+
size_t addresses = 1, used_addresses = 0;
541+
assert(addrs != NULL && "getaddrinfo() returned zero addresses");
542+
for (struct addrinfo *p = addrs->ai_next; p != NULL; p = p->ai_next) {
543+
addresses++;
544+
}
545+
struct pollfd *fds = calloc(addresses, sizeof(struct pollfd));
546+
if (fds == NULL) {
547+
LOG(ERROR, "Out of memory allocating %zu pollfds!", addresses);
548+
goto freeaddrs;
549+
}
550+
for (struct addrinfo *p = addrs; p != NULL; p = p->ai_next) {
551+
switch (p->ai_family) {
552+
case AF_INET:
553+
assert(p->ai_addrlen >= sizeof(struct sockaddr_in));
554+
LOG(DEBUG, "Port number %d", ntohs(((struct sockaddr_in*)p->ai_addr)->sin_port));
555+
break;
556+
case AF_INET6:
557+
assert(p->ai_addrlen >= sizeof(struct sockaddr_in6));
558+
LOG(DEBUG, "Port number %d", ntohs(((struct sockaddr_in6*)p->ai_addr)->sin6_port));
559+
break;
560+
default:
561+
LOG(ERROR, "Unknown socket family");
562+
break;
563+
}
564+
int sockfd = socket(p->ai_family,
565+
p->ai_socktype | SOCK_CLOEXEC | SOCK_NONBLOCK,
566+
p->ai_protocol);
567+
if (sockfd < 0) {
568+
LOG(ERROR, "Cannot create socket, skipping");
569+
continue;
570+
}
571+
int res = connect(sockfd, p->ai_addr, p->ai_addrlen);
572+
if (res == 0) {
573+
rc = sockfd;
574+
goto done;
575+
}
576+
if (errno != EINPROGRESS) {
577+
PERROR("connect");
578+
close(sockfd);
579+
continue;
580+
}
581+
fds[used_addresses].fd = sockfd;
582+
fds[used_addresses].events = POLLIN|POLLOUT|POLLPRI|POLLRDHUP;
583+
used_addresses++;
584+
}
585+
/* FIXME: USE EPOLL!!!!!!! */
586+
while (used_addresses != 0) {
587+
for (size_t i = 0; i < used_addresses; ++i) {
588+
fds[i].revents = 0;
589+
}
590+
int res = poll(fds, used_addresses, 1000 /* 1 second */);
591+
if (res > 0) {
592+
struct pollfd *p = fds;
593+
for (size_t i = 0; i < used_addresses; ++i) {
594+
if (fds[i].revents == 0)
595+
*p++ = fds[i];
596+
else if (fds[i].revents & ~(short)(POLLIN | POLLOUT)) {
597+
LOG(ERROR, "FD %d (offset %zu) had events 0%s%s%s%s%s%s", fds[i].fd, i,
598+
(fds[i].revents & POLLIN) ? " | POLLIN" : "",
599+
(fds[i].revents & POLLOUT) ? " | POLLOUT" : "",
600+
(fds[i].revents & POLLPRI) ? " | POLLPRI" : "",
601+
(fds[i].revents & POLLERR) ? " | POLLERR" : "",
602+
(fds[i].revents & POLLHUP) ? " | POLLHUP" : "",
603+
(fds[i].revents & POLLNVAL) ? " | POLLNVAL" : "");
604+
close(fds[i].fd);
605+
fds[i].fd = -1;
606+
} else {
607+
rc = fds[i].fd;
608+
fds[i].fd = -1;
609+
goto done;
610+
}
611+
}
612+
used_addresses = (size_t)(p - fds);
613+
} else if (res == 0) {
614+
LOG(ERROR, "TCP connection timeout");
615+
break;
616+
} else if (errno == EINTR || errno == EAGAIN) {
617+
continue;
618+
} else {
619+
PERROR("poll");
620+
break;
621+
}
622+
}
623+
done:
624+
if (rc < 0)
625+
LOG(ERROR, "None of %zu TCP sockets could connect", addresses);
626+
else
627+
LOG(DEBUG, "Connection succeeded");
628+
/* Close all FDs but the chosen one */
629+
for (size_t i = 0; i < used_addresses; ++i) {
630+
if (fds[i].fd != -1) {
631+
close(fds[i].fd);
632+
}
633+
}
634+
free(fds);
635+
freeaddrs:
636+
freeaddrinfo(addrs);
637+
return rc;
638+
invalid_port:
639+
LOG(ERROR, "Invalid port number %s", port);
640+
return -1;
641+
}
642+
504643
static int execute_qrexec_service(
505-
const struct qrexec_parsed_command *cmd,
644+
struct qrexec_parsed_command *cmd,
506645
int *pid, int *stdin_fd, int *stdout_fd, int *stderr_fd,
507646
struct buffer *stdin_buffer) {
508647

@@ -528,10 +667,11 @@ static int execute_qrexec_service(
528667
return -1;
529668
}
530669

670+
const char *desc = cmd->command + RPC_REQUEST_COMMAND_LEN + 1;
531671
if (S_ISSOCK(statbuf.st_mode)) {
532672
/* Socket-based service. */
533673
int s;
534-
if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
674+
if ((s = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) == -1) {
535675
PERROR("socket");
536676
return -1;
537677
}
@@ -545,14 +685,60 @@ static int execute_qrexec_service(
545685
if (stderr_fd)
546686
*stderr_fd = -1;
547687
*pid = 0;
548-
set_nonblock(s);
549688

550689
if (cmd->send_service_descriptor) {
551690
/* send part after "QUBESRPC ", including trailing NUL */
552-
const char *desc = cmd->command + RPC_REQUEST_COMMAND_LEN + 1;
553691
buffer_append(stdin_buffer, desc, strlen(desc) + 1);
554692
}
555693
return 0;
694+
} else if (S_ISLNK(statbuf.st_mode)) {
695+
if (stderr_fd)
696+
*stderr_fd = -1;
697+
*pid = 0;
698+
/* TCP-based service */
699+
assert(memcmp(service_full_path, "/dev/tcp/", sizeof "/dev/tcp") == 0);
700+
char *address = service_full_path + sizeof "/dev/tcp";
701+
char *slash = strchr(address, '/');
702+
char *host, *port;
703+
if (slash == NULL) {
704+
if (cmd->arg == NULL || *cmd->arg == ' ') {
705+
LOG(ERROR, "No or empty argument provided, cannot connect to %s",
706+
service_full_path);
707+
return -1;
708+
}
709+
char *ptr = cmd->service_descriptor + (cmd->arg - desc);
710+
if (*address == '\0') {
711+
/* Get both host and port from service arguments */
712+
host = ptr;
713+
port = strrchr(ptr, '+');
714+
if (port == NULL) {
715+
LOG(ERROR, "No host provided, cannot connect at %s", service_full_path);
716+
return -1;
717+
}
718+
*port = '\0';
719+
for (char *p = host; p < port; ++p) {
720+
if (*p == '_')
721+
*p = ':';
722+
else if (*p == '+')
723+
*p = '%';
724+
}
725+
port++;
726+
} else {
727+
/* Get just port from service arguments */
728+
host = address;
729+
port = ptr;
730+
}
731+
} else {
732+
*slash = '\0';
733+
host = address;
734+
port = slash + 1;
735+
}
736+
int res = qubes_tcp_connect(host, port);
737+
if (res == -1)
738+
return -1;
739+
*stdin_fd = *stdout_fd = res;
740+
cmd->send_service_descriptor = false;
741+
return 0;
556742
}
557743

558744
if (euidaccess(service_full_path, X_OK) == 0) {

libqrexec/libqrexec-utils.h

+11-2
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ struct buffer {
4848
#define WRITE_STDIN_BUFFERED 1 /* something still in the buffer */
4949
#define WRITE_STDIN_ERROR 2 /* write error, errno set */
5050

51-
/* Parsed Qubes RPC or legacy command. */
51+
/* Parsed Qubes RPC or legacy command.
52+
* The size of this structure is not part of the public API or ABI.
53+
* Only use instances allocated by libqrexec-utils. */
5254
struct qrexec_parsed_command {
5355
const char *cmdline;
5456

@@ -82,6 +84,13 @@ struct qrexec_parsed_command {
8284

8385
/* For socket-based services: Should the service descriptor be sent? */
8486
bool send_service_descriptor;
87+
88+
/* Remaining fields are private to libqrexec-utils. Do not access them
89+
* directly - they may change in any update. */
90+
91+
/* Pointer to the argument, or NULL if there is no argument.
92+
* Same buffer as "command" and "cmdline". */
93+
const char *arg;
8594
};
8695

8796
/* Parse a command, return NULL on failure. Uses cmd->cmdline
@@ -142,7 +151,7 @@ int fork_and_flush_stdin(int fd, struct buffer *buffer);
142151
* nonzero on failure.
143152
*/
144153
int execute_parsed_qubes_rpc_command(
145-
const struct qrexec_parsed_command *cmd, int *pid, int *stdin_fd,
154+
struct qrexec_parsed_command *cmd, int *pid, int *stdin_fd,
146155
int *stdout_fd, int *stderr_fd, struct buffer *stdin_buffer);
147156

148157
/**

libqrexec/process_io.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ static void close_stdout(int fd, bool restore_block) {
7676
} else if (shutdown(fd, SHUT_RD) == -1) {
7777
if (errno == ENOTSOCK)
7878
close(fd);
79-
else
79+
else if (errno != ENOTCONN) /* can happen with TCP, harmless */
8080
PERROR("shutdown close_stdout");
8181
}
8282
}

0 commit comments

Comments
 (0)