Skip to content

Commit 84dc0f4

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. Of course, there are full unit tests. Fixes: QubesOS/qubes-issues#9037
1 parent 8dfd4b0 commit 84dc0f4

File tree

5 files changed

+389
-11
lines changed

5 files changed

+389
-11
lines changed

libqrexec/exec.c

+206-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

@@ -418,7 +419,7 @@ struct qrexec_parsed_command *parse_qubes_rpc_command(
418419
/* Parse service name ("qubes.Service") */
419420

420421
const char *const plus = memchr(start, '+', descriptor_len);
421-
size_t const name_len = plus != NULL ? (size_t)(plus - start) : descriptor_len;
422+
size_t name_len = plus != NULL ? (size_t)(plus - start) : descriptor_len;
422423
if (name_len > NAME_MAX) {
423424
LOG(ERROR, "Service name too long to execute (length %zu)", name_len);
424425
goto err;
@@ -430,6 +431,7 @@ struct qrexec_parsed_command *parse_qubes_rpc_command(
430431
cmd->service_name = memdupnul(start, name_len);
431432
if (!cmd->service_name)
432433
goto err;
434+
cmd->arg = plus != NULL ? plus + 1 : NULL;
433435

434436
/* If there is no service argument, add a trailing "+" to the descriptor */
435437
cmd->service_descriptor = memdupnul(start, descriptor_len + (plus == NULL));
@@ -487,7 +489,7 @@ int execute_qubes_rpc_command(const char *cmdline, int *pid, int *stdin_fd,
487489
}
488490

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

503652
static int execute_qrexec_service(
504-
const struct qrexec_parsed_command *cmd,
653+
struct qrexec_parsed_command *cmd,
505654
int *pid, int *stdin_fd, int *stdout_fd, int *stderr_fd,
506655
struct buffer *stdin_buffer) {
507656

@@ -527,10 +676,11 @@ static int execute_qrexec_service(
527676
return -1;
528677
}
529678

679+
const char *desc = cmd->command + RPC_REQUEST_COMMAND_LEN + 1;
530680
if (S_ISSOCK(statbuf.st_mode)) {
531681
/* Socket-based service. */
532682
int s;
533-
if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
683+
if ((s = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) == -1) {
534684
PERROR("socket");
535685
return -1;
536686
}
@@ -544,14 +694,62 @@ static int execute_qrexec_service(
544694
if (stderr_fd)
545695
*stderr_fd = -1;
546696
*pid = 0;
547-
set_nonblock(s);
548697

549698
if (cmd->send_service_descriptor) {
550699
/* send part after "QUBESRPC ", including trailing NUL */
551-
const char *desc = cmd->command + RPC_REQUEST_COMMAND_LEN + 1;
552700
buffer_append(stdin_buffer, desc, strlen(desc) + 1);
553701
}
554702
return 0;
703+
} else if (S_ISLNK(statbuf.st_mode)) {
704+
if (stderr_fd)
705+
*stderr_fd = -1;
706+
*pid = 0;
707+
/* TCP-based service */
708+
assert(memcmp(service_full_path, "/dev/tcp/", sizeof "/dev/tcp") == 0);
709+
char *address = service_full_path + sizeof "/dev/tcp";
710+
char *slash = strchr(address, '/');
711+
char *host, *port;
712+
if (slash == NULL) {
713+
if (cmd->arg == NULL || *cmd->arg == ' ') {
714+
LOG(ERROR, "No or empty argument provided, cannot connect to %s",
715+
service_full_path);
716+
return -1;
717+
}
718+
char *ptr = cmd->service_descriptor + (cmd->arg - desc);
719+
if (*address == '\0') {
720+
/* Get both host and port from service arguments */
721+
host = ptr;
722+
port = strrchr(ptr, '+');
723+
if (port == NULL) {
724+
LOG(ERROR, "No host provided, cannot connect at %s", service_full_path);
725+
return -1;
726+
}
727+
*port = '\0';
728+
for (char *p = host; p < port; ++p) {
729+
if (*p == '_') {
730+
LOG(ERROR, "Underscore not allowed in hostname %s", host);
731+
return -1;
732+
}
733+
if (*p == '+')
734+
*p = ':';
735+
}
736+
port++;
737+
} else {
738+
/* Get just port from service arguments */
739+
host = address;
740+
port = ptr;
741+
}
742+
} else {
743+
*slash = '\0';
744+
host = address;
745+
port = slash + 1;
746+
}
747+
int res = qubes_tcp_connect(host, port);
748+
if (res == -1)
749+
return -1;
750+
*stdin_fd = *stdout_fd = res;
751+
cmd->send_service_descriptor = false;
752+
return 0;
555753
}
556754

557755
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)