From cf2945e7421c6a419da4198afb13c0235df43b00 Mon Sep 17 00:00:00 2001 From: "Manuel Amador (Rudd-O)" Date: Mon, 5 Feb 2024 21:51:59 +0000 Subject: [PATCH] Qubes 4.2 support. --- .gitignore | 1 + Makefile | 5 +- README.md | 6 +- build.parameters | 2 +- networkserversetup.py | 26 + qubes-network-server.spec | 9 +- qubesroutingmanager/__init__.py | 310 +++++ qubesroutingmanager/test_firewalling.py | 1578 +++++++++++++++++++++++ qubesroutingmanager/worker.py | 135 ++ routingmanagersetup.py | 21 + setup.py | 24 - src/qubes-routing-manager | 261 +--- src/qubes-routing-manager.service.in | 7 +- 13 files changed, 2089 insertions(+), 296 deletions(-) create mode 100644 networkserversetup.py create mode 100644 qubesroutingmanager/__init__.py create mode 100644 qubesroutingmanager/test_firewalling.py create mode 100644 qubesroutingmanager/worker.py create mode 100644 routingmanagersetup.py delete mode 100644 setup.py diff --git a/.gitignore b/.gitignore index 2dd9a98..7284d3a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ pkgs/ build *.egg-info src/*.service +.mypy_cache diff --git a/Makefile b/Makefile index 4943dda..64d3ed0 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 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,13 @@ 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 diff --git a/README.md b/README.md index 64ed066..9cadff4 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Qubes network server -This software lets you turn your [Qubes OS 4.1](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. -This release is only intended for use with Qubes OS 4.1. Older Qubes OS releases -will not support it. For Qubes OS 4.0, check branch `r4.0`. +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`. ## Why? diff --git a/build.parameters b/build.parameters index edf5e7a..9986a39 100644 --- a/build.parameters +++ b/build.parameters @@ -1 +1 @@ -["RELEASE": "q4.1 37"] +["RELEASE": "q4.2 37 38 39"] diff --git a/networkserversetup.py b/networkserversetup.py new file mode 100644 index 0000000..db117f2 --- /dev/null +++ b/networkserversetup.py @@ -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", + ], + }, + ) diff --git a/qubes-network-server.spec b/qubes-network-server.spec index ba45d66..8b60494 100644 --- a/qubes-network-server.spec +++ b/qubes-network-server.spec @@ -3,7 +3,7 @@ %define mybuildnumber %{?build_number}%{?!build_number:1} Name: qubes-network-server -Version: 0.0.19 +Version: 0.1.0 Release: %{mybuildnumber}%{?dist} Summary: Turn your Qubes OS into a network server BuildArch: noarch @@ -20,9 +20,10 @@ BuildRequires: python3 BuildRequires: python3-rpm-macros BuildRequires: systemd-rpm-macros -Requires: qubes-core-agent-networking >= 4.1 +Requires: qubes-core-agent-networking >= 4.2 Requires: python3 Requires: python3-qubesdb +Requires: nftables %description This package lets you turn your Qubes OS into a network server. Install this @@ -44,7 +45,7 @@ BuildRequires: python3-rpm-macros BuildRequires: python3-setuptools Requires: python3 -Requires: qubes-core-dom0 >= 4.1 +Requires: 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 @@ -70,6 +71,8 @@ echo 'enable qubes-routing-manager.service' > "$RPM_BUILD_ROOT"/%{_presetdir}/75 %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 diff --git a/qubesroutingmanager/__init__.py b/qubesroutingmanager/__init__.py new file mode 100644 index 0000000..4a33adc --- /dev/null +++ b/qubesroutingmanager/__init__.py @@ -0,0 +1,310 @@ +#!/usr/bin/python3 + +import json +import logging +import subprocess + +from typing import TypedDict, Any, cast, Literal + + +ADDRESS_FAMILIES = 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" +ROUTING_MANAGER_CHAIN_NAME = "qubes-routing-manager" +NFTABLES_CMD = "nft" +ADD_RULE_AFTER_THIS_RULE = "custom-forward" + + +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_rule_after( + address_family: ADDRESS_FAMILIES, table: str, chain: str, handle: int, *rest: str +) -> None: + subprocess.check_output( + [ + NFTABLES_CMD, + "-n", + "-j", + "add", + "rule", + address_family, + table, + chain, + "position", + str(handle), + ] + + list(rest), + text=True, + ) + + +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 + ] + except IndexError: + logging.warn( + "No forward chain in table %s, not setting up forwarding", TABLE_NAME + ) + return + + qubes_routing_manager_chain: None | Chain = None + try: + qubes_routing_manager_chain = [ + x for x in existing_chains if x["name"] == ROUTING_MANAGER_CHAIN_NAME + ].pop() + except IndexError: + pass + + if not qubes_routing_manager_chain: + logging.info( + "Adding %s chain to table %s", ROUTING_MANAGER_CHAIN_NAME, TABLE_NAME + ) + add_chain(af, TABLE_NAME, ROUTING_MANAGER_CHAIN_NAME) + + qubes_routing_manager_rule: None | Rule = None + try: + qubes_routing_manager_rule = [ + x + for x in existing_rules + if x["chain"] == forward_chain["name"] + and x["family"] == af + and len(x["expr"]) == 1 + and x["expr"][0].get("jump", {}).get("target") == ROUTING_MANAGER_CHAIN_NAME + ].pop() + except IndexError: + pass + + if not qubes_routing_manager_rule: + try: + custom_forwarding_rule = [ + x + for x in existing_rules + if x["chain"] == forward_chain["name"] + and len(x["expr"]) == 1 + and x["expr"][0].get("jump", {}).get("target") + == ADD_RULE_AFTER_THIS_RULE + ][0] + except IndexError: + logging.warn( + "No state forwarding rule in chain %s of table %s, not setting up forwarding", + forward_chain["name"], + TABLE_NAME, + ) + logging.info( + "Adding rule to jump to %s to table %s after jump to %s", + ROUTING_MANAGER_CHAIN_NAME, + TABLE_NAME, + ADD_RULE_AFTER_THIS_RULE, + ) + append_rule_after( + af, + TABLE_NAME, + forward_chain["name"], + custom_forwarding_rule["handle"], + "jump", + ROUTING_MANAGER_CHAIN_NAME, + ) + append_counter_at_end( + af, + TABLE_NAME, + ROUTING_MANAGER_CHAIN_NAME, + ) + + address_rules = [ + x + for x in existing_rules + if x["chain"] == ROUTING_MANAGER_CHAIN_NAME + and len(x["expr"]) == 2 + and x["expr"][0].get("match", {}).get("op", {}) == "==" + and x["expr"][0]["match"].get("left", {}).get("payload", {}).get("protocol", "") + == af + and x["expr"][0]["match"]["left"]["payload"].get("field", "") == "daddr" + and x["expr"][0].get("match", {}).get("right", []) == source + and "accept" in x["expr"][1] + ] + + if enable and not address_rules: + logging.info( + "Adding accept rule on chain %s to allow traffic to %s.", + ROUTING_MANAGER_CHAIN_NAME, + source, + ) + append_rule_at_end( + af, + TABLE_NAME, + ROUTING_MANAGER_CHAIN_NAME, + af, + "daddr", + source, + "accept", + ) + elif not enable and address_rules: + logging.info( + "Removing %s accept rules from chain %s to stop traffic to %s.", + len(address_rules), + ROUTING_MANAGER_CHAIN_NAME, + source, + ) + for rule in reversed(sorted(address_rules, key=lambda r: r["handle"])): + delete_rule(af, TABLE_NAME, ROUTING_MANAGER_CHAIN_NAME, rule["handle"]) diff --git a/qubesroutingmanager/test_firewalling.py b/qubesroutingmanager/test_firewalling.py new file mode 100644 index 0000000..fb6be1b --- /dev/null +++ b/qubesroutingmanager/test_firewalling.py @@ -0,0 +1,1578 @@ +import subprocess + +from unittest import mock +from qubesroutingmanager import setup_plain_forwarding_for_address + + +ALREADY_ADDED = """ +{ + "nftables": [ + { + "metainfo": { + "version": "1.0.5", + "release_name": "Lester Gooch #4", + "json_schema_version": 1 + } + }, + { + "table": { + "family": "ip", + "name": "qubes", + "handle": 1 + } + }, + { + "set": { + "family": "ip", + "name": "downstream", + "table": "qubes", + "type": "ipv4_addr", + "handle": 3, + "elem": [ + "10.137.0.10", + "10.250.4.13" + ] + } + }, + { + "set": { + "family": "ip", + "name": "allowed", + "table": "qubes", + "type": [ + "ifname", + "ipv4_addr" + ], + "handle": 4, + "elem": [ + { + "concat": [ + "vif6.0", + "10.137.0.10" + ] + }, + { + "concat": [ + "vif12.0", + "10.250.4.13" + ] + } + ] + } + }, + { + "chain": { + "family": "ip", + "table": "qubes", + "name": "prerouting", + "handle": 1, + "type": "filter", + "hook": "prerouting", + "prio": -300, + "policy": "accept" + } + }, + { + "chain": { + "family": "ip", + "table": "qubes", + "name": "antispoof", + "handle": 2 + } + }, + { + "chain": { + "family": "ip", + "table": "qubes", + "name": "postrouting", + "handle": 60, + "type": "nat", + "hook": "postrouting", + "prio": 100, + "policy": "accept" + } + }, + { + "chain": { + "family": "ip", + "table": "qubes", + "name": "input", + "handle": 61, + "type": "filter", + "hook": "input", + "prio": 0, + "policy": "drop" + } + }, + { + "chain": { + "family": "ip", + "table": "qubes", + "name": "forward", + "handle": 62, + "type": "filter", + "hook": "forward", + "prio": 0, + "policy": "accept" + } + }, + { + "chain": { + "family": "ip", + "table": "qubes", + "name": "custom-input", + "handle": 63 + } + }, + { + "chain": { + "family": "ip", + "table": "qubes", + "name": "custom-forward", + "handle": 64 + } + }, + { + "chain": { + "family": "ip", + "table": "qubes", + "name": "dnat-dns", + "handle": 87, + "type": "nat", + "hook": "prerouting", + "prio": -100, + "policy": "accept" + } + }, + { + "chain": { + "family": "ip", + "table": "qubes", + "name": "qubes-routing-manager", + "handle": 105 + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "prerouting", + "handle": 5, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "iifgroup" + } + }, + "right": 2 + } + }, + { + "goto": { + "target": "antispoof" + } + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "prerouting", + "handle": 6, + "expr": [ + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "ip", + "field": "saddr" + } + }, + "right": "@downstream" + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "drop": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "antispoof", + "handle": 7, + "expr": [ + { + "match": { + "op": "==", + "left": { + "concat": [ + { + "meta": { + "key": "iifname" + } + }, + { + "payload": { + "protocol": "ip", + "field": "saddr" + } + } + ] + }, + "right": "@allowed" + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "antispoof", + "handle": 8, + "expr": [ + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "drop": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "postrouting", + "handle": 65, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "oifgroup" + } + }, + "right": 2 + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "postrouting", + "handle": 66, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "oif" + } + }, + "right": "lo" + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "postrouting", + "handle": 67, + "expr": [ + { + "masquerade": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "postrouting", + "handle": 90, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "oifgroup" + } + }, + "right": 2 + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "postrouting", + "handle": 91, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "oif" + } + }, + "right": "lo" + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "postrouting", + "handle": 92, + "expr": [ + { + "masquerade": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "postrouting", + "handle": 109, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "oifgroup" + } + }, + "right": 2 + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "postrouting", + "handle": 110, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "oif" + } + }, + "right": "lo" + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "postrouting", + "handle": 111, + "expr": [ + { + "masquerade": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 68, + "expr": [ + { + "jump": { + "target": "custom-input" + } + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 69, + "expr": [ + { + "match": { + "op": "in", + "left": { + "ct": { + "key": "state" + } + }, + "right": "invalid" + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "drop": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 70, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "iifgroup" + } + }, + "right": 2 + } + }, + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "udp", + "field": "dport" + } + }, + "right": 68 + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "drop": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 71, + "expr": [ + { + "match": { + "op": "in", + "left": { + "ct": { + "key": "state" + } + }, + "right": [ + "established", + "related" + ] + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 72, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "iifgroup" + } + }, + "right": 2 + } + }, + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "l4proto" + } + }, + "right": "icmp" + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 73, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "iif" + } + }, + "right": "lo" + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 74, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "iifgroup" + } + }, + "right": 2 + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "reject": { + "type": "icmp", + "expr": "host-prohibited" + } + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 75, + "expr": [ + { + "counter": { + "packets": 22933, + "bytes": 1253148 + } + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 93, + "expr": [ + { + "jump": { + "target": "custom-input" + } + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 94, + "expr": [ + { + "match": { + "op": "in", + "left": { + "ct": { + "key": "state" + } + }, + "right": "invalid" + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "drop": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 95, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "iifgroup" + } + }, + "right": 2 + } + }, + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "udp", + "field": "dport" + } + }, + "right": 68 + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "drop": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 96, + "expr": [ + { + "match": { + "op": "in", + "left": { + "ct": { + "key": "state" + } + }, + "right": [ + "established", + "related" + ] + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 97, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "iifgroup" + } + }, + "right": 2 + } + }, + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "l4proto" + } + }, + "right": "icmp" + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 98, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "iif" + } + }, + "right": "lo" + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 99, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "iifgroup" + } + }, + "right": 2 + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "reject": { + "type": "icmp", + "expr": "host-prohibited" + } + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 100, + "expr": [ + { + "counter": { + "packets": 1648, + "bytes": 90088 + } + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 112, + "expr": [ + { + "jump": { + "target": "custom-input" + } + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 113, + "expr": [ + { + "match": { + "op": "in", + "left": { + "ct": { + "key": "state" + } + }, + "right": "invalid" + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "drop": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 114, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "iifgroup" + } + }, + "right": 2 + } + }, + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "udp", + "field": "dport" + } + }, + "right": 68 + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "drop": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 115, + "expr": [ + { + "match": { + "op": "in", + "left": { + "ct": { + "key": "state" + } + }, + "right": [ + "established", + "related" + ] + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 116, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "iifgroup" + } + }, + "right": 2 + } + }, + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "l4proto" + } + }, + "right": "icmp" + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 117, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "iif" + } + }, + "right": "lo" + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 118, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "iifgroup" + } + }, + "right": 2 + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "reject": { + "type": "icmp", + "expr": "host-prohibited" + } + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "input", + "handle": 119, + "expr": [ + { + "counter": { + "packets": 947, + "bytes": 51684 + } + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "forward", + "handle": 76, + "expr": [ + { + "jump": { + "target": "custom-forward" + } + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "forward", + "handle": 138, + "expr": [ + { + "jump": { + "target": "qubes-routing-manager" + } + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "forward", + "handle": 77, + "expr": [ + { + "match": { + "op": "in", + "left": { + "ct": { + "key": "state" + } + }, + "right": "invalid" + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "drop": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "forward", + "handle": 78, + "expr": [ + { + "match": { + "op": "in", + "left": { + "ct": { + "key": "state" + } + }, + "right": [ + "established", + "related" + ] + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "forward", + "handle": 79, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "oifgroup" + } + }, + "right": 2 + } + }, + { + "counter": { + "packets": 105866, + "bytes": 6788164 + } + }, + { + "drop": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "forward", + "handle": 101, + "expr": [ + { + "jump": { + "target": "custom-forward" + } + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "forward", + "handle": 102, + "expr": [ + { + "match": { + "op": "in", + "left": { + "ct": { + "key": "state" + } + }, + "right": "invalid" + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "drop": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "forward", + "handle": 103, + "expr": [ + { + "match": { + "op": "in", + "left": { + "ct": { + "key": "state" + } + }, + "right": [ + "established", + "related" + ] + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "forward", + "handle": 104, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "oifgroup" + } + }, + "right": 2 + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "drop": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "forward", + "handle": 120, + "expr": [ + { + "jump": { + "target": "custom-forward" + } + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "forward", + "handle": 121, + "expr": [ + { + "match": { + "op": "in", + "left": { + "ct": { + "key": "state" + } + }, + "right": "invalid" + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "drop": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "forward", + "handle": 122, + "expr": [ + { + "match": { + "op": "in", + "left": { + "ct": { + "key": "state" + } + }, + "right": [ + "established", + "related" + ] + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "forward", + "handle": 123, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "oifgroup" + } + }, + "right": 2 + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "drop": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "dnat-dns", + "handle": 88, + "expr": [ + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "ip", + "field": "daddr" + } + }, + "right": "10.139.1.1" + } + }, + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "udp", + "field": "dport" + } + }, + "right": 53 + } + }, + { + "dnat": { + "addr": "10.250.7.2" + } + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "dnat-dns", + "handle": 89, + "expr": [ + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "ip", + "field": "daddr" + } + }, + "right": "10.139.1.1" + } + }, + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "tcp", + "field": "dport" + } + }, + "right": 53 + } + }, + { + "dnat": { + "addr": "10.250.7.2" + } + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "qubes-routing-manager", + "handle": 140, + "expr": [ + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "ip", + "field": "daddr" + } + }, + "right": "10.250.4.13" + } + }, + { + "accept": null + } + ] + } + } + ] +} +""" + + +def mock_collector(): + final_args = [] + + class MockedPopen: + def __init__(self, args, **kwargs): + final_args.append(args) + 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 = ALREADY_ADDED + stderr = "" + self.returncode = 1 + return stdout, stderr + + def poll(self): + return 0 + + return final_args, MockedPopen + + +def test_forwarding_does_not_add_twice(): + args, MockedPopen = mock_collector() + expected = [ + ["nft", "-n", "-j", "list", "table", "ip", "qubes"], + ] + with mock.patch("subprocess.Popen", MockedPopen): + setup_plain_forwarding_for_address("10.250.4.13", True, 4) + + assert args == expected diff --git a/qubesroutingmanager/worker.py b/qubesroutingmanager/worker.py new file mode 100644 index 0000000..24e7eb8 --- /dev/null +++ b/qubesroutingmanager/worker.py @@ -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 + +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 diff --git a/routingmanagersetup.py b/routingmanagersetup.py new file mode 100644 index 0000000..aab9750 --- /dev/null +++ b/routingmanagersetup.py @@ -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",), + ) diff --git a/setup.py b/setup.py deleted file mode 100644 index 416d33a..0000000 --- a/setup.py +++ /dev/null @@ -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', - ], - } - ) diff --git a/src/qubes-routing-manager b/src/qubes-routing-manager index 294b04d..36c6236 100755 --- a/src/qubes-routing-manager +++ b/src/qubes-routing-manager @@ -1,265 +1,8 @@ #!/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 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(table) 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_nat = subprocess.check_output( - [cmd + "-save", "-t", "nat"], universal_newlines=True - ).splitlines() - out_filter = subprocess.check_output( - [cmd + "-save", "-t", "filter"], universal_newlines=True - ).splitlines() - - if enable: - # Create necessary prerouting chain. - if not find_pos_of_first_rule(out_nat, ":PR-PLAIN-FORWARDING - "): - logging.info("Creating chain PR-PLAIN-FORWARDING on table nat.") - run_ipt("-t", "nat", "-N", "PR-PLAIN-FORWARDING") - - # Route prerouting traffic to necessary chain. - if not find_pos_of_first_rule(out_nat, "-A POSTROUTING -j PR-PLAIN-FORWARDING"): - rule_num = find_pos_of_first_rule(out_nat, "-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_nat, "-A POSTROUTING") - pos = rule_num - first_rule_num + 1 - logging.info("Adding chain PR-PLAIN-FORWARDING to chain POSTROUTING on table nat.") - run_ipt( - "-t", - "nat", - "-I", - "POSTROUTING", - str(pos), - "-j", - "PR-PLAIN-FORWARDING", - ) - - # Create necessary forward chain. - if not find_pos_of_first_rule(out_filter, ":PLAIN-FORWARDING - "): - logging.info("Creating chain PLAIN-FORWARDING on table filter.") - run_ipt("-t", "filter", "-N", "PLAIN-FORWARDING") - - # Route forward traffic to necessary chain. - if not find_pos_of_first_rule(out_filter, "-A FORWARD -j PLAIN-FORWARDING"): - rule_num = find_pos_of_first_rule( - out_filter, "-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_filter, "-A FORWARD") - pos = rule_num - first_rule_num + 1 - logging.info("Adding chain PLAIN-FORWARDING to chain FORWARD on table filter.") - run_ipt( - "-t", "filter", "-I", "FORWARD", str(pos), "-j", "PLAIN-FORWARDING" - ) - - rule = find_pos_of_first_rule( - out_nat, "-A PR-PLAIN-FORWARDING -s {}{} -j ACCEPT".format(source, mask) - ) - if enable: - if rule: - pass - else: - logging.info( - "Adding PR-PLAIN-FORWARDING rule on table nat 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_nat, "-A PR-PLAIN-FORWARDING") - pos = rule - first_rule + 1 - logging.info( - "Removing PR-PLAIN-FORWARDING rule on table nat forwarding traffic from %s.", source - ) - run_ipt("-t", "nat", "-D", "PR-PLAIN-FORWARDING", str(pos)) - else: - pass - - rule = find_pos_of_first_rule( - out_filter, "-A PLAIN-FORWARDING -d {}{} -o vif+ -j ACCEPT".format(source, mask) - ) - if enable: - if rule: - pass - else: - logging.info("Adding PLAIN-FORWARDING rule on table filter 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 PLAIN-FORWARDING rule on table filter allowing traffic to %s.", source) - first_rule = find_pos_of_first_rule(out_filter, "-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 - if __name__ == "__main__": + from qubesroutingmanager.worker import AdjunctWorker + w = AdjunctWorker() w.main() diff --git a/src/qubes-routing-manager.service.in b/src/qubes-routing-manager.service.in index b294be9..e10d7e2 100644 --- a/src/qubes-routing-manager.service.in +++ b/src/qubes-routing-manager.service.in @@ -1,13 +1,12 @@ [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-iptables.service +BindsTo=qubes-iptables.service [Service] Type=notify ExecStart=@SBINDIR@/qubes-routing-manager [Install] -WantedBy=multi-user.target +WantedBy=qubes-iptables.service \ No newline at end of file