From 710b18a1ed57dc3737b0b853447f2b7716ccb3ab Mon Sep 17 00:00:00 2001 From: "Manuel Amador (Rudd-O)" Date: Tue, 14 Apr 2020 03:43:34 +0000 Subject: [PATCH] Include dom0 agent in source. --- .gitignore | 3 + Makefile | 14 ++- Makefile.builder | 2 - README.md | 139 ++++++++++------------------ qubes-network-server.spec | 30 +++++- qubesnetworkserver/__init__.py | 98 ++++++++++++++++++++ setup.py | 24 +++++ src/qubes-network-server-dom0.patch | 122 ------------------------ src/qubes-routing-manager | 4 +- 9 files changed, 213 insertions(+), 223 deletions(-) create mode 100644 qubesnetworkserver/__init__.py create mode 100644 setup.py delete mode 100644 src/qubes-network-server-dom0.patch diff --git a/.gitignore b/.gitignore index dd58e6e..2dd9a98 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ pkgs/ *.tar.gz *.rpm .*.swp +build +*.egg-info +src/*.service diff --git a/Makefile b/Makefile index fd703c6..8f41aa2 100644 --- a/Makefile +++ b/Makefile @@ -10,11 +10,12 @@ src/qubes-routing-manager.service: src/qubes-routing-manager.service.in ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) -.PHONY: clean dist rpm srpm install +.PHONY: clean dist rpm srpm install-template install-dom0 clean: - find -name '*.pyc' -o -name '*~' -print0 | xargs -0 rm -f - rm -rf *.tar.gz *.rpm + cd $(ROOT_DIR) || exit $$? ; find -name '*.pyc' -o -name '*~' -print0 | xargs -0 rm -f + cd $(ROOT_DIR) || exit $$? ; rm -rf *.tar.gz *.rpm + cd $(ROOT_DIR) || exit $$? ; rm -rf *.egg-info build dist: clean @which rpmspec || { echo 'rpmspec is not available. Please install the rpm-build package with the command `dnf install rpm-build` to continue, then rerun this step.' ; exit 1 ; } @@ -29,6 +30,11 @@ rpm: dist cd $(ROOT_DIR) || exit $$? ; rpmbuild --define "_srcrpmdir ." --define "_rpmdir builddir.rpm" -ta `rpmspec -q --queryformat '%{name}-%{version}.tar.gz\n' *spec | head -1` cd $(ROOT_DIR) ; mv -f builddir.rpm/*/* . && rm -rf builddir.rpm -install: all +install-template: all install -Dm 755 src/qubes-routing-manager -t $(DESTDIR)/$(SBINDIR)/ install -Dm 644 src/qubes-routing-manager.service -t $(DESTDIR)/$(UNITDIR)/ + +install-dom0: + PYTHONDONTWRITEBYTECODE=1 python3 setup.py install $(PYTHON_PREFIX_ARG) -O0 --root $(DESTDIR) + +install: install-dom0 install-template diff --git a/Makefile.builder b/Makefile.builder index 54c7119..a6812ac 100644 --- a/Makefile.builder +++ b/Makefile.builder @@ -1,3 +1 @@ -ifneq ($(PACKAGE_SET),dom0) RPM_SPEC_FILES=qubes-network-server.spec -endif diff --git a/README.md b/README.md index cc41285..136b1c5 100644 --- a/README.md +++ b/README.md @@ -102,97 +102,20 @@ Here are documents that will help you take advantage of Qubes network server: ## Installation -Installation consists of two steps. +Installation consists of two steps: -### Apply software patch to `dom0` +1. Deploy the `qubes-core-admin-addon-network-server` RPM to your `dom0`. +2. Deploy the `qubes-network-server` RPM to the TemplateVM backing your + NetVM (which must be a Fedora instance). If your NetVM is a StandaloneVM, + then you must deploy this RPM to the NetVM directly. -The first step is to apply a patch to your Qubes OS `dom0`. -The patch is distributed in the source, within the `src/` directory. -Future releases of this software will contain a better method for -deploying the `dom0` part of the software. +After that, to make it all take effect: -Copy the patch to your `dom0` somehow, then apply it: +1. Power off the TemplateVM. +2. Reboot the NetVM. -``` -# Change into the Python 3.5 site-packages directory. -cd /usr/lib/python3.5/site-packages - -# Back up your `net.py` file. -if ! test -f qubes/vm/mix/net.py.bak - sudo cp qubes/vm/mix/net.py qubes/vm/mix/net.py.bak -fi - -# Patch the `net.py` file. -sudo patch -p1 < /home/user/qubes-network-server-dom0.patch - -# Restart qubesd. -sudo systemctl restart qubesd.service -``` - -You can now verify that `qubesd` is running: - -``` -sudo systemctl status qubesd.service | cat -``` - -#### Restoring from backup in case of failed installation. - -If `qubesd` has failed for some reason, you can restore -from the backup and restart again. This will nullify -the changes you made in this routine. To restore from -the backup, use the following commands: - -``` -udo cp qubes/vm/mix/net.py.bak qubes/vm/mix/net.py -sudo systemctl restart qubesd.service -``` - -### Install the agent in the TemplateVM - -The second step is to build and install the component that goes -in the TemplateVM that your NetVM (typically `sys-net` uses). -If your NetVM is a StandaloneVM, you should modify these instructions -to install the component directly in the NetVM, as in this case -your NetVM does not instantiation from a Qubes template. - -Within a DisposableVM, build said package as follows: - -``` -# Dependencies -dnf install git rpm-build make coreutils tar gawk findutils systemd systemd-rpm-macros -# Source code -git clone https://github.com/Rudd-O/qubes-network-server -# -cd qubes-network-server -make rpm -``` - -The process will output a `qubes-network-server-*.noarch.rpm` in the -directory where it ran. Copy that RPM to your TemplateVM. Here is -a trick to do that from `dom0`, which fetchs the RPM file from the DisposableVM -and saves it within the TemplateVM, which you can then feed as an argument -to the `rpm -ivh` command: - -``` -qvm-run --pass-io disp8474 'cat /home/user/qubes-network-server/qubes-network-server*.noarch.rpm' | \ - qvm-run --pass-io fedora-30 'cat > qns.rpm' -``` - -You can power off the DisposableVM now. - -Now you can install this package in your TemplateVM (or NetVM, if it is a StandaloneVM): - -``` -# (In TemplateVM) -# rpm -ivh qns.rpm -``` - -Now power off both your TemplateVM and your NetVM. - -Then start your NetVM back up. - -You can verify that the necessary component is running by launching a terminal -in your NetVM, then typing the following: +You're done. You can verify that the necessary component is running by launching +a terminal in your NetVM, then typing the following: ``` systemctl status qubes-routing-manager.service @@ -200,11 +123,51 @@ systemctl status qubes-routing-manager.service The routing manager should show as `enabled` and `active` in the terminal output. +### How to build the packages to install + +You will first build the `qubes-core-admin-addon-network-server` RPM. + +To build this package, you will need to use a `chroot` jail containing +a Fedora installation of the exact same release as your `dom0` (Fedora 25 +for Qubes release 4.0, Fedora 31 for Qubes release 4.1). + +Copy the source of the package to your `chroot`. Then start a shell in +your `chroot`, and type `make rpm`. You may have to install some packages +in your `chroot` -- use `dnf install git rpm-build make coreutils tar gawk findutils systemd systemd-rpm-macros` +to get the minimum dependency set installed. + +Once built, in the source directory you will find the RPM built for the +exact release of Qubes you need. + +Alternatively, you may first create a source RPM using `make srpm` on your +regular workstation, then use `mock` to rebuild the source RPM produced +in the source directory, using a Fedora release compatible with your `dom0`. + +To build the `qubes-network-server` RPM, you can use a DisposableVM running +the same Fedora release as your NetVM. Build said package as follows: + +``` +# Dependencies +dnf install git rpm-build make coreutils tar gawk findutils systemd systemd-rpm-macros +# Source code +cd /path/to/qubes-network-server +# +make rpm +``` + +The process will output a `qubes-network-server-*.noarch.rpm` in the +directory where it ran. Fish it out and save it into the VM where you'll +install it. + +You can power off the DisposableVM now. + ### Upgrading to new / bug-fixing releases -Follow the same procedures as above, but when asked to install the package +Follow the same procedures as above, but when asked to install the packages with `rpm -ivh`, change it to `rpm -Uvh` (uppercase U for upgrade). +Always restart your NetVMs between upgrades. + ## Theory of operation Qubes OS relies on layer 3 (IP) routing. VMs in its networked tree send traffic through diff --git a/qubes-network-server.spec b/qubes-network-server.spec index 76bfc74..6a44fa2 100644 --- a/qubes-network-server.spec +++ b/qubes-network-server.spec @@ -1,5 +1,3 @@ -%{!?python2_sitearch: %define python2_sitearch %(%{__python2} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)")} - %define debug_package %{nil} %define mybuildnumber %{?build_number}%{?!build_number:1} @@ -24,14 +22,26 @@ BuildRequires: systemd-rpm-macros Requires: qubes-core-agent-networking >= 4.0.51-1 Requires: qubes-core-agent-networking < 4.1 -Requires: python2 -Requires: python2-qubesdb +Requires: python3 +Requires: python3-qubesdb %description -This package lets you turn your Qubes OS into a network server. +This package lets you turn your Qubes OS into a network server. Install this +in the TemplateVM of your NetVM. Then install the companion %{name}-dom0 package +in your dom0. Please see README.md enclosed in the package for instructions on how to use this. +%package -n qubes-core-admin-addon-network-server +Summary: dom0 administrative extension for Qubes network server + +Requires: qubes-core-dom0 >= 4.0.49-1 + +%description -n qubes-core-admin-addon-network-server +This package lets you turn your Qubes OS into a network server. Install this +in your dom0. Then install the companion qubes-network-server package in the +TemplateVM of your NetVM. + %prep %setup -q @@ -52,6 +62,10 @@ echo 'enable qubes-routing-manager.service' > "$RPM_BUILD_ROOT"/%{_presetdir}/75 %config %attr(0644, root, root) %{_unitdir}/qubes-routing-manager.service %doc README.md TODO +%files -n qubes-core-admin-addon-network-server +%attr(0644, root, root) %{python3_sitelib}/qubesnetworkserver +%{python3_sitelib}/qubesnetworkserver-*.egg-info + %post %systemd_post qubes-routing-manager.service @@ -61,6 +75,12 @@ echo 'enable qubes-routing-manager.service' > "$RPM_BUILD_ROOT"/%{_presetdir}/75 %postun %systemd_postun_with_restart qubes-routing-manager.service +%post -n qubes-core-admin-addon-network-server +%systemd_post qubesd.service + +%postun -n qubes-core-admin-addon-network-server +%systemd_postun_with_restart qubesd.service + %changelog * Mon Apr 13 2020 Manuel Amador (Rudd-O) - Update to Qubes 4.0 diff --git a/qubesnetworkserver/__init__.py b/qubesnetworkserver/__init__.py new file mode 100644 index 0000000..72540f1 --- /dev/null +++ b/qubesnetworkserver/__init__.py @@ -0,0 +1,98 @@ +import qubes.ext +import qubes.vm.templatevm + + +class QubesNetworkServerExtension(qubes.ext.Extension): + + def shutdown_routing_for_vm(self, netvm, appvm): + self.reload_routing_for_vm(netvm, appvm, True) + + def reload_routing_for_vm(self, netvm, appvm, shutdown=False): + '''Reload the routing method for the VM.''' + if not netvm.is_running(): + return + for addr_family in (4, 6): + ip = appvm.ip6 if addr_family == 6 else appvm.ip + if ip is None: + continue + # report routing method + self.setup_forwarding_for_vm(netvm, appvm, ip, remove=shutdown) + + def setup_forwarding_for_vm(self, netvm, appvm, ip, remove=False): + ''' + Record in Qubes DB that the passed VM may be meant to have traffic + forwarded to and from it, rather than masqueraded from it and blocked + to it. + + The relevant incantation on the command line to assign the forwarding + behavior is `qvm-features routing-method forward`. If the feature + is set on the TemplateVM upon which the VM is based, then that counts + as the forwarding method for the VM as well. + + The counterpart code in qubes-firewall handles setting up the NetVM + with the proper networking configuration to permit forwarding without + masquerading behavior. + + If `remove` is True, then we remove the respective routing method from + the Qubes DB instead. + ''' + if ip is None: + return + routing_method = appvm.features.check_with_template( + 'routing-method', 'masquerade' + ) + base_file = '/qubes-routing-method/{}'.format(ip) + if remove: + netvm.untrusted_qdb.rm(base_file) + elif routing_method == 'forward': + netvm.untrusted_qdb.write(base_file, 'forward') + else: + netvm.untrusted_qdb.write(base_file, 'masquerade') + + @qubes.ext.handler( + 'domain-feature-set:routing-method', + 'domain-feature-delete:routing-method', + ) + def on_routing_method_changed( + self, + vm, + ignored_feature, + **kwargs + ): + # pylint: disable=no-self-use,unused-argument + if 'oldvalue' not in kwargs or kwargs.get('oldvalue') != kwargs.get('value'): + if vm.netvm: + self.reload_routing_for_vm(vm.netvm, vm) + + @qubes.ext.handler('domain-qdb-create') + def on_domain_qdb_create(self, vm, event, **kwargs): + ''' Fills the QubesDB with firewall entries. ''' + # pylint: disable=unused-argument + if vm.netvm: + self.reload_routing_for_vm(vm.netvm, vm) + + @qubes.ext.handler('domain-start') + def on_domain_started(self, vm, event, **kwargs): + # pylint: disable=unused-argument + try: + for downstream_vm in vm.connected_vms: + self.reload_routing_for_vm(vm, downstream_vm) + except AttributeError: + pass + + @qubes.ext.handler('domain-shutdown') + def on_domain_shutdown(self, vm, event, **kwargs): + # pylint: disable=unused-argument + try: + for downstream_vm in self.connected_vms: + self.shutdown_routing_for_vm(vm, downstream_vm) + except AttributeError: + pass + if vm.netvm: + self.shutdown_routing_for_vm(vm.netvm, vm) + + @qubes.ext.handler('net-domain-connect') + def on_net_domain_connect(self, vm, event): + # pylint: disable=unused-argument + if vm.netvm: + self.reload_routing_for_vm(vm.netvm, vm) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..416d33a --- /dev/null +++ b/setup.py @@ -0,0 +1,24 @@ +import os +import setuptools + +if __name__ == '__main__': + version = open(os.path.join(os.path.dirname(__file__), 'qubes-network-server.spec')).read().strip() + version = [v for v in version.splitlines() if v.startswith("Version:")][0] + version = version.split()[-1] + setuptools.setup( + name='qubesnetworkserver', + version=version, + author='Manuel Amador (Rudd-O)', + author_email='rudd-o@rudd-o.com', + description='Qubes network server dom0 component', + license='GPL2+', + url='https://github.com/Rudd-O/qubes-network-server', + + packages=('qubesnetworkserver',), + + entry_points={ + 'qubes.ext': [ + 'qubesnetworkserver = qubesnetworkserver:QubesNetworkServerExtension', + ], + } + ) diff --git a/src/qubes-network-server-dom0.patch b/src/qubes-network-server-dom0.patch deleted file mode 100644 index a0dabe8..0000000 --- a/src/qubes-network-server-dom0.patch +++ /dev/null @@ -1,122 +0,0 @@ -diff --git a/qubes/vm/mix/net.py b/qubes/vm/mix/net.py -index 129bc107..fac6ec52 100644 ---- a/qubes/vm/mix/net.py -+++ b/qubes/vm/mix/net.py -@@ -262,6 +262,8 @@ class NetVMMixin(qubes.events.Emitter): - This will allow re-reconnecting them cleanly later. - ''' - # pylint: disable=unused-argument -+ if self.netvm: -+ self.netvm.shutdown_routing_for_vm(self) - for vm in self.connected_vms: - if not vm.is_running(): - continue -@@ -271,6 +273,19 @@ class NetVMMixin(qubes.events.Emitter): - # ignore errors - pass - -+ @qubes.ext.handler( -+ 'domain-feature-set:routing-method', -+ 'domain-feature-delete:routing-method', -+ ) -+ def on_routing_method_changed( -+ self, -+ event, feature, -+ value=None, oldvalue=None -+ ): -+ # pylint: disable=no-self-use,unused-argument -+ if self.netvm: -+ self.netvm.reload_routing_for_vm(self) -+ - @qubes.events.handler('domain-start') - def on_domain_started(self, event, **kwargs): - '''Connect this domain to its downstream domains. Also reload firewall -@@ -281,6 +296,7 @@ class NetVMMixin(qubes.events.Emitter): - - if self.netvm: - self.netvm.reload_firewall_for_vm(self) # pylint: disable=no-member -+ self.netvm.reload_routing_for_vm(self) # pylint: disable=no-member - - for vm in self.connected_vms: - if not vm.is_running(): -@@ -350,6 +366,20 @@ class NetVMMixin(qubes.events.Emitter): - - return self.netvm is not None - -+ def shutdown_routing_for_vm(self, vm): -+ self.reload_routing_for_vm(vm, True) -+ -+ def reload_routing_for_vm(self, vm, shutdown=False): -+ '''Reload the routing method for the VM.''' -+ if not self.is_running(): -+ return -+ for addr_family in (4, 6): -+ ip = vm.ip6 if addr_family == 6 else vm.ip -+ if ip is None: -+ continue -+ # report routing method -+ self.setup_non_masquerade_forwarding_for_vm(vm, ip, remove=shutdown) -+ - def reload_firewall_for_vm(self, vm): - ''' Reload the firewall rules for the vm ''' - if not self.is_running(): -@@ -370,6 +400,35 @@ class NetVMMixin(qubes.events.Emitter): - # signal its done - self.untrusted_qdb.write(base_dir[:-1], '') - -+ def setup_non_masquerade_forwarding_for_vm(self, vm, ip, remove=False): -+ ''' -+ Record in Qubes DB that the passed VM may be meant to have traffic -+ forwarded to and from it, rather than masqueraded from it and blocked -+ to it. -+ -+ The relevant incantation on the command line to assign the forwarding -+ behavior is `qvm-features routing-method forward`. If the feature -+ is set on the TemplateVM upon which the VM is based, then that counts -+ as the forwarding method for the VM as well. -+ -+ The counterpart code in qubes-firewall handles setting up the NetVM -+ with the proper networking configuration to permit forwarding without -+ masquerading behavior. -+ -+ If `remove` is True, then we remove the respective routing method from -+ the Qubes DB instead. -+ ''' -+ if ip is None: -+ return -+ routing_method = vm.features.check_with_template('routing-method', 'masquerade') -+ base_file = '/qubes-routing-method/{}'.format(ip) -+ if remove: -+ self.untrusted_qdb.rm(base_file) -+ elif routing_method == 'forward': -+ self.untrusted_qdb.write(base_file, 'forward') -+ else: -+ self.untrusted_qdb.write(base_file, 'masquerade') -+ - def set_mapped_ip_info_for_vm(self, vm): - ''' - Set configuration to possibly hide real IP from the VM. -@@ -451,6 +510,7 @@ class NetVMMixin(qubes.events.Emitter): - ''' Reloads the firewall config for vm ''' - # pylint: disable=unused-argument - self.reload_firewall_for_vm(vm) -+ self.reload_routing_for_vm(vm) - - @qubes.events.handler('domain-qdb-create') - def on_domain_qdb_create(self, event): -@@ -461,6 +521,7 @@ class NetVMMixin(qubes.events.Emitter): - # keep in sync with on_firewall_changed - self.set_mapped_ip_info_for_vm(vm) - self.reload_firewall_for_vm(vm) -+ self.reload_routing_for_vm(vm) - - @qubes.events.handler('firewall-changed', 'domain-spawn') - def on_firewall_changed(self, event, **kwargs): -@@ -469,6 +530,7 @@ class NetVMMixin(qubes.events.Emitter): - if self.is_running() and self.netvm: - self.netvm.set_mapped_ip_info_for_vm(self) - self.netvm.reload_firewall_for_vm(self) # pylint: disable=no-member -+ self.netvm.reload_routing_for_vm(self) # pylint: disable=no-member - - # CORE2: swallowed get_firewall_conf, write_firewall_conf, - # get_firewall_defaults diff --git a/src/qubes-routing-manager b/src/qubes-routing-manager index 79e31f3..26e0701 100755 --- a/src/qubes-routing-manager +++ b/src/qubes-routing-manager @@ -1,4 +1,4 @@ -#!/usr/bin/python2 +#!/usr/bin/python3 ''' This program reads the /qubes-firewall/{ip}/qubes-routing-method file @@ -42,7 +42,7 @@ class AdjunctWorker(object): def run_ipt(*args): return subprocess.check_call([cmd, '-w'] + list(args)) - out = subprocess.check_output([cmd + '-save']).splitlines() + out = subprocess.check_output([cmd + '-save', '-w'], universal_newlines=True).splitlines() if enable: # Create necessary prerouting chain.