Skip to content

Commit c01e533

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 695c3f3 commit c01e533

File tree

5 files changed

+355
-8
lines changed

5 files changed

+355
-8
lines changed

libqrexec/exec.c

+145-4
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"
@@ -274,6 +275,10 @@ static int find_file(
274275
{
275276
if (target_len >= buffer_size) {
276277
/* buffer too small */
278+
LOG(ERROR, "Buffer size %zu too small for target length %zu", buffer_size, target_len);
279+
rc = -2;
280+
} else if (target_len == sizeof("/dev/tcp")) {
281+
LOG(ERROR, "/dev/tcp/ not followed by host");
277282
rc = -2;
278283
} else {
279284
memcpy(buffer, buf, target_len);
@@ -425,7 +430,7 @@ struct qrexec_parsed_command *parse_qubes_rpc_command(
425430

426431
/* Parse service name ("qubes.Service") */
427432

428-
const char *const plus = memchr(start, '+', descriptor_len);
433+
char *const plus = memchr(start, '+', descriptor_len);
429434
size_t const name_len = plus != NULL ? (size_t)(plus - start) : descriptor_len;
430435
if (name_len > NAME_MAX) {
431436
LOG(ERROR, "Service name too long to execute (length %zu)", name_len);
@@ -445,6 +450,8 @@ struct qrexec_parsed_command *parse_qubes_rpc_command(
445450
goto err;
446451
if (plus == NULL)
447452
cmd->service_descriptor[descriptor_len] = '+';
453+
else
454+
cmd->arg = cmd->service_descriptor + (plus + 1 - start);
448455

449456
/* Parse source domain */
450457

@@ -495,7 +502,7 @@ int execute_qubes_rpc_command(const char *cmdline, int *pid, int *stdin_fd,
495502
}
496503

497504
int execute_parsed_qubes_rpc_command(
498-
const struct qrexec_parsed_command *cmd, int *pid, int *stdin_fd,
505+
struct qrexec_parsed_command *cmd, int *pid, int *stdin_fd,
499506
int *stdout_fd, int *stderr_fd, struct buffer *stdin_buffer) {
500507
if (cmd->service_descriptor) {
501508
// Proper Qubes RPC call
@@ -516,9 +523,81 @@ int execute_parsed_qubes_rpc_command(
516523
pid, stdin_fd, stdout_fd, stderr_fd);
517524
}
518525
}
526+
static bool validate_port(const char *port) {
527+
#define MAXPORT "65535"
528+
#define MAXPORTLEN (sizeof MAXPORT - 1)
529+
if (*port < '1' || *port > '9')
530+
return false;
531+
const char *p = port + 1;
532+
for (; *p != '\0'; ++p) {
533+
if (*p < '0' || *p > '9')
534+
return false;
535+
}
536+
if (p - port > (ptrdiff_t)MAXPORTLEN)
537+
return false;
538+
if (p - port < (ptrdiff_t)MAXPORTLEN)
539+
return true;
540+
return memcmp(port, MAXPORT, MAXPORTLEN) <= 0;
541+
#undef MAXPORT
542+
#undef MAXPORTLEN
543+
}
544+
545+
static int qubes_tcp_connect(const char *host, const char *port)
546+
{
547+
// Work around a glibc bug: overly-large port numbers not rejected
548+
if (!validate_port(port)) {
549+
LOG(ERROR, "Invalid port number %s", port);
550+
return -1;
551+
}
552+
/* If there is ':' or '%' in the host, then this must be an IPv6 address, not IPv4. */
553+
bool const must_be_ipv6_addr = strchr(host, ':') != NULL || strchr(host, '%') != NULL;
554+
LOG(DEBUG, "Connecting to %s%s%s:%s",
555+
must_be_ipv6_addr ? "[" : "",
556+
host,
557+
must_be_ipv6_addr ? "]" : "",
558+
port);
559+
struct addrinfo hints = {
560+
.ai_flags = AI_NUMERICSERV | AI_NUMERICHOST,
561+
.ai_family = must_be_ipv6_addr ? AF_INET6 : AF_UNSPEC,
562+
.ai_socktype = SOCK_STREAM,
563+
.ai_protocol = IPPROTO_TCP,
564+
}, *addrs;
565+
int rc = getaddrinfo(host, port, &hints, &addrs);
566+
if (rc != 0) {
567+
/* data comes from symlink or from qrexec service argument, which has already
568+
* been sanitized */
569+
LOG(ERROR, "getaddrinfo(%s, %s) failed: %s", host, port, gai_strerror(rc));
570+
return -1;
571+
}
572+
rc = -1;
573+
assert(addrs != NULL && "getaddrinfo() returned zero addresses");
574+
assert(addrs->ai_next == NULL &&
575+
"getaddrinfo() returned multiple addresses despite AI_NUMERICHOST | AI_NUMERICSERV");
576+
int sockfd = socket(addrs->ai_family,
577+
addrs->ai_socktype | SOCK_CLOEXEC,
578+
addrs->ai_protocol);
579+
if (sockfd < 0)
580+
goto freeaddrs;
581+
{
582+
int one = 1;
583+
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one) != 0)
584+
abort();
585+
}
586+
int res = connect(sockfd, addrs->ai_addr, addrs->ai_addrlen);
587+
if (res != 0) {
588+
PERROR("connect");
589+
close(sockfd);
590+
} else {
591+
rc = sockfd;
592+
LOG(DEBUG, "Connection succeeded");
593+
}
594+
freeaddrs:
595+
freeaddrinfo(addrs);
596+
return rc;
597+
}
519598

520599
bool find_qrexec_service(
521-
const struct qrexec_parsed_command *cmd,
600+
struct qrexec_parsed_command *cmd,
522601
int *socket_fd, struct buffer *stdin_buffer) {
523602
assert(cmd->service_descriptor);
524603

@@ -565,6 +644,68 @@ bool find_qrexec_service(
565644

566645
*socket_fd = s;
567646
return true;
647+
} else if (S_ISLNK(statbuf.st_mode)) {
648+
/* TCP-based service */
649+
assert(path_buffer.buflen >= (int)sizeof("/dev/tcp") - 1);
650+
assert(memcmp(path_buffer.data, "/dev/tcp", sizeof("/dev/tcp") - 1) == 0);
651+
char *address = path_buffer.data + (sizeof("/dev/tcp") - 1);
652+
char *host = NULL, *port = NULL;
653+
if (*address == '/') {
654+
host = address + 1;
655+
char *slash = strchr(host, '/');
656+
if (slash != NULL) {
657+
*slash = '\0';
658+
port = slash + 1;
659+
}
660+
} else {
661+
assert(*address == '\0');
662+
}
663+
if (port == NULL) {
664+
if (cmd->arg == NULL || *cmd->arg == '\0') {
665+
LOG(ERROR, "No or empty argument provided, cannot connect to %s",
666+
path_buffer.data);
667+
return false;
668+
}
669+
if (host == NULL) {
670+
/* Get both host and port from service arguments */
671+
host = cmd->arg;
672+
port = strrchr(cmd->arg, '+');
673+
if (port == NULL) {
674+
LOG(ERROR, "No port provided, cannot connect to %s", cmd->arg);
675+
return false;
676+
}
677+
*port = '\0';
678+
for (char *p = host; p < port; ++p) {
679+
if (*p == '_') {
680+
LOG(ERROR, "Underscore not allowed in hostname %s", host);
681+
return false;
682+
}
683+
if (*p == '+')
684+
*p = ':';
685+
}
686+
port++;
687+
} else {
688+
/* Get just port from service arguments */
689+
port = cmd->arg;
690+
}
691+
} else {
692+
if (cmd->arg != NULL && *cmd->arg != '\0') {
693+
LOG(ERROR, "Unexpected argument %s to service %s", cmd->arg, path_buffer.data);
694+
return false;
695+
}
696+
}
697+
698+
if (cmd->send_service_descriptor) {
699+
/* send part after "QUBESRPC ", including trailing NUL */
700+
const char *desc = cmd->command + RPC_REQUEST_COMMAND_LEN + 1;
701+
buffer_append(stdin_buffer, desc, strlen(desc) + 1);
702+
}
703+
704+
int res = qubes_tcp_connect(host, port);
705+
if (res == -1)
706+
return false;
707+
*socket_fd = res;
708+
return true;
568709
}
569710

570711
if (euidaccess(path_buffer.data, X_OK) == 0) {

libqrexec/libqrexec-utils.h

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

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

@@ -83,6 +85,13 @@ struct qrexec_parsed_command {
8385

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

8897
/* Parse a command, return NULL on failure. Uses cmd->cmdline
@@ -142,7 +151,7 @@ int write_stdin(int fd, const char *data, int len, 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
/**
@@ -157,7 +166,7 @@ int execute_parsed_qubes_rpc_command(
157166
* successfully, false on failure.
158167
*/
159168
bool find_qrexec_service(
160-
const struct qrexec_parsed_command *cmd,
169+
struct qrexec_parsed_command *cmd,
161170
int *socket_fd, struct buffer *stdin_buffer);
162171

163172
/** Suggested buffer size for the path buffer of find_qrexec_service. */

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)