Skip to content

Commit 96192f7

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 cbebc3a commit 96192f7

File tree

4 files changed

+383
-24
lines changed

4 files changed

+383
-24
lines changed

libqrexec/exec.c

+203-21
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

@@ -265,21 +266,17 @@ static int find_file(
265266
if (res == 0 && S_ISLNK(statbuf->st_mode)) {
266267
if (buf != NULL) {
267268
ssize_t res = readlink(buffer, buf, buffer_size);
268-
if (res >= (ssize_t)(sizeof "/dev/tcp/" - 1) &&
269-
memcmp(buf, "/dev/tcp/", (sizeof "/dev/tcp/" - 1)) == 0) {
270-
if ((size_t)res >= buffer_size) {
271-
errno = ERANGE;
272-
rc = -2;
273-
} else {
274-
memcpy(buffer, buf, (size_t)res);
275-
buffer[res] = '\0';
276-
rc = 0;
277-
}
278-
goto done;
279-
} else if (res < 0) {
269+
if (res < (ssize_t)(sizeof "/dev/tcp") ||
270+
(size_t)res >= buffer_size ||
271+
memcmp(buf, "/dev/tcp/", sizeof "/dev/tcp") != 0) {
280272
rc = -2;
281-
goto done;
273+
} else {
274+
memcpy(buffer, buf, (size_t)res);
275+
buffer[res] = '\0';
276+
rc = 0;
277+
LOG(ERROR, "Symlink to /dev/tcp detected: value is %s", buffer);
282278
}
279+
goto done;
283280
}
284281
/* check if the symlink is valid */
285282
res = stat(buffer, statbuf);
@@ -410,7 +407,7 @@ struct qrexec_parsed_command *parse_qubes_rpc_command(
410407
/* Parse service name ("qubes.Service") */
411408

412409
const char *const plus = memchr(start, '+', descriptor_len);
413-
size_t const name_len = plus != NULL ? (size_t)(plus - start) : descriptor_len;
410+
size_t name_len = plus != NULL ? (size_t)(plus - start) : descriptor_len;
414411
if (name_len > NAME_MAX) {
415412
LOG(ERROR, "Service name too long to execute (length %zu)", name_len);
416413
goto err;
@@ -426,6 +423,7 @@ struct qrexec_parsed_command *parse_qubes_rpc_command(
426423
}
427424
memcpy(cmd->service_name, start, name_len);
428425
cmd->service_name[name_len] = '\0';
426+
cmd->arg = plus != NULL ? plus + 1 : NULL;
429427

430428
/* If there is no service argument, add a trailing "+" to the descriptor */
431429
size_t const descriptor_buffer_size = descriptor_len + 1 + (plus == NULL);
@@ -493,7 +491,7 @@ int execute_qubes_rpc_command(const char *cmdline, int *pid, int *stdin_fd,
493491
}
494492

495493
int execute_parsed_qubes_rpc_command(
496-
const struct qrexec_parsed_command *cmd, int *pid, int *stdin_fd,
494+
struct qrexec_parsed_command *cmd, int *pid, int *stdin_fd,
497495
int *stdout_fd, int *stderr_fd, struct buffer *stdin_buffer) {
498496
if (cmd->service_descriptor) {
499497
// Proper Qubes RPC call
@@ -506,8 +504,145 @@ int execute_parsed_qubes_rpc_command(
506504
}
507505
}
508506

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

@@ -533,10 +668,11 @@ static int execute_qrexec_service(
533668
return -1;
534669
}
535670

671+
const char *desc = cmd->command + RPC_REQUEST_COMMAND_LEN + 1;
536672
if (S_ISSOCK(statbuf.st_mode)) {
537673
/* Socket-based service. */
538674
int s;
539-
if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
675+
if ((s = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) == -1) {
540676
PERROR("socket");
541677
return -1;
542678
}
@@ -550,14 +686,60 @@ static int execute_qrexec_service(
550686
if (stderr_fd)
551687
*stderr_fd = -1;
552688
*pid = 0;
553-
set_nonblock(s);
554689

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

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