Compare commits

...

38 Commits

Author SHA1 Message Date
Manuel Amador (Rudd-O)
658e6dfa08 Fix scriptlet to reenable unit when obsolete wantedbys are removed. 2025-02-27 00:32:43 +00:00
Manuel Amador (Rudd-O)
86fa5c509c Fix scriptlet to remove obsolete starts. 2025-02-26 23:57:29 +00:00
Manuel Amador (Rudd-O)
15edce34a8 Actually bind to qubes-network.service which starts only when network needs to be routed. 2025-02-26 23:43:45 +00:00
Manuel Amador (Rudd-O)
3a214bdfe1 Merge remote-tracking branch 'origin/master' 2024-02-29 03:53:31 +00:00
Manuel Amador (Rudd-O)
f7bfd46bdc Ensure the forward rule is added after connection tracking.
Also improve tests and add Tox for mypy and pytest.
2024-02-29 03:24:36 +00:00
Manuel Amador (Rudd-O)
26b0b2a357 New build strategies. 2024-02-22 17:57:50 +00:00
Manuel Amador (Rudd-O)
7289f59867 Use QUBES_RELEASES. 2024-02-21 22:18:38 +00:00
Rudd-O
2bc9d929f9
Link to upgrade instructions. 2024-02-20 17:04:10 +00:00
Rudd-O
022414a4f8
Instructions to upgrade to Qubes OS 4.2 2024-02-20 17:03:58 +00:00
Manuel Amador (Rudd-O)
ff3d6b55f2 Ensure conflicts so it is not attempted to upgrade with the wrong base dep. 2024-02-20 14:44:24 +00:00
Manuel Amador (Rudd-O)
ef4845548f Insert custom postrouting chain before masquerade. 2024-02-20 08:57:43 +00:00
Manuel Amador (Rudd-O)
7b5cae5b0e Neuter masquerading of outbound traffic from VMs in routing mode. 2024-02-06 03:54:35 +00:00
Manuel Amador (Rudd-O)
e9e65f7da1 Improve documentation some more. 2024-02-06 03:54:08 +00:00
Manuel Amador (Rudd-O)
da69c75642 Improve user documentation. 2024-02-06 03:10:10 +00:00
Manuel Amador (Rudd-O)
3f58f6bda6 Do not need subprocess here. 2024-02-06 03:03:02 +00:00
Manuel Amador (Rudd-O)
e5534a5225 Better instructions to build packages. 2024-02-06 02:54:35 +00:00
Manuel Amador (Rudd-O)
cf2945e742 Qubes 4.2 support. 2024-02-06 02:45:35 +00:00
Manuel Amador (Rudd-O)
06c5b1b0ae F37 2023-02-24 10:24:18 +00:00
Manuel Amador (Rudd-O)
e7c4a2115a Tag 0.0.19. 2022-10-26 16:58:07 +00:00
Manuel Amador (Rudd-O)
a1288b8466 Python interpreter fixed at 3 in spec file. 2022-10-26 16:52:27 +00:00
Manuel Amador (Rudd-O)
a972656553 Blacken. 2022-10-26 16:49:15 +00:00
Manuel Amador (Rudd-O)
0e75677fab F36 2022-10-26 16:49:15 +00:00
Manuel Amador (Rudd-O)
f34c1a6574 Disable testing, . 2022-10-26 16:49:15 +00:00
Manuel Amador (Rudd-O)
5fbf15dc4b Disable testing. 2022-10-26 16:49:15 +00:00
Rudd-O
f0c6387fbf
Merge pull request #15 from gunkaaa/patch-1
Fix broken link and image embeds
2022-10-13 23:15:08 +00:00
gunkaaa
df694b870a
Update README.md
Fix images with spaces in filenames not displaying on github
2022-10-14 02:27:06 +13:00
gunkaaa
2b7be939fe
Fix broken link to "enable remote management..." 2022-10-14 02:23:19 +13:00
Manuel Amador (Rudd-O)
60b952171e Doc updates. 2021-10-29 00:44:49 +00:00
Manuel Amador (Rudd-O)
581a913999 Tag 0.0.18. 2021-10-29 00:43:28 +00:00
Manuel Amador (Rudd-O)
21c09202cc Tag 0.0.17. 2021-10-29 00:24:42 +00:00
Manuel Amador (Rudd-O)
4e6c87fb36 Merge branch 'master' into r4.1 2021-10-28 23:08:28 +00:00
Manuel Amador (Rudd-O)
b201701442 Documentation buffups. 2021-06-16 10:43:17 +00:00
Manuel Amador (Rudd-O)
aa664192d6 Build.parameters updated. 2021-05-18 13:10:25 +00:00
Manuel Amador (Rudd-O)
4b6100efce Add build.parameters. 2021-03-30 01:54:55 +00:00
Manuel Amador (Rudd-O)
498c3c4105 Tag 0.0.16. 2021-03-29 17:04:53 +00:00
Manuel Amador (Rudd-O)
524f2f4341 Merge branch 'master' into r4.1 2020-12-01 16:29:52 +00:00
Manuel Amador (Rudd-O)
605f5f4c73 Update to Python 3. 2020-04-14 01:47:04 +00:00
Manuel Amador (Rudd-O)
ddf5bd36b8 Add 4.1 release patch. 2020-04-14 01:41:42 +00:00
22 changed files with 5392 additions and 401 deletions

2
.gitignore vendored
View File

@ -9,3 +9,5 @@ pkgs/
build
*.egg-info
src/*.service
.mypy_cache
.tox

3
Jenkinsfile vendored
View File

@ -1,4 +1,5 @@
// https://github.com/Rudd-O/shared-jenkins-libraries
@Library('shared-jenkins-libraries@master') _
genericFedoraRPMPipeline()
genericFedoraRPMPipeline(null, null, null, null, TestStrategySkipTests())

View File

@ -11,12 +11,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-template install-dom0
.PHONY: clean dist rpm srpm install-template install-dom0 test
clean:
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
cd $(ROOT_DIR) || exit $$? ; rm -rf *.egg-info build .mypy_cache
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 ; }
@ -32,12 +32,16 @@ rpm: dist
cd $(ROOT_DIR) ; mv -f builddir.rpm/*/* . && rm -rf builddir.rpm
install-template: all
PYTHONDONTWRITEBYTECODE=1 python3 routingmanagersetup.py install $(PYTHON_PREFIX_ARG) -O0 --root $(DESTDIR)
install -Dm 755 src/qubes-routing-manager -t $(DESTDIR)/$(SBINDIR)/
sed -i "s,^#!.*,#!$(PYTHON)," $(DESTDIR)/$(SBINDIR)/qubes-routing-manager
install -Dm 644 src/qubes-routing-manager.service -t $(DESTDIR)/$(UNITDIR)/
# Python 3 is always used for Qubes admin package.
install-dom0:
PYTHONDONTWRITEBYTECODE=1 python3 setup.py install $(PYTHON_PREFIX_ARG) -O0 --root $(DESTDIR)
PYTHONDONTWRITEBYTECODE=1 python3 networkserversetup.py install $(PYTHON_PREFIX_ARG) -O0 --root $(DESTDIR)
install: install-dom0 install-template
test:
tox --current-env

137
README.md
View File

@ -1,22 +1,23 @@
# Qubes network server
This software lets you turn your [Qubes OS 4.0](https://www.qubes-os.org/) machine into
This software lets you turn your [Qubes OS 4.2](https://www.qubes-os.org/) machine into
a network server, enjoying all the benefits of Qubes OS (isolation, secure
inter-VM process communication, ease of use) with none of the drawbacks
of setting up your own Xen server.
**Note**: this software only supports release 4.0 of Qubes OS. For Qubes OS release 3.2 support,
please see `release-3.2` branch. For Qubes OS release 4.1 support, please see `r4.1 branch`.
This release is only intended for use with Qubes OS 4.2. Older Qubes OS releases
will not support it. For Qubes OS 4.1, check branch `r4.1`.
**Important note about upgrades**: when you upgrade your system from Qubes OS 4.1 to
Qubes OS 4.2, if you have this package installed in your template, the template will
likely **fail to update**. Please consult [our upgrade instructions](doc/distupgrade.md)
for information on how to proceed.
## Why?
Qubes OS is a magnificent operating system, but there are so many use cases that its networking
model cannot crack:
Qubes OS is a magnificent operating system. That said, there are many use cases its networking
model does not work well for:
* As an automated integration testing system. Qubes OS would be
phenomenal for this, and its automation tools would make it
extremely easy to bring up and tear down entire environments.
If only those environments could network with each other securely!
* Remote management of Qubes OS instances. Vanilla Qubes OS cannot
easily be managed remotely. A better networking model would allow
for orchestration tools — such as
@ -25,8 +26,12 @@ model cannot crack:
within each VM.
* Anything that involves a secure server, serving data to people or
machines, simply cannot be done under vanilla Qubes OS.
* As an automated integration testing system. Qubes OS would be
phenomenal for this, and its automation tools would make it
extremely easy to bring up and tear down entire environments.
If only those environments could network with each other securely!
## Enhanced networking model
### The traditional Qubes networking model
The traditional Qubes OS networking model contemplates a client-only
use case. User VMs (AppVMs or StandaloneVMs) are attached to ProxyVMs,
@ -34,26 +39,32 @@ which give the user control over outbound connections taking place from
user VMs. ProxyVMs in turn attach to NetVMs, which provide outbound
connectivity for ProxyVMs and other user VMs alike.
![Standard Qubes OS network model](./doc/Standard Qubes OS network model.png)
![Standard Qubes OS network model](./doc/Standard%20Qubes%20OS%20network%20model.png)
No provision is made for running a server in a virtualized environment,
such that the server's ports are accessible by (a) other VMs (b) machines
beyond the perimeter of the NetVM. To the extent that such a thing is
possible, it is only possible by painstakingly maintaining firewall rules
for multiple VMs, which need to carefully override the existing firewall
rules, and require careful thought not to open the system to unexpected
attack vectors. The Qubes OS user interface provides no help either.
beyond the perimeter of the NetVM. By default, firewall rules in NetVMs
prevent traffic from reaching any VM attached to them. Furthermore, even
with custom, permissive firewall rules, the IP addresses of VMs attached
to any NetVM are not visible "on the other side of the NetVM", so firewall
rules can only help with something like DNAT. Finally, such custom firewalls
require careful thought not to open the system to unexpected attack vectors.
The Qubes OS user interface provides no means to set this up either.
Qubes network server changes all that.
### The Qubes network server networking model
![Qubes network server model](./doc/Qubes network server model.png)
Qubes network server builds on the Qubes security model and enhances it
to optionally permit traffic to user VMs.
![Qubes network server model](./doc/Qubes%20network%20server%20model.png)
With the Qubes network server software, it becomes possible to make
network servers in user VMs available to other machines, be them
peer VMs in the same Qubes OS system or machines connected to
a physical link shared by a NetVM. Those network server VMs also
obey the Qubes OS outbound firewall rules controls, letting you run
services with outbound connections restricted.
services with outbound connections restricted using the standard Qubes OS
firewall system.
This is all, of course, opt-in, so the standard Qubes OS network security
model remains in effect until you decide to enable the feature on any
@ -66,44 +77,62 @@ to machines on the same network as the NetVM.
## How to use this software
Once installed (see below), usage of the software is straightforward.
Once installed (**see below for installation instructions**), usage of
the software is straightforward.
These sample instructions assume you already have an AppVM VM set up,
named `testvm`, and that your `sys-net` VM is attached to a network with
subnet `192.168.16.0/24`.
These sample instructions assume:
First, attach the VM you want to expose to the network
* the software is properly installed (see below),
* you understand the distinction between dom0 and qubes,
* you already have an AppVM VM set up, named `testvm`, and
* your `sys-net` VM is attached to a network with subnet `192.168.16.0/24`
— this, of course, may vary depending on your local router configuration.
*Do not proceed any further if you do not yet meet these requirements.*
First, **attach** the VM you want to expose to the network
to a NetVM that has an active network connection:
`qvm-prefs -s testvm netvm sys-net`
Then, set an IP address on the VM:
Set an **IP** address on `testvm` belonging to the same LAN as `sys-net`:
`qvm-prefs -s testvm ip 192.168.16.25`
(The step above requires you restart the `testvm` VM if it was running.)
**Restart** the `testvm` VM if it was already running.
Then, to enable the network server feature for your `testvm` VM, all you have
to do in your AdminVM (`dom0`) is run the following command:
**Configure** routing method; to enable the network server feature for
your `testvm` VM, all you have to do in your AdminVM (`dom0`) is run
the following command:
`qvm-features testvm routing-method forward`
Now `testvm` is exposed to the network with address `192.168.16.25`, as well
as to other VMs attached to `NetVM`.
Do note that `testvm` will have the standard Qubes OS firewall rules stopping
inbound traffic. To solve that issue, you can
[use the standard `rc.local` Qubes OS mechanism to alter the firewall rules](https://www.qubes-os.org/doc/firewall/#where-to-put-firewall-rules)
in your `testvm` AppVM.
Finally, adjust **input firewall rules** on `testvm` to permit traffic coming from
machines in your LAN. `testvm` will have the standard Qubes OS firewall
rules stopping inbound traffic. To solve that issue, you can use a sample
rule in `testvm`:
```
sudo nft add rule qubes custom-input ip saddr 192.168.16.0/24 ct state new,established,related counter accept
```
You can make these rules persistent by [following instructions on the Qubes
OS firewall documentation page](https://www.qubes-os.org/doc/firewall/#enabling-networking-between-two-qubes).
Note that you do not need to change the `custom-forward` chain at all
on any qube -- Qubes network server manages that for you transparently
in your `NetVM`.
Here are documents that will help you take advantage of Qubes network server:
* [Setting up your first server](doc/Setting up your first server.md)
* [Setting up an SSH server](doc/Setting up an SSH server.md)
## Installation
## Setup
Installation consists of two steps:
Package installation consists of two steps (**the package creation instructions are below**):
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
@ -122,30 +151,38 @@ a terminal in your NetVM, then typing the following:
systemctl status qubes-routing-manager.service
```
The routing manager should show as `enabled` and `active` in the terminal output.
The routing manager should show as `enabled` and `active` in the terminal
output, with no errors. You can now follow the usage instructions above.
### 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).
a Fedora installation of the exact same release as your `dom0` (Fedora 37
for Qubes 4.2). You can do this using `toolbox` (for maximum safety
within a disposable qube):
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.
```
dnf install -y toolbox
toolbox create -r 37
toolbox enter fedora-toolbox-37
# Bam! You have a shell in an isolated Fedora 37 instance now.
```
Once built, in the source directory you will find the RPM built for the
exact release of Qubes you need.
Within the toolbox, all your normal files from your home directory are
visible. Change into the directory that contains this source code,
then type `make rpm`. You may have to install some packages within your
toolbox -- use `dnf install git rpm-build make coreutils tar gawk findutils systemd systemd-rpm-macros`
to get the minimum dependency set installed. Don't worry -- nothing within
the toolbox affects
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`.
Once built, in the source directory you will find the
`qubes-core-admin-addon-network-server` RPM built for 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:
To build the `qubes-network-server` RPM that goes in your template, you
can simply use a DisposableVM running the same Fedora release as your NetVM.
Build said package as follows:
```
# Dependencies
@ -157,8 +194,8 @@ 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.
directory where it ran. Fish it out and copy it into the template where
you'll install it.
You can power off the DisposableVM now.

1
build.parameters Normal file
View File

@ -0,0 +1 @@
["QUBES_RELEASES": "4.2"]

View File

@ -100,4 +100,4 @@ to your Qubes OS server. You are also able to run commands on the
`exp-ssh` VM, directly from your manager machine.
Should you want to run commands on *other* VMs of your Qubes OS server,
then learn how to [enable remote management of your Qubes network server](https://github.com/Rudd-O/ansible-qubes/tree/master/doc/Remote management of Qubes OS servers.md).
then learn how to [enable remote management of your Qubes network server](https://github.com/Rudd-O/ansible-qubes/tree/master/doc/Remote%20management%20of%20Qubes%20OS%20servers.md).

View File

@ -5,13 +5,14 @@ is meant to be a standalone VM that contains files, being served by
a running HTTP server (port 80) within it. This VM is attached to a
NetVM `sys-net`, with IP address `192.168.1.4` on a local network
`192.168.1.0/24`. Our goal will be to make `httpserver` accessible
to your laptop on the same physical network, which we'll assume has
IP address `192.168.1.8`.
to your client laptop on the same physical network, which we'll
assume has IP address `192.168.1.8`.
##Assign a static address
First step is to assign an address — let's make it `192.168.1.6`
to `httpserver`:
to `httpserver` (of course, you should make sure that this IP
address isn't used by any other equipment in your network):
```
qvm-prefs -s httpserver ip 192.168.1.6

64
doc/distupgrade.md Normal file
View File

@ -0,0 +1,64 @@
# How to upgrade a Qubes network server from Qubes OS 4.1 to Qubes OS 4.2
The [standard instructions to upgrade Qubes OS systems](https://www.qubes-os.org/doc/upgrade/4.2/)
will fail to work. The instructions tell you to run something to the effect of:
```
qubes-dist-upgrade --all-pre-reboot <other flags>
```
then reboot, then run:
```
qubes-dist-upgrade --all-post-reboot <other flags>
```
The pre-reboot phase will fail if run without the following precautions.
## Step by step instructions
First, build a `qubes-network-server` RPM with the instructions provided
by this package's [README.md](../README.md) file. Then, for each template
where `qubes-network-server` is installed, deposit your build of the
`qubes-network-server` RPM in a folder `/root/update` of the template,
and run the command `createrepo_c /root/update` (you may have to install
package `createrepo_c` via `dnf` to run it).
Now build a `qubes-core-admin-addon-network-server` package for your dom0,
then copy the file to your profile directory into dom0. Remember this
package has to be built *in the same Fedora release (37)* as the Qubes OS
4.2 dom0 (the `toolbox` command in a disposable qube is handy for this!).
Now open the file `/etc/dnf/dnf.conf` on every template qube where you
did the above, then add an `exclude=qubes-network-server` setting under
its `[main]` section.
Remove the currently-installed `qubes-core-admin-addon-network-server`
package from your dom0 (using `dnf remove`).
Run the pre-reboot phase.
Install the recently-built `qubes-core-admin-addon-network-server` package
into dom0 (using `dnf install` with the path to the RPM file).
Reboot.
Before running the post-reboot phase, remove the setting you added to the
`dnf.conf` file of each template you modified. Finally, add the file
`/etc/yum.repos.d/local.repo` with the following contents:
```
[local]
name=Local packages
baseurl=file:///root/update
enabled=1
gpgcheck=0
metadata_expire=15
```
Now run the post-reboot phase. The template upgrade should succeed now.
To finalize, delete folder `/root/update` and file `/etc/yum.repos.d/local.repo`
from every template that has it.
You are now updated to Qubes OS 4.2 and `qubes-network-server` is ready.

26
networkserversetup.py Normal file
View File

@ -0,0 +1,26 @@
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",
],
},
)

View File

@ -3,7 +3,7 @@
%define mybuildnumber %{?build_number}%{?!build_number:1}
Name: qubes-network-server
Version: 0.0.15
Version: 0.1.6
Release: %{mybuildnumber}%{?dist}
Summary: Turn your Qubes OS into a network server
BuildArch: noarch
@ -16,27 +16,18 @@ BuildRequires: make
BuildRequires: coreutils
BuildRequires: tar
BuildRequires: findutils
%if 0%{?fedora} < 31
BuildRequires: python2
BuildRequires: python2-rpm-macros
%global pythoninterp %{_bindir}/python2
%else
BuildRequires: python3
BuildRequires: python3-rpm-macros
%global pythoninterp %{_bindir}/python3
%endif
%if 0%{?fedora} > 29
BuildRequires: systemd-rpm-macros
%else
%global _presetdir %{_prefix}/lib/systemd/system-preset
%global _unitdir %{_prefix}/lib/systemd/system
%endif
BuildRequires: python3-tox-current-env
BuildRequires: python3-mypy
BuildRequires: python3-pytest
Requires: qubes-core-agent-networking >= 4.0.51-1
Conflicts: qubes-core-agent-networking >= 4.1
Requires: python2
Requires: python2-qubesdb
Requires: qubes-core-agent-networking >= 4.2
Conflicts: qubes-core-agent < 4.2
Requires: python3
Requires: python3-qubesdb
Requires: nftables
%description
This package lets you turn your Qubes OS into a network server. Install this
@ -58,8 +49,8 @@ BuildRequires: python3-rpm-macros
BuildRequires: python3-setuptools
Requires: python3
Requires: qubes-core-dom0 >= 4.0.49-1
Conflicts: qubes-core-dom0 >= 4.1
Requires: qubes-core-dom0 >= 4.2
Conflicts: qubes-core-dom0 < 4.2
%description -n qubes-core-admin-addon-network-server
This package lets you turn your Qubes OS into a network server. Install this
@ -74,27 +65,52 @@ this software.
%build
# variables must be kept in sync with install
make DESTDIR=$RPM_BUILD_ROOT SBINDIR=%{_sbindir} UNITDIR=%{_unitdir} PYTHON=%{pythoninterp}
make DESTDIR=$RPM_BUILD_ROOT SBINDIR=%{_sbindir} UNITDIR=%{_unitdir} PYTHON=%{__python3}
%install
rm -rf $RPM_BUILD_ROOT
# variables must be kept in sync with build
make install DESTDIR=$RPM_BUILD_ROOT SBINDIR=%{_sbindir} UNITDIR=%{_unitdir} PYTHON=%{pythoninterp}
make install DESTDIR=$RPM_BUILD_ROOT SBINDIR=%{_sbindir} UNITDIR=%{_unitdir} PYTHON=%{__python3}
mkdir -p "$RPM_BUILD_ROOT"/%{_presetdir}
echo 'enable qubes-routing-manager.service' > "$RPM_BUILD_ROOT"/%{_presetdir}/75-%{name}.preset
%check
tox --current-env
%files
%attr(0755, root, root) %{_sbindir}/qubes-routing-manager
%attr(0644, root, root) %{python3_sitelib}/qubesroutingmanager/*
%{python3_sitelib}/qubesroutingmanager-*.egg-info
%attr(0644, root, root) %{_presetdir}/75-%{name}.preset
%config %attr(0644, root, root) %{_unitdir}/qubes-routing-manager.service
%doc README.md TODO
%files -n qubes-core-admin-addon-network-server
%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
%systemd_post qubes-routing-manager.service
%posttrans
# Remove old unit enablement paths.
reenable=0
if [ -h %{_sysconfdir}/systemd/system/multi-user.target.wants/qubes-routing-manager.service ]
then
reenable=1
rm -f %{_sysconfdir}/systemd/system/multi-user.target.wants/qubes-routing-manager.service
fi
if [ -h %{_sysconfdir}/systemd/system/qubes-iptables.service.wants/qubes-routing-manager.service ]
then
reenable=1
rm -f %{_sysconfdir}/systemd/system/qubes-iptables.service.wants/qubes-routing-manager.service
fi
if [ $reenable = 1 ]
then
mkdir -p %{_sysconfdir}/systemd/system/qubes-network.service.wants
ln -sf %{_unitdir}/qubes-routing-manager.service %{_sysconfdir}/systemd/system/qubes-network.service.wants/qubes-routing-manager.service
fi
exit 0
%preun
%systemd_preun qubes-routing-manager.service
@ -102,19 +118,11 @@ 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
%if 0%{?fedora} > 29
%post -n qubes-core-admin-addon-network-server
%systemd_post qubesd.service
%else
systemctl try-restart qubesd.service
%endif
%postun -n qubes-core-admin-addon-network-server
%if 0%{?fedora} > 29
%postun -n qubes-core-admin-addon-network-server
%systemd_postun_with_restart qubesd.service
%else
systemctl try-restart qubesd.service
%endif
%changelog
* Mon Apr 13 2020 Manuel Amador (Rudd-O) <rudd-o@rudd-o.com>

View File

@ -1,14 +1,27 @@
import qubes.ext
import qubes.vm.templatevm
def l(text, *parms):
return # This exists only to debug.
if parms:
text = text % parms
import sys
print("nsext:", text, file=sys.stderr)
sys.stderr.flush()
l("loaded")
class QubesNetworkServerExtension(qubes.ext.Extension):
def shutdown_routing_for_vm(self, netvm, appvm):
l("shutdown routing for vm %s %s", 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.'''
"""Reload the routing method for the VM."""
l("reload routing for vm %s %s shutdown %s", netvm, appvm, shutdown)
if not netvm.is_running():
return
for addr_family in (4, 6):
@ -19,7 +32,7 @@ class QubesNetworkServerExtension(qubes.ext.Extension):
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.
@ -35,54 +48,54 @@ class QubesNetworkServerExtension(qubes.ext.Extension):
If `remove` is True, then we remove the respective routing method from
the Qubes DB instead.
'''
"""
l("setup forwarding for vm vm %s %s %s remove %s", netvm, appvm, ip, remove)
if ip is None:
return
routing_method = appvm.features.check_with_template(
'routing-method', 'masquerade'
"routing-method", "masquerade"
)
base_file = '/qubes-routing-method/{}'.format(ip)
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')
elif routing_method == "forward":
netvm.untrusted_qdb.write(base_file, "forward")
else:
netvm.untrusted_qdb.write(base_file, 'masquerade')
netvm.untrusted_qdb.write(base_file, "masquerade")
@qubes.ext.handler(
'domain-feature-set:routing-method',
'domain-feature-delete:routing-method',
"domain-feature-set:routing-method",
"domain-feature-delete:routing-method",
)
def on_routing_method_changed(
self,
vm,
ignored_feature,
**kwargs
):
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'):
l("routing method changed %s", vm)
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')
@qubes.ext.handler("domain-qdb-create")
def on_domain_qdb_create(self, vm, event, **kwargs):
''' Fills the QubesDB with firewall entries. '''
"""Fills the QubesDB with firewall entries."""
# pylint: disable=unused-argument
l("domain create %s %s", vm, event)
if vm.netvm:
self.reload_routing_for_vm(vm.netvm, vm)
@qubes.ext.handler('domain-start')
@qubes.ext.handler("domain-start")
def on_domain_started(self, vm, event, **kwargs):
# pylint: disable=unused-argument
l("domain started %s %s", vm, event)
try:
for downstream_vm in vm.connected_vms:
self.reload_routing_for_vm(vm, downstream_vm)
except AttributeError:
pass
@qubes.ext.handler('domain-shutdown')
@qubes.ext.handler("domain-shutdown")
def on_domain_shutdown(self, vm, event, **kwargs):
# pylint: disable=unused-argument
# pylint: disable=unused-argument
l("domain shutdown %s %s", vm, event)
try:
for downstream_vm in self.connected_vms:
self.shutdown_routing_for_vm(vm, downstream_vm)
@ -90,9 +103,10 @@ class QubesNetworkServerExtension(qubes.ext.Extension):
pass
if vm.netvm:
self.shutdown_routing_for_vm(vm.netvm, vm)
@qubes.ext.handler('net-domain-connect')
@qubes.ext.handler("net-domain-connect")
def on_net_domain_connect(self, vm, event):
# pylint: disable=unused-argument
l("domain connect %s %s", vm, event)
if vm.netvm:
self.reload_routing_for_vm(vm.netvm, vm)

View File

@ -0,0 +1,379 @@
#!/usr/bin/python3
import json
import logging
import subprocess
from typing import TypedDict, Any, cast, Literal, Union
ADDRESS_FAMILIES = Union[Literal["ip"], Literal["ip6"]]
class Chain(TypedDict):
name: str
family: str
table: str
handle: int
type: str
hook: str
prio: int
policy: str
class Table(TypedDict):
family: str
name: str
handle: int
class Metainfo(TypedDict):
version: str
release_name: str
json_schema_version: int
class Rule(TypedDict):
family: str
table: str
chain: str
handle: int
expr: list[dict[str, Any]]
class ChainContainer(TypedDict):
chain: Chain
class MetainfoContainer(TypedDict):
metainfo: Metainfo
class TableContainer(TypedDict):
table: Table
class RuleContainer(TypedDict):
rule: Rule
class NFTablesOutput(TypedDict):
nftables: list[ChainContainer | MetainfoContainer | TableContainer | RuleContainer]
ADDRESS_FAMILY_IPV6 = "ip6"
ADDRESS_FAMILY_IPV4 = "ip"
TABLE_NAME = "qubes"
FORWARD_CHAIN_NAME = "forward"
POSTROUTING_CHAIN_NAME = "postrouting"
ROUTING_MANAGER_CHAIN_NAME = "qubes-routing-manager"
ROUTING_MANAGER_POSTROUTING_CHAIN_NAME = "qubes-routing-manager-postrouting"
NFTABLES_CMD = "nft"
def get_table(address_family: ADDRESS_FAMILIES, table: str) -> NFTablesOutput:
return cast(
NFTablesOutput,
json.loads(
subprocess.check_output(
[NFTABLES_CMD, "-n", "-j", "list", "table", address_family, table],
text=True,
)
),
)
def add_chain(address_family: ADDRESS_FAMILIES, table: str, chain: str) -> None:
subprocess.check_output(
[
NFTABLES_CMD,
"-n",
"-j",
"add",
"chain",
address_family,
table,
chain,
],
text=True,
)
def append_rule_at_end(
address_family: ADDRESS_FAMILIES, table: str, chain: str, *rest: str
) -> None:
subprocess.check_output(
[
NFTABLES_CMD,
"-n",
"-j",
"add",
"rule",
address_family,
table,
chain,
]
+ list(rest),
text=True,
)
def append_counter_at_end(
address_family: ADDRESS_FAMILIES, table: str, chain: str, *rest: str
) -> None:
subprocess.check_output(
[
NFTABLES_CMD,
"-n",
"-j",
"add",
"rule",
address_family,
table,
chain,
"counter",
]
+ list(rest),
text=True,
)
def _append_or_insert_rule(
where: Literal["add"] | Literal["insert"],
address_family: ADDRESS_FAMILIES,
table: str,
chain: str,
handle: int,
*rest: str,
) -> None:
subprocess.check_output(
[
NFTABLES_CMD,
"-n",
"-j",
where,
"rule",
address_family,
table,
chain,
"position",
str(handle),
]
+ list(rest),
text=True,
)
def append_rule_after(
address_family: ADDRESS_FAMILIES, table: str, chain: str, handle: int, *rest: str
) -> None:
_append_or_insert_rule("add", address_family, table, chain, handle, *rest)
def insert_rule_before(
address_family: ADDRESS_FAMILIES, table: str, chain: str, handle: int, *rest: str
) -> None:
_append_or_insert_rule("insert", address_family, table, chain, handle, *rest)
def delete_rule(
address_family: ADDRESS_FAMILIES, table: str, chain: str, handle: int
) -> None:
subprocess.check_output(
[
NFTABLES_CMD,
"-n",
"-j",
"delete",
"rule",
address_family,
table,
chain,
"handle",
str(handle),
],
text=True,
)
def setup_plain_forwarding_for_address(source: str, enable: bool, family: int) -> None:
logging.info("Handling forwarding for address %s family %s.", source, family)
af = cast(
ADDRESS_FAMILIES,
ADDRESS_FAMILY_IPV6 if family == 6 else ADDRESS_FAMILY_IPV4,
)
# table ip qubes {
# set downstream {
# type ipv4_addr
# elements = { 10.137.0.10, 10.250.4.13 }
# }
# ...
existing_table_output = get_table(af, TABLE_NAME)
existing_table_items = existing_table_output["nftables"]
existing_chains = [x["chain"] for x in existing_table_items if "chain" in x] # type: ignore
existing_rules = [x["rule"] for x in existing_table_items if "rule" in x] # type: ignore
try:
forward_chain = [x for x in existing_chains if x["name"] == FORWARD_CHAIN_NAME][
0
]
postrouting_chain = [
x for x in existing_chains if x["name"] == POSTROUTING_CHAIN_NAME
][0]
except IndexError:
logging.warn(
"No forward or postrouting chains in table %s, not setting up forwarding",
TABLE_NAME,
)
return
for chain_name in [
ROUTING_MANAGER_CHAIN_NAME,
ROUTING_MANAGER_POSTROUTING_CHAIN_NAME,
]:
chain: None | Chain = None
try:
chain = [x for x in existing_chains if x["name"] == chain_name].pop()
except IndexError:
pass
if not chain:
logging.info(
"Adding %s chain to table %s and counter to chain",
chain_name,
TABLE_NAME,
)
add_chain(af, TABLE_NAME, chain_name)
append_counter_at_end(
af,
TABLE_NAME,
chain_name,
)
def is_oifgroup_2(rule):
return (
rule["chain"] == forward_chain["name"]
and len(rule["expr"]) == 3
and (
rule["expr"][0].get("match", {}).get("op") == "=="
and rule["expr"][0]
.get("match", {})
.get("left", {})
.get("meta", {})
.get("key")
== "oifgroup"
and rule["expr"][0].get("match", {}).get("right") == 2
)
and (rule["expr"][-1].get("drop", "not none") is None)
)
def is_postrouting_masquerade(rule):
return (
rule["chain"] == postrouting_chain["name"]
and len(rule["expr"]) == 1
and "masquerade" in rule["expr"][0]
)
for parent_chain, child_chain_name, previous_rule_detector, insertor in [
(
forward_chain,
ROUTING_MANAGER_CHAIN_NAME,
is_oifgroup_2,
insert_rule_before,
),
(
postrouting_chain,
ROUTING_MANAGER_POSTROUTING_CHAIN_NAME,
is_postrouting_masquerade,
insert_rule_before,
),
]:
jump_rule: None | Rule = None
try:
jump_rule = [
x
for x in existing_rules
if x["chain"] == parent_chain["name"]
and x["family"] == af
and len(x["expr"]) == 1
and x["expr"][0].get("jump", {}).get("target") == child_chain_name
].pop()
except IndexError:
pass
if not jump_rule:
try:
previous_rule = [
x for x in existing_rules if previous_rule_detector(x)
][0]
except IndexError:
logging.warn(
"Cannot find appropriate previous rule in chain %s of table %s, not setting up forwarding",
parent_chain["name"],
TABLE_NAME,
)
logging.info(
"Adding rule to jump from chain %s to chain %s in table %s",
parent_chain["name"],
child_chain_name,
TABLE_NAME,
)
insertor(
af,
TABLE_NAME,
parent_chain["name"],
previous_rule["handle"],
"jump",
child_chain_name,
)
def detect_ip_rule(rule: Rule, chain_name: str, ip: str, mode: str):
return (
rule["chain"] == chain_name
and len(rule["expr"]) == 2
and rule["expr"][0].get("match", {}).get("op", {}) == "=="
and rule["expr"][0]["match"]
.get("left", {})
.get("payload", {})
.get("protocol", "")
== af
and rule["expr"][0]["match"]["left"]["payload"].get("field", "") == mode
and rule["expr"][0].get("match", {}).get("right", []) == ip
and "accept" in rule["expr"][1]
)
for chain_name, mode in [
(ROUTING_MANAGER_CHAIN_NAME, "daddr"),
(ROUTING_MANAGER_POSTROUTING_CHAIN_NAME, "saddr"),
]:
address_rules = [
x for x in existing_rules if detect_ip_rule(x, chain_name, source, mode)
]
if enable and not address_rules:
logging.info(
"Adding accept rule on chain %s for %s.",
chain_name,
source,
)
append_rule_at_end(
af,
TABLE_NAME,
chain_name,
af,
mode,
source,
"accept",
)
elif not enable and address_rules:
logging.info(
"Removing %s accept rules from chain %s for %s.",
len(address_rules),
chain_name,
source,
)
for rule in reversed(sorted(address_rules, key=lambda r: r["handle"])):
delete_rule(af, TABLE_NAME, chain_name, rule["handle"])

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,162 @@
import os
from unittest import mock
from qubesroutingmanager import setup_plain_forwarding_for_address
def get_fixture(name):
with open(os.path.join(os.path.dirname(__file__), "fixtures", name)) as f:
return f.read()
def mock_collector(output: str):
final_args = []
class MockedPopen:
def __init__(self, args, **kwargs):
final_args.append(args[3:])
self.args = args
self.returncode = 0
def __enter__(self):
return self
def __exit__(self, exc_type, value, traceback):
pass
def communicate(self, input=None, timeout=None):
stdout = output
stderr = ""
self.returncode = 1
return stdout, stderr
def poll(self):
return 0
return final_args, MockedPopen
def test_partial_add_completes_the_add():
got, MockedPopen = mock_collector(get_fixture("partially_added.json"))
expected = [
["list", "table", "ip", "qubes"],
["add", "chain", "ip", "qubes", "qubes-routing-manager-postrouting"],
[
"add",
"rule",
"ip",
"qubes",
"qubes-routing-manager-postrouting",
"counter",
],
[
"insert",
"rule",
"ip",
"qubes",
"postrouting",
"position",
"67",
"jump",
"qubes-routing-manager-postrouting",
],
[
"add",
"rule",
"ip",
"qubes",
"qubes-routing-manager-postrouting",
"ip",
"saddr",
"10.250.4.13",
"accept",
],
]
with mock.patch("subprocess.Popen", MockedPopen):
setup_plain_forwarding_for_address("10.250.4.13", True, 4)
assert got == expected
def test_forward_rule_added_before_oifgroup_2():
got, MockedPopen = mock_collector(get_fixture("no_routing_manager.json"))
expected = [
["list", "table", "ip", "qubes"],
["add", "chain", "ip", "qubes", "qubes-routing-manager"],
[
"add",
"rule",
"ip",
"qubes",
"qubes-routing-manager",
"counter",
],
["add", "chain", "ip", "qubes", "qubes-routing-manager-postrouting"],
[
"add",
"rule",
"ip",
"qubes",
"qubes-routing-manager-postrouting",
"counter",
],
[
"insert",
"rule",
"ip",
"qubes",
"forward",
"position",
"79",
"jump",
"qubes-routing-manager",
],
[
"insert",
"rule",
"ip",
"qubes",
"postrouting",
"position",
"67",
"jump",
"qubes-routing-manager-postrouting",
],
[
"add",
"rule",
"ip",
"qubes",
"qubes-routing-manager",
"ip",
"daddr",
"10.250.4.13",
"accept",
],
[
"add",
"rule",
"ip",
"qubes",
"qubes-routing-manager-postrouting",
"ip",
"saddr",
"10.250.4.13",
"accept",
],
]
with mock.patch("subprocess.Popen", MockedPopen):
setup_plain_forwarding_for_address("10.250.4.13", True, 4)
assert got == expected
def test_forwarding_does_not_add_twice():
got, MockedPopen = mock_collector(get_fixture("fully_added.json"))
expected = [
["list", "table", "ip", "qubes"],
]
with mock.patch("subprocess.Popen", MockedPopen):
setup_plain_forwarding_for_address("10.250.4.13", True, 4)
assert got == expected

View File

@ -0,0 +1,135 @@
#!/usr/bin/python3
"""
This program reads the /qubes-firewall/{ip}/qubes-routing-method file
for any firewall configuration, then configures the network to obey
the routing method for the VM. If the routing method is "masquerade",
then nothing happens. If, however, the routing method is "forward",
then VM-specific rules are enacted in the VM's attached NetVM to allow
traffic coming from other VMs and the outside world to reach this VM.
"""
import glob
import logging
import os
import socket
import qubesdb # type: ignore
from qubesroutingmanager import setup_plain_forwarding_for_address
FORWARD_ROUTING_METHOD = "forward"
def _s(v):
if isinstance(v, bytes):
return v.decode("utf-8")
return v
class AdjunctWorker(object):
def __init__(self):
self.qdb = qubesdb.QubesDB()
@staticmethod
def is_ip6(addr):
return addr.count(":") > 0
def setup_proxy_arp_ndp(self, enabled, family):
# If any of the IP addresses is assigned the forward routing method,
# then enable proxy ARP/NDP on the upstream interfaces, so that the
# interfaces in question will impersonate the IP addresses in question.
# Ideally, this impersonation would be exclusively done for the
# specific IP addresses in question, but it is not clear to me how
# to cause this outcome to take place.
if family == 6:
globber = "/proc/sys/net/ipv6/conf/*/proxy_ndp"
name = "proxy NDP"
elif family == 4:
globber = "/proc/sys/net/ipv4/conf/*/proxy_arp"
name = "proxy ARP"
else:
return
if enabled:
action = "Enabling"
val = "1\n"
else:
action = "Disabling"
val = "0\n"
matches = glob.glob(globber)
for m in matches:
iface = m.split("/")[6]
if iface in ("all", "lo") or iface.startswith("vif"):
# No need to enable it for "all", or VIFs, or loopback.
continue
with open(m, "w+") as f:
oldval = f.read()
if oldval != val:
logging.info("%s %s on interface %s.", action, name, iface)
f.seek(0)
f.write(val)
def handle_addr(self, addr):
# Setup plain forwarding for this specific address.
routing_method = _s(self.qdb.read("/qubes-routing-method/{}".format(addr)))
setup_plain_forwarding_for_address(
addr,
routing_method == FORWARD_ROUTING_METHOD,
6 if self.is_ip6(addr) else 4,
)
# Manipulate proxy ARP for all known addresses.
methods = [
(_s(k).split("/")[2], _s(v))
for k, v in self.qdb.multiread("/qubes-routing-method/").items()
]
mmethods = {
4: [m[1] for m in methods if not self.is_ip6(m[0])],
6: [m[1] for m in methods if self.is_ip6(m[0])],
}
for family, methods in mmethods.items():
self.setup_proxy_arp_ndp(
FORWARD_ROUTING_METHOD in methods,
family,
)
def list_targets(self):
return set(_s(t).split("/")[2] for t in self.qdb.list("/qubes-routing-method/"))
def sd_notify(self, state):
"""Send notification to systemd, if available"""
# based on sdnotify python module
if "NOTIFY_SOCKET" not in os.environ:
return
addr = os.environ["NOTIFY_SOCKET"]
if addr[0] == "@":
addr = "\0" + addr[1:]
try:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
sock.connect(addr)
sock.sendall(state.encode())
except BaseException:
# generally ignore error on systemd notification
pass
def main(self):
logging.basicConfig(level=logging.INFO)
self.qdb.watch("/qubes-routing-method/")
for source_addr in self.list_targets():
self.handle_addr(source_addr)
self.sd_notify("READY=1")
try:
for watch_path in iter(self.qdb.read_watch, None):
# ignore writing rules itself - wait for final write at
# source_addr level empty write (/qubes-firewall/SOURCE_ADDR)
watch_path = _s(watch_path)
if watch_path.count("/") != 2:
continue
source_addr = watch_path.split("/")[2]
self.handle_addr(source_addr)
except OSError: # EINTR
# signal received, don't continue the loop
return

21
routingmanagersetup.py Normal file
View File

@ -0,0 +1,21 @@
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="qubesroutingmanager",
version=version,
author="Manuel Amador (Rudd-O)",
author_email="rudd-o@rudd-o.com",
description="Qubes network server network qube (template) component",
license="GPL2+",
url="https://github.com/Rudd-O/qubes-network-server",
packages=("qubesroutingmanager",),
)

View File

@ -1,24 +0,0 @@
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',
],
}
)

View File

@ -1,260 +1,8 @@
#!/usr/bin/python2
"""
This program reads the /qubes-firewall/{ip}/qubes-routing-method file
for any firewall configuration, then configures the network to obey
the routing method for the VM. If the routing method is "masquerade",
then nothing happens. If, however, the routing method is "forward",
then VM-specific rules are enacted in the VM's attached NetVM to allow
traffic coming from other VMs and the outside world to reach this VM.
"""
import glob
import logging
import os
import socket
import subprocess
import qubesdb
def _s(v):
if isinstance(v, bytes):
return v.decode("utf-8")
return v
FORWARD_ROUTING_METHOD = "forward"
class AdjunctWorker(object):
def __init__(self):
self.qdb = qubesdb.QubesDB()
@staticmethod
def is_ip6(addr):
return addr.count(":") > 0
def setup_plain_forwarding_for_address(self, source, enable, family):
def find_pos_of_first_rule(table, startswith):
rules = [n for n, l in enumerate(out) if l.startswith(startswith)]
if rules:
return rules[0]
return None
cmd = "ip6tables" if family == 6 else "iptables"
mask = "/128" if family == 6 else "/32"
def run_ipt(*args):
return subprocess.check_call([cmd, "-w"] + list(args))
out = subprocess.check_output(
[cmd + "-save"], universal_newlines=True
).splitlines()
if enable:
# Create necessary prerouting chain.
if not find_pos_of_first_rule(out, ":PR-PLAIN-FORWARDING - "):
run_ipt("-t", "nat", "-N", "PR-PLAIN-FORWARDING")
# Route prerouting traffic to necessary chain.
if not find_pos_of_first_rule(out, "-A POSTROUTING -j PR-PLAIN-FORWARDING"):
rule_num = find_pos_of_first_rule(out, "-A POSTROUTING -j MASQUERADE")
if not rule_num:
# This table does not contain the masquerading rule.
# Accordingly, we will not do anything.
return
first_rule_num = find_pos_of_first_rule(out, "-A POSTROUTING")
pos = rule_num - first_rule_num + 1
logging.info("Adding POSTROUTING chain PR-PLAIN-FORWARDING.")
run_ipt(
"-t",
"nat",
"-I",
"POSTROUTING",
str(pos),
"-j",
"PR-PLAIN-FORWARDING",
)
# Create necessary forward chain.
if not find_pos_of_first_rule(out, ":PLAIN-FORWARDING - "):
run_ipt("-t", "filter", "-N", "PLAIN-FORWARDING")
# Route forward traffic to necessary chain.
if not find_pos_of_first_rule(out, "-A FORWARD -j PLAIN-FORWARDING"):
rule_num = find_pos_of_first_rule(
out, "-A FORWARD -i vif+ -o vif+ -j DROP"
)
if not rule_num:
# This table does not contain the masquerading rule.
# Accordingly, we will not do anything.
return
first_rule_num = find_pos_of_first_rule(out, "-A FORWARD")
pos = rule_num - first_rule_num + 1
logging.info("Adding FORWARD chain PLAIN-FORWARDING.")
run_ipt(
"-t", "filter", "-I", "FORWARD", str(pos), "-j", "PLAIN-FORWARDING"
)
rule = find_pos_of_first_rule(
out, "-A PR-PLAIN-FORWARDING -s {}{} -j ACCEPT".format(source, mask)
)
if enable:
if rule:
pass
else:
logging.info(
"Adding POSTROUTING rule to forward traffic from %s.", source
)
run_ipt(
"-t",
"nat",
"-A",
"PR-PLAIN-FORWARDING",
"-s",
"{}{}".format(source, mask),
"-j",
"ACCEPT",
)
else:
if rule:
first_rule = find_pos_of_first_rule(out, "-A PR-PLAIN-FORWARDING")
pos = rule - first_rule + 1
logging.info(
"Removing POSTROUTING rule forwarding traffic from %s.", source
)
run_ipt("-t", "nat", "-D", "PR-PLAIN-FORWARDING", str(pos))
else:
pass
rule = find_pos_of_first_rule(
out, "-A PLAIN-FORWARDING -d {}{} -o vif+ -j ACCEPT".format(source, mask)
)
if enable:
if rule:
pass
else:
logging.info("Adding FORWARD rule to allow traffic to %s.", source)
run_ipt(
"-t",
"filter",
"-A",
"PLAIN-FORWARDING",
"-d",
"{}{}".format(source, mask),
"-o",
"vif+",
"-j",
"ACCEPT",
)
else:
if rule:
logging.info("Removing FORWARD rule allowing traffic to %s.", source)
first_rule = find_pos_of_first_rule(out, "-A PLAIN-FORWARDING")
pos = rule - first_rule + 1
run_ipt("-t", "filter", "-D", "PLAIN-FORWARDING", str(pos))
else:
pass
def setup_proxy_arp_ndp(self, enabled, family):
# If any of the IP addresses is assigned the forward routing method,
# then enable proxy ARP/NDP on the upstream interfaces, so that the
# interfaces in question will impersonate the IP addresses in question.
# Ideally, this impersonation would be exclusively done for the
# specific IP addresses in question, but it is not clear to me how
# to cause this outcome to take place.
if family == 6:
globber = "/proc/sys/net/ipv6/conf/*/proxy_ndp"
name = "proxy NDP"
elif family == 4:
globber = "/proc/sys/net/ipv4/conf/*/proxy_arp"
name = "proxy ARP"
else:
return
if enabled:
action = "Enabling"
val = "1\n"
else:
action = "Disabling"
val = "0\n"
matches = glob.glob(globber)
for m in matches:
iface = m.split("/")[6]
if iface in ("all", "lo") or iface.startswith("vif"):
# No need to enable it for "all", or VIFs, or loopback.
continue
with open(m, "w+") as f:
oldval = f.read()
if oldval != val:
logging.info("%s %s on interface %s.", action, name, iface)
f.seek(0)
f.write(val)
def handle_addr(self, addr):
# Setup plain forwarding for this specific address.
routing_method = _s(self.qdb.read("/qubes-routing-method/{}".format(addr)))
self.setup_plain_forwarding_for_address(
addr,
routing_method == FORWARD_ROUTING_METHOD,
6 if self.is_ip6(addr) else 4,
)
# Manipulate proxy ARP for all known addresses.
methods = [
(_s(k).split("/")[2], _s(v))
for k, v in self.qdb.multiread("/qubes-routing-method/").items()
]
mmethods = {
4: [m[1] for m in methods if not self.is_ip6(m[0])],
6: [m[1] for m in methods if self.is_ip6(m[0])],
}
for family, methods in mmethods.items():
self.setup_proxy_arp_ndp(
FORWARD_ROUTING_METHOD in methods, family,
)
def list_targets(self):
return set(_s(t).split("/")[2] for t in self.qdb.list("/qubes-routing-method/"))
def sd_notify(self, state):
"""Send notification to systemd, if available"""
# based on sdnotify python module
if not "NOTIFY_SOCKET" in os.environ:
return
addr = os.environ["NOTIFY_SOCKET"]
if addr[0] == "@":
addr = "\0" + addr[1:]
try:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
sock.connect(addr)
sock.sendall(state.encode())
except:
# generally ignore error on systemd notification
pass
def main(self):
logging.basicConfig(level=logging.INFO)
self.qdb.watch("/qubes-routing-method/")
for source_addr in self.list_targets():
self.handle_addr(source_addr)
self.sd_notify("READY=1")
try:
for watch_path in iter(self.qdb.read_watch, None):
# ignore writing rules itself - wait for final write at
# source_addr level empty write (/qubes-firewall/SOURCE_ADDR)
watch_path = _s(watch_path)
if watch_path.count("/") != 2:
continue
source_addr = watch_path.split("/")[2]
self.handle_addr(source_addr)
except OSError: # EINTR
# signal received, don't continue the loop
return
#!/usr/bin/python3
if __name__ == "__main__":
from qubesroutingmanager.worker import AdjunctWorker
w = AdjunctWorker()
w.main()

View File

@ -1,13 +1,13 @@
[Unit]
Description=Configure the network to allow network server VMs
Documentation=https://github.com/Rudd-O/qubes-network-server
ConditionPathExists=/var/run/qubes-service/qubes-firewall
After=qubes-firewall.service
BindsTo=qubes-firewall.service
After=qubes-network.service qubes-iptables.service
BindsTo=qubes-iptables.service
ConditionPathExists=/var/run/qubes-service/qubes-network
[Service]
Type=notify
ExecStart=@SBINDIR@/qubes-routing-manager
[Install]
WantedBy=multi-user.target
WantedBy=qubes-network.service

10
tox.ini Normal file
View File

@ -0,0 +1,10 @@
[tox]
envlist = basepython
[testenv]
deps =
pytest
mypy
commands =
pytest -vv
mypy -p qubesroutingmanager