From 04ce72381b5a48dd94f4c68370be6d20a6a15b99 Mon Sep 17 00:00:00 2001 From: Pawel Marczewski Date: Tue, 4 Feb 2020 11:12:08 +0100 Subject: [PATCH 1/9] agent: add options for alternative socket paths For testing as normal user. --- agent/qrexec-agent.c | 56 +++++++++++++++++++++++++++++++++++++--- agent/qrexec-client-vm.c | 13 +++++++--- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/agent/qrexec-agent.c b/agent/qrexec-agent.c index d00ec618..a1e0d090 100644 --- a/agent/qrexec-agent.c +++ b/agent/qrexec-agent.c @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -77,6 +78,9 @@ int trigger_fd; int meminfo_write_started = 0; +static const char *agent_trigger_path = QREXEC_AGENT_TRIGGER_PATH; +static const char *fork_server_path = QREXEC_FORK_SERVER_SOCKET; + void handle_server_exec_request_do(int type, int connect_domain, int connect_port, char *cmdline); void no_colon_in_cmd() @@ -355,7 +359,7 @@ void init() if (handle_handshake(ctrl_vchan) < 0) exit(1); old_umask = umask(0); - trigger_fd = get_server_socket(QREXEC_AGENT_TRIGGER_PATH); + trigger_fd = get_server_socket(agent_trigger_path); umask(old_umask); register_exec_func(do_exec); @@ -409,13 +413,16 @@ int try_fork_server(int type, int connect_domain, int connect_port, struct sockaddr_un remote; struct qrexec_cmd_info info; + if (!fork_server_path) + return -1; + strncpy(username, cmdline, cmdline_len); colon = index(username, ':'); if (!colon) return -1; *colon = '\0'; - if (asprintf(&fork_server_socket_path, QREXEC_FORK_SERVER_SOCKET, username) < 0) { + if (asprintf(&fork_server_socket_path, fork_server_path, username) < 0) { fprintf(stderr, "Memory allocation failed\n"); return -1; } @@ -934,12 +941,55 @@ void handle_terminated_fork_client(fd_set *rdset) { } } -int main() +struct option longopts[] = { + { "help", no_argument, 0, 'h' }, + { "agent-socket", required_argument, 0, 'a' }, + { "fork-server-socket", optional_argument, 0, 's' }, + { "no-fork-server", no_argument, 0, 'S' }, + { NULL, 0, 0, 0 }, +}; + +_Noreturn void usage(const char *argv0) +{ + fprintf(stderr, "usage: %s [options]\n", argv0); + fprintf(stderr, "Options:\n"); + fprintf(stderr, " -h, --help - display usage\n"); + fprintf(stderr, " --agent-socket=PATH - path to listen at, default: %s\n", + QREXEC_AGENT_TRIGGER_PATH); + fprintf(stderr, " --fork-server-socket=PATH - where to find the fork server, default: %s\n", + QREXEC_FORK_SERVER_SOCKET); + fprintf(stderr, " (set empty to disable, use %%s as username)\n"); + fprintf(stderr, " --no-fork-server - don't try to connect to fork server\n"); + exit(2); +} + +int main(int argc, char **argv) { fd_set rdset, wrset; int max; sigset_t chld_set; + int opt; + while (1) { + opt = getopt_long(argc, argv, "ha:s:S", longopts, NULL); + if (opt == -1) + break; + switch (opt) { + case 'a': + agent_trigger_path = strdup(optarg); + break; + case 's': + fork_server_path = strdup(optarg); + break; + case 'S': + fork_server_path = NULL; + break; + case 'h': + case '?': + usage(argv[0]); + } + } + init(); signal(SIGCHLD, sigchld_handler); signal(SIGPIPE, SIG_IGN); diff --git a/agent/qrexec-client-vm.c b/agent/qrexec-client-vm.c index 5ac35d86..83cd854a 100644 --- a/agent/qrexec-client-vm.c +++ b/agent/qrexec-client-vm.c @@ -43,7 +43,7 @@ void do_exec(char *cmd __attribute__((__unused__))) { exit(1); } -int connect_unix_socket(char *path) +int connect_unix_socket(const char *path) { int s, len; struct sockaddr_un remote; @@ -97,6 +97,7 @@ struct option longopts[] = { { "filter-escape-chars-stderr", no_argument, 0, 'T'}, { "no-filter-escape-chars-stdout", no_argument, 0, opt_no_filter_stdout}, { "no-filter-escape-chars-stderr", no_argument, 0, opt_no_filter_stderr}, + { "agent-socket", required_argument, 0, 'a'}, { NULL, 0, 0, 0}, }; @@ -110,6 +111,8 @@ _Noreturn void usage(const char *argv0) { fprintf(stderr, " -T, --filter-escape-chars-stderr - filter non-ASCII and control characters on stderr (default if stderr is a terminal)\n"); fprintf(stderr, " --no-filter-escape-chars-stdout - opposite to --filter-escape-chars-stdout\n"); fprintf(stderr, " --no-filter-escape-chars-stderr - opposite to --filter-escape-chars-stderr\n"); + fprintf(stderr, " --agent-socket=PATH - path to connect to, default: %s\n", + QREXEC_AGENT_TRIGGER_PATH); exit(2); } @@ -128,9 +131,10 @@ int main(int argc, char **argv) int inpipe[2], outpipe[2]; int buffer_size = 0; int opt; + const char *agent_trigger_path = QREXEC_AGENT_TRIGGER_PATH; while (1) { - opt = getopt_long(argc, argv, "+tT", longopts, NULL); + opt = getopt_long(argc, argv, "+tTa:", longopts, NULL); if (opt == -1) break; switch (opt) { @@ -149,6 +153,9 @@ int main(int argc, char **argv) case opt_no_filter_stderr: replace_chars_stderr = 0; break; + case 'a': + agent_trigger_path = strdup(optarg); + break; case '?': usage(argv[0]); } @@ -172,7 +179,7 @@ int main(int argc, char **argv) service_name_len = strlen(service_name) + 1; - trigger_fd = connect_unix_socket(QREXEC_AGENT_TRIGGER_PATH); + trigger_fd = connect_unix_socket(agent_trigger_path); hdr.type = MSG_TRIGGER_SERVICE3; hdr.len = sizeof(params) + service_name_len; From af34138ddd2482870a62c9bbcf481e2abaf760e0 Mon Sep 17 00:00:00 2001 From: Pawel Marczewski Date: Tue, 4 Feb 2020 11:12:46 +0100 Subject: [PATCH 2/9] agent: don't try to change user if not necessary --- agent/qrexec-agent.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/agent/qrexec-agent.c b/agent/qrexec-agent.c index a1e0d090..5fcfa783 100644 --- a/agent/qrexec-agent.c +++ b/agent/qrexec-agent.c @@ -164,6 +164,21 @@ void do_exec(char *cmd) signal(SIGCHLD, SIG_DFL); signal(SIGPIPE, SIG_DFL); + pw = getpwuid(geteuid()); + if (!pw) { + perror("getpwuid"); + exit(1); + } + if (!strcmp(pw->pw_name, user)) { + /* call QUBESRPC if requested */ + exec_qubes_rpc_if_requested(realcmd, environ); + + /* otherwise exec shell */ + execl("/bin/sh", "sh", "-c", realcmd, NULL); + perror("execl"); + exit(1); + } + #ifdef HAVE_PAM pw = getpwnam (user); if (! (pw && pw->pw_name && pw->pw_name[0] && pw->pw_dir && pw->pw_dir[0] From f9ec673704ddb2dbacc146da79794254323ced02 Mon Sep 17 00:00:00 2001 From: Pawel Marczewski Date: Tue, 4 Feb 2020 11:14:15 +0100 Subject: [PATCH 3/9] Add tests for agent using vchan-socket --- .travis.yml | 12 +- ci/Dockerfile | 3 +- ci/requirements.txt | 1 + libqrexec/Makefile | 3 +- run-tests | 9 ++ tests/.gitignore | 1 + tests/__init__.py | 0 tests/qrexec.py | 153 ++++++++++++++++++++++++ tests/test_agent.py | 277 ++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 454 insertions(+), 5 deletions(-) create mode 100755 run-tests create mode 100644 tests/.gitignore create mode 100644 tests/__init__.py create mode 100644 tests/qrexec.py create mode 100644 tests/test_agent.py diff --git a/.travis.yml b/.travis.yml index 531036bb..6a227669 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,17 @@ jobs: include: - python: '3.7' script: pylint qrexec - - install: + - python: '3.7' + env: VCHAN_SOCKET_TESTS=1 + install: + - sudo apt-get -y install pandoc + - pip install --quiet -r ci/requirements.txt + - git clone https://github.com/QubesOS/qubes-core-vchan-socket ~/qubes-core-vchan-socket + - make -C ~/qubes-core-vchan-socket all + - sudo make -C ~/qubes-core-vchan-socket install + script: + ./run-tests + - install: - pip install --quiet codecov - docker build -t qrexec-test ci env: DOCKER_RUN="docker run -v $TRAVIS_BUILD_DIR:$TRAVIS_BUILD_DIR -v /tmp/.X11-unix:/tmp/.X11-unix -w $TRAVIS_BUILD_DIR -e DISPLAY=$DISPLAY -- qrexec-test" diff --git a/ci/Dockerfile b/ci/Dockerfile index 9d0a28be..4d7d9408 100644 --- a/ci/Dockerfile +++ b/ci/Dockerfile @@ -1,8 +1,7 @@ FROM fedora:31 -RUN dnf install -y python3-pip python3-gobject gtk3 python3-pytest python3-coverage +RUN dnf install -y python3-pip python3-gobject gtk3 python3-pytest python3-coverage python3-devel gcc ADD requirements.txt / RUN pip3 install -r requirements.txt - diff --git a/ci/requirements.txt b/ci/requirements.txt index e814f785..4b19b3a2 100644 --- a/ci/requirements.txt +++ b/ci/requirements.txt @@ -5,3 +5,4 @@ pylint sphinx codecov pydbus +psutil diff --git a/libqrexec/Makefile b/libqrexec/Makefile index 0a6749f9..5aaa9eb7 100644 --- a/libqrexec/Makefile +++ b/libqrexec/Makefile @@ -18,7 +18,7 @@ libqrexec-utils.so: libqrexec-utils.so.$(SO_VER) %.a: $(AR) rcs $@ $^ clean: - rm -f *.o *~ *.a *.so.* + rm -f *.o *~ *.a *.so *.so.* install: mkdir -p $(DESTDIR)$(LIBDIR) @@ -27,4 +27,3 @@ install: mkdir -p $(DESTDIR)$(INCLUDEDIR) cp libqrexec-utils.h $(DESTDIR)$(INCLUDEDIR) cp qrexec.h $(DESTDIR)$(INCLUDEDIR) - diff --git a/run-tests b/run-tests new file mode 100755 index 00000000..483aa555 --- /dev/null +++ b/run-tests @@ -0,0 +1,9 @@ +#!/bin/sh -ex + +# make -C libqrexec clean +# make -C agent clean + +make -C libqrexec BACKEND_VMM=socket +make -C agent BACKEND_VMM=socket + +python3 -m unittest discover -p 'test_*.py' -v "$@" diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 00000000..0d20b648 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/qrexec.py b/tests/qrexec.py new file mode 100644 index 00000000..92d07a70 --- /dev/null +++ b/tests/qrexec.py @@ -0,0 +1,153 @@ +# -*- encoding: utf-8 -*- +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2020 Paweł Marczewski +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with this program; if not, see . + +import socket +import os +import struct +import time + + +# See libqrexec/qrexec.h +MSG_DATA_STDIN = 0x190 +MSG_DATA_STDOUT = 0x191 +MSG_DATA_STDERR = 0x192 +MSG_DATA_EXIT_CODE = 0x193 +MSG_EXEC_CMDLINE = 0x200 +MSG_JUST_EXEC = 0x201 +MSG_SERVICE_CONNECT = 0x202 +MSG_TRIGGER_SERVICE3 = 0x212 +MSG_HELLO = 0x300 +QREXEC_PROTOCOL_VERSION = 3 + + +class QrexecClient: + def __init__(self, conn): + self.conn = conn + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def sendall(self, data): + self.conn.sendall(data) + + def recvall(self, data_len): + data = b'' + while len(data) < data_len: + res = self.conn.recv(data_len - len(data)) + if not res: + return data + data += res + return data + + def close(self): + self.conn.close() + + def send_message(self, message_type, data): + header = struct.pack(' +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with this program; if not, see . + +import unittest +import subprocess +import os.path +import tempfile +import shutil +import time +import struct +import psutil +import getpass + +from . import qrexec + + +ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) + + + +class TestAgent(unittest.TestCase): + agent = None + domain = 42 + target_domain = 43 + target_port = 1024 + + def setUp(self): + self.tempdir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.tempdir) + + def start_agent(self): + env = os.environ.copy() + env['LD_LIBRARY_PATH'] = os.path.join(ROOT_PATH, 'libqrexec') + env['VCHAN_DOMAIN'] = str(self.domain) + env['VCHAN_SOCKET_DIR'] = self.tempdir + env['QREXEC_NO_ROOT'] = '1' + cmd = [ + os.path.join(ROOT_PATH, 'agent', 'qrexec-agent'), + '--no-fork-server', + '--agent-socket=' + os.path.join(self.tempdir, 'agent.sock'), + ] + if os.environ.get('USE_STRACE'): + cmd = ['strace', '-f'] + cmd + self.agent = subprocess.Popen( + cmd, + env=env, + ) + self.addCleanup(self.stop_agent) + + def stop_agent(self): + if self.agent: + self.wait_for_agent_children() + self.agent.terminate() + self.agent.wait() + self.agent = None + + def wait_for_agent_children(self): + proc = psutil.Process(self.agent.pid) + children = proc.children(recursive=True) + psutil.wait_procs(children) + + def wait_until(self, func, message, n_tries=10, delay=0.1): + for _ in range(n_tries): + if func(): + return + time.sleep(delay) + self.fail('Timed out waiting: ' + message) + + def connect_dom0(self): + dom0 = qrexec.vchan_client(self.tempdir, self.domain, 0, 512) + self.addCleanup(dom0.close) + return dom0 + + def connect_target(self): + target = qrexec.vchan_server( + self.tempdir, self.target_domain, self.domain, self.target_port) + self.addCleanup(target.close) + target.accept() + return target + + def connect_client(self): + client = qrexec.socket_client(os.path.join(self.tempdir, 'agent.sock')) + self.addCleanup(client.close) + return client + + def test_handshake(self): + self.start_agent() + + dom0 = self.connect_dom0() + dom0.handshake() + + def test_just_exec(self): + self.start_agent() + + dom0 = self.connect_dom0() + dom0.handshake() + + user = getpass.getuser().encode('ascii') + + cmd = (('touch ' + os.path.join(self.tempdir, 'new_file')) + .encode('ascii')) + dom0.send_message( + qrexec.MSG_JUST_EXEC, + struct.pack(' Date: Wed, 5 Feb 2020 21:43:59 +0100 Subject: [PATCH 4/9] Move socket tests to qrexec/tests/socket --- .travis.yml | 12 +----------- ci/Dockerfile | 8 ++++++-- {tests => qrexec/tests/socket}/.gitignore | 0 {tests => qrexec/tests/socket}/__init__.py | 0 .../tests/socket/agent.py | 12 +++++++++--- {tests => qrexec/tests/socket}/qrexec.py | 2 +- rpm_spec/qubes-qrexec.spec.in | 8 ++++++++ run-tests | 16 ++++++++++------ 8 files changed, 35 insertions(+), 23 deletions(-) rename {tests => qrexec/tests/socket}/.gitignore (100%) rename {tests => qrexec/tests/socket}/__init__.py (100%) rename tests/test_agent.py => qrexec/tests/socket/agent.py (97%) rename {tests => qrexec/tests/socket}/qrexec.py (99%) diff --git a/.travis.yml b/.travis.yml index 6a227669..490ccace 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,21 +22,11 @@ jobs: include: - python: '3.7' script: pylint qrexec - - python: '3.7' - env: VCHAN_SOCKET_TESTS=1 - install: - - sudo apt-get -y install pandoc - - pip install --quiet -r ci/requirements.txt - - git clone https://github.com/QubesOS/qubes-core-vchan-socket ~/qubes-core-vchan-socket - - make -C ~/qubes-core-vchan-socket all - - sudo make -C ~/qubes-core-vchan-socket install - script: - ./run-tests - install: - pip install --quiet codecov - docker build -t qrexec-test ci env: DOCKER_RUN="docker run -v $TRAVIS_BUILD_DIR:$TRAVIS_BUILD_DIR -v /tmp/.X11-unix:/tmp/.X11-unix -w $TRAVIS_BUILD_DIR -e DISPLAY=$DISPLAY -- qrexec-test" - script: $DOCKER_RUN python3 -m coverage run -m unittest discover -s qrexec/tests -t . -p '*.py' -v + script: $DOCKER_RUN ./run-tests # - python: '3.7' # script: python -m coverage run -m unittest discover -s qrexec/tests -t . -p '*.py' -v - stage: deploy diff --git a/ci/Dockerfile b/ci/Dockerfile index 4d7d9408..db5edadd 100644 --- a/ci/Dockerfile +++ b/ci/Dockerfile @@ -1,7 +1,11 @@ FROM fedora:31 -RUN dnf install -y python3-pip python3-gobject gtk3 python3-pytest python3-coverage python3-devel gcc +RUN dnf install -y python3-pip python3-gobject gtk3 python3-pytest \ + python3-coverage python3-devel pam-devel pandoc gcc git make -ADD requirements.txt / +RUN git clone https://github.com/QubesOS/qubes-core-vchan-socket ~/qubes-core-vchan-socket +RUN make -C ~/qubes-core-vchan-socket all +RUN make -C ~/qubes-core-vchan-socket install LIBDIR=/usr/lib64 +ADD requirements.txt / RUN pip3 install -r requirements.txt diff --git a/tests/.gitignore b/qrexec/tests/socket/.gitignore similarity index 100% rename from tests/.gitignore rename to qrexec/tests/socket/.gitignore diff --git a/tests/__init__.py b/qrexec/tests/socket/__init__.py similarity index 100% rename from tests/__init__.py rename to qrexec/tests/socket/__init__.py diff --git a/tests/test_agent.py b/qrexec/tests/socket/agent.py similarity index 97% rename from tests/test_agent.py rename to qrexec/tests/socket/agent.py index 9e7fba68..8101988c 100644 --- a/tests/test_agent.py +++ b/qrexec/tests/socket/agent.py @@ -20,20 +20,24 @@ import unittest import subprocess import os.path +import os import tempfile import shutil import time import struct -import psutil import getpass +import psutil -from . import qrexec +from . import qrexec -ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), + '..', '..', '..')) +@unittest.skipIf(os.environ.get('SKIP_SOCKET_TESTS'), + 'socket tests not set up') class TestAgent(unittest.TestCase): agent = None domain = 42 @@ -201,6 +205,8 @@ def test_trigger_service(self): (self.target_domain, self.target_port)) +@unittest.skipIf(os.environ.get('SKIP_SOCKET_TESTS'), + 'socket tests not set up') class TestClientVm(unittest.TestCase): client = None domain = 42 diff --git a/tests/qrexec.py b/qrexec/tests/socket/qrexec.py similarity index 99% rename from tests/qrexec.py rename to qrexec/tests/socket/qrexec.py index 92d07a70..2d08d3a6 100644 --- a/tests/qrexec.py +++ b/qrexec/tests/socket/qrexec.py @@ -95,8 +95,8 @@ def handshake(self): class QrexecServer(QrexecClient): def __init__(self, server_conn): + super().__init__(None) self.server_conn = server_conn - self.conn = None def close(self): if self.server_conn: diff --git a/rpm_spec/qubes-qrexec.spec.in b/rpm_spec/qubes-qrexec.spec.in index 6a0c4724..721ea3fe 100644 --- a/rpm_spec/qubes-qrexec.spec.in +++ b/rpm_spec/qubes-qrexec.spec.in @@ -142,6 +142,14 @@ rm -f %{name}-%{version} %{python3_sitelib}/qrexec/tests/policy_api.py %{python3_sitelib}/qrexec/tests/policy_parser.py +%dir %{python3_sitelib}/qrexec/tests/socket +%dir %{python3_sitelib}/qrexec/tests/socket/__pycache__ +%{python3_sitelib}/qrexec/tests/socket/__pycache__/* +%{python3_sitelib}/qrexec/tests/socket/__init__.py +%{python3_sitelib}/qrexec/tests/socket/agent.py +%{python3_sitelib}/qrexec/tests/socket/qrexec.py + + %dir %{python3_sitelib}/qrexec/glade %{python3_sitelib}/qrexec/glade/PolicyCreateConfirmationWindow.glade %{python3_sitelib}/qrexec/glade/RPCConfirmationWindow.glade diff --git a/run-tests b/run-tests index 483aa555..14e63db9 100755 --- a/run-tests +++ b/run-tests @@ -1,9 +1,13 @@ -#!/bin/sh -ex +#!/bin/sh -e -# make -C libqrexec clean -# make -C agent clean +if pkg-config vchan-socket; then + make -C libqrexec BACKEND_VMM=socket + make -C agent BACKEND_VMM=socket +else + echo "libvchan-socket not available, skipping socket tests" + export SKIP_SOCKET_TESTS=1 +fi -make -C libqrexec BACKEND_VMM=socket -make -C agent BACKEND_VMM=socket +set -x -python3 -m unittest discover -p 'test_*.py' -v "$@" +python3 -m coverage run -m unittest discover -s qrexec/tests -t . -p '*.py' -v "$@" From 3aaa5b085b4379dbc490c89b20bc588b76860cfe Mon Sep 17 00:00:00 2001 From: Pawel Marczewski Date: Thu, 6 Feb 2020 11:16:22 +0100 Subject: [PATCH 5/9] Add tests for MSG_SERVICE_REFUSED --- qrexec/tests/socket/agent.py | 89 +++++++++++++++++++++++++---------- qrexec/tests/socket/qrexec.py | 1 + 2 files changed, 66 insertions(+), 24 deletions(-) diff --git a/qrexec/tests/socket/agent.py b/qrexec/tests/socket/agent.py index 8101988c..a37b452f 100644 --- a/qrexec/tests/socket/agent.py +++ b/qrexec/tests/socket/agent.py @@ -173,25 +173,8 @@ def test_trigger_service(self): dom0.handshake() client = self.connect_client() - client.send_message( - qrexec.MSG_TRIGGER_SERVICE3, - struct.pack('<64s32s', - target_domain_name, b'SOCKET') + - b'qubes.ServiceName\0' - ) - - message_type, data = dom0.recv_message() - self.assertEqual(message_type, qrexec.MSG_TRIGGER_SERVICE3) - - target, ident = struct.unpack('<64s32s', data[:96]) - target = target[:target.find(b'\0')] - ident = ident[:ident.find(b'\0')] - self.assertEqual(target, target_domain_name) - self.assertTrue(ident.startswith(b'SOCKET'), - 'wrong ident: {}'.format(ident)) - - service_name = data[96:] - self.assertEqual(service_name, b'qubes.ServiceName\0') + ident = self.trigger_service( + dom0, client, target_domain_name, b'qubes.ServiceName') dom0.send_message( qrexec.MSG_SERVICE_CONNECT, @@ -204,6 +187,53 @@ def test_trigger_service(self): self.assertEqual(struct.unpack(' Date: Thu, 6 Feb 2020 11:33:43 +0100 Subject: [PATCH 6/9] daemon: add options to enable testing - different socket directory - different policy program - no daemonizing (so that we can kill it more easily) Add an option for socket directory, and don't open a log file. --- daemon/qrexec-daemon.c | 149 +++++++++++++++++++++++++++-------------- libqrexec/qrexec.h | 1 + 2 files changed, 98 insertions(+), 52 deletions(-) diff --git a/daemon/qrexec-daemon.c b/daemon/qrexec-daemon.c index 7a830c45..6146d28e 100644 --- a/daemon/qrexec-daemon.c +++ b/daemon/qrexec-daemon.c @@ -30,6 +30,7 @@ #include #include #include +#include #include "qrexec.h" #include "libqrexec-utils.h" @@ -86,6 +87,10 @@ const char default_user_keyword[] = "DEFAULT:"; #define default_user_keyword_len_without_colon (sizeof(default_user_keyword)-2) int opt_quiet = 0; +int opt_direct = 0; + +const char *socket_dir = QREXEC_DAEMON_SOCKET_DIR; +const char *policy_program = QREXEC_POLICY_PROGRAM; #ifdef __GNUC__ # define UNUSED(x) UNUSED_ ## x __attribute__((__unused__)) @@ -129,9 +134,9 @@ void unlink_qrexec_socket() char link_to_socket_name[strlen(remote_domain_name) + sizeof(socket_address)]; snprintf(socket_address, sizeof(socket_address), - QREXEC_DAEMON_SOCKET_DIR "/qrexec.%d", remote_domain_id); + "%s/qrexec.%d", socket_dir, remote_domain_id); snprintf(link_to_socket_name, sizeof link_to_socket_name, - QREXEC_DAEMON_SOCKET_DIR "/qrexec.%s", remote_domain_name); + "%s/qrexec.%s", socket_dir, remote_domain_name); unlink(socket_address); unlink(link_to_socket_name); } @@ -149,9 +154,9 @@ int create_qrexec_socket(int domid, const char *domname) char link_to_socket_name[strlen(domname) + sizeof(socket_address)]; snprintf(socket_address, sizeof(socket_address), - QREXEC_DAEMON_SOCKET_DIR "/qrexec.%d", domid); + "%s/qrexec.%d", socket_dir, domid); snprintf(link_to_socket_name, sizeof link_to_socket_name, - QREXEC_DAEMON_SOCKET_DIR "/qrexec.%s", domname); + "%s/qrexec.%s", socket_dir, domname); unlink(link_to_socket_name); if (symlink(socket_address, link_to_socket_name)) { fprintf(stderr, "symlink(%s,%s) failed: %s\n", socket_address, @@ -256,53 +261,60 @@ void init(int xid) // invalid or negative number startup_timeout = MAX_STARTUP_TIME_DEFAULT; } - signal(SIGUSR1, sigusr1_handler); - signal(SIGCHLD, sigchld_parent_handler); - switch (pid=fork()) { - case -1: - perror("fork"); - exit(1); - case 0: - break; - default: - if (getenv("QREXEC_STARTUP_NOWAIT")) - exit(0); - if (!opt_quiet) - fprintf(stderr, "Waiting for VM's qrexec agent."); - for (i=0;i 3) { - fprintf(stderr, "usage: %s [-q] domainid domain-name [default user]\n", argv[0]); - exit(1); + usage(argv[0]); } remote_domain_id = atoi(argv[optind]); remote_domain_name = argv[optind+1]; diff --git a/libqrexec/qrexec.h b/libqrexec/qrexec.h index 8932cc27..ed2f7263 100644 --- a/libqrexec/qrexec.h +++ b/libqrexec/qrexec.h @@ -164,5 +164,6 @@ enum { #define MEMINFO_WRITER_PIDFILE "/var/run/meminfo-writer.pid" #define QUBES_RPC_MULTIPLEXER_PATH "/usr/lib/qubes/qubes-rpc-multiplexer" #define QREXEC_DAEMON_SOCKET_DIR "/var/run/qubes" +#define QREXEC_POLICY_PROGRAM "/usr/bin/qrexec-policy-exec" #endif /* _QREXEC_H */ From 735e042d1f99a2a495a4d84edd5f3862e20cd0bf Mon Sep 17 00:00:00 2001 From: Pawel Marczewski Date: Thu, 6 Feb 2020 13:35:34 +0100 Subject: [PATCH 7/9] Add tests for qrexec-daemon --- daemon/Makefile | 5 +- qrexec/tests/socket/agent.py | 1 - qrexec/tests/socket/daemon.py | 187 ++++++++++++++++++++++++++++++++++ rpm_spec/qubes-qrexec.spec.in | 1 + run-tests | 1 + 5 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 qrexec/tests/socket/daemon.py diff --git a/daemon/Makefile b/daemon/Makefile index db8d9b04..dc347a31 100644 --- a/daemon/Makefile +++ b/daemon/Makefile @@ -2,10 +2,10 @@ CC=gcc CFLAGS+=-I. -g -O2 -Wall -Wextra -Werror -pie -fPIC `pkg-config --cflags vchan-$(BACKEND_VMM)` CFLAGS += -I../libqrexec LIBS = -L../libqrexec -LIBS=`pkg-config --libs vchan-$(BACKEND_VMM)` -lqrexec-utils +LIBS += `pkg-config --libs vchan-$(BACKEND_VMM)` -lqrexec-utils -all: qrexec-daemon qrexec-client +all: qrexec-daemon qrexec-client clean: rm -f *.o *~ qrexec-daemon qrexec-client install: @@ -21,4 +21,3 @@ qrexec-daemon: qrexec-daemon.o $(CC) -pie -g -o qrexec-daemon qrexec-daemon.o $(LIBS) qrexec-client: qrexec-client.o $(CC) -pie -g -o qrexec-client qrexec-client.o $(LIBS) - diff --git a/qrexec/tests/socket/agent.py b/qrexec/tests/socket/agent.py index a37b452f..6fe507f4 100644 --- a/qrexec/tests/socket/agent.py +++ b/qrexec/tests/socket/agent.py @@ -53,7 +53,6 @@ def start_agent(self): env['LD_LIBRARY_PATH'] = os.path.join(ROOT_PATH, 'libqrexec') env['VCHAN_DOMAIN'] = str(self.domain) env['VCHAN_SOCKET_DIR'] = self.tempdir - env['QREXEC_NO_ROOT'] = '1' cmd = [ os.path.join(ROOT_PATH, 'agent', 'qrexec-agent'), '--no-fork-server', diff --git a/qrexec/tests/socket/daemon.py b/qrexec/tests/socket/daemon.py new file mode 100644 index 00000000..6f552f19 --- /dev/null +++ b/qrexec/tests/socket/daemon.py @@ -0,0 +1,187 @@ +# -*- encoding: utf-8 -*- +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2020 Paweł Marczewski +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with this program; if not, see . + +import unittest +import subprocess +import os.path +import os +import tempfile +import shutil +import struct +import psutil +from typing import Tuple + +from . import qrexec + + +ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), + '..', '..', '..')) + + +@unittest.skipIf(os.environ.get('SKIP_SOCKET_TESTS'), + 'socket tests not set up') +class TestDaemon(unittest.TestCase): + daemon = None + domain = 42 + domain_name = 'domain_name' + + # Stub qrexec-policy-exec program. + # Strictly speaking, the program should also run qrexec-client in case the + # call is allowed, but we will simulate that elsewhere. + POLICY_PROGRAM = '''\ +#!/bin/sh + +echo "$@" > {tempdir}/qrexec-policy-params +exit 1 +''' + + def setUp(self): + self.tempdir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.tempdir) + + def start_daemon(self): + policy_program_path = os.path.join(self.tempdir, 'qrexec-policy-exec') + with open(policy_program_path, 'w') as f: + f.write(self.POLICY_PROGRAM.format(tempdir=self.tempdir)) + os.chmod(policy_program_path, 0o700) + + env = os.environ.copy() + env['LD_LIBRARY_PATH'] = os.path.join(ROOT_PATH, 'libqrexec') + env['VCHAN_DOMAIN'] = '0' + env['VCHAN_SOCKET_DIR'] = self.tempdir + cmd = [ + os.path.join(ROOT_PATH, 'daemon', 'qrexec-daemon'), + '--socket-dir=' + self.tempdir, + '--policy-program=' + policy_program_path, + '--direct', + str(self.domain), + self.domain_name, + ] + if os.environ.get('USE_STRACE'): + cmd = ['strace', '-f'] + cmd + self.daemon = subprocess.Popen( + cmd, + env=env, + ) + self.addCleanup(self.stop_daemon) + + def stop_daemon(self): + if self.daemon: + self.wait_for_daemon_children() + self.daemon.terminate() + self.daemon.wait() + self.daemon = None + + def wait_for_daemon_children(self): + proc = psutil.Process(self.daemon.pid) + children = proc.children(recursive=True) + psutil.wait_procs(children) + + def get_policy_program_params(self): + with open(os.path.join(self.tempdir, 'qrexec-policy-params')) as f: + return f.read().split() + + def start_daemon_with_agent(self): + agent = self.connect_agent() + self.start_daemon() + agent.accept() + return agent + + def connect_agent(self): + agent = qrexec.vchan_server( + self.tempdir, self.domain, 0, 512) + self.addCleanup(agent.close) + return agent + + def connect_client(self): + client = qrexec.socket_client( + os.path.join(self.tempdir, 'qrexec.{}'.format(self.domain))) + self.addCleanup(client.close) + return client + + def test_handshake(self): + agent = self.start_daemon_with_agent() + agent.handshake() + + def test_trigger_service_refused(self): + agent = self.start_daemon_with_agent() + agent.handshake() + + target_domain_name = 'target_domain' + ident = 'SOCKET42' + + message_type, data = self.trigger_service( + agent, target_domain_name, 'qubes.ForbiddenServiceName', ident) + self.assertEqual(message_type, qrexec.MSG_SERVICE_REFUSED) + self.assertEqual(data, struct.pack('<32s', ident.encode())) + + def trigger_service(self, + agent, + target_domain_name: str, + service_name: str, + ident: str) -> Tuple[int, bytes]: + agent.send_message( + qrexec.MSG_TRIGGER_SERVICE3, + struct.pack('<64s32s', + target_domain_name.encode(), ident.encode()) + + service_name.encode() + b'\0' + ) + message_type, data = agent.recv_message() + self.assertListEqual(self.get_policy_program_params(), [ + '--', + str(self.domain), + self.domain_name, + target_domain_name, + service_name, + ident + ]) + + return message_type, data + + def test_client_handshake(self): + agent = self.start_daemon_with_agent() + agent.handshake() + + client = self.connect_client() + client.handshake() + + def test_client_cmdline(self): + agent = self.start_daemon_with_agent() + agent.handshake() + + client = self.connect_client() + client.handshake() + + cmd = b'user:echo Hello world' + + client.send_message( + qrexec.MSG_JUST_EXEC, + struct.pack(' Date: Thu, 6 Feb 2020 14:02:21 +0100 Subject: [PATCH 8/9] Don't run tests as root in Docker --- ci/Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ci/Dockerfile b/ci/Dockerfile index db5edadd..f7bddf39 100644 --- a/ci/Dockerfile +++ b/ci/Dockerfile @@ -9,3 +9,6 @@ RUN make -C ~/qubes-core-vchan-socket install LIBDIR=/usr/lib64 ADD requirements.txt / RUN pip3 install -r requirements.txt + +RUN useradd --create-home --shell /bin/bash travis --uid 2000 +USER travis From df1dbf5b378bc2cc763ae1e41bc4c01a41e56fc9 Mon Sep 17 00:00:00 2001 From: Pawel Marczewski Date: Thu, 6 Feb 2020 20:29:10 +0100 Subject: [PATCH 9/9] qrexec-daemon: don't open log file when run with --direct --- daemon/qrexec-daemon.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daemon/qrexec-daemon.c b/daemon/qrexec-daemon.c index 6146d28e..6a55efb3 100644 --- a/daemon/qrexec-daemon.c +++ b/daemon/qrexec-daemon.c @@ -291,7 +291,7 @@ void init(int xid) close(0); - if (getuid() == 0) { + if (!opt_direct) { snprintf(qrexec_error_log_name, sizeof(qrexec_error_log_name), "/var/log/qubes/qrexec.%s.log", remote_domain_name); umask(0007); // make the log readable by the "qubes" group @@ -971,7 +971,7 @@ _Noreturn void usage(const char *argv0) QREXEC_DAEMON_SOCKET_DIR); fprintf(stderr, " -p, --policy-program=PATH - program to execute to check policy, default: %s\n", QREXEC_POLICY_PROGRAM); - fprintf(stderr, " -D, --direct - run directly, don't daemonize\n"); + fprintf(stderr, " -D, --direct - run directly, don't daemonize, log to stderr\n"); exit(1); }