From f7bfd46bdc0ca8d8b31e8e2f9fd83c1fbabbe9c2 Mon Sep 17 00:00:00 2001 From: "Manuel Amador (Rudd-O)" Date: Thu, 29 Feb 2024 02:37:11 +0000 Subject: [PATCH] Ensure the forward rule is added after connection tracking. Also improve tests and add Tox for mypy and pytest. --- .gitignore | 1 + Makefile | 5 +- qubes-network-server.spec | 8 +- qubesroutingmanager/__init__.py | 26 +- .../fixtures/no_routing_manager.json | 1290 +++++++++++++++++ qubesroutingmanager/test_firewalling.py | 77 +- qubesroutingmanager/worker.py | 2 +- tox.ini | 10 + 8 files changed, 1405 insertions(+), 14 deletions(-) create mode 100644 qubesroutingmanager/fixtures/no_routing_manager.json create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 7284d3a..55a5523 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ build *.egg-info src/*.service .mypy_cache +.tox diff --git a/Makefile b/Makefile index 64d3ed0..e132233 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ 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 @@ -42,3 +42,6 @@ install-dom0: PYTHONDONTWRITEBYTECODE=1 python3 networkserversetup.py install $(PYTHON_PREFIX_ARG) -O0 --root $(DESTDIR) install: install-dom0 install-template + +test: + tox --current-env diff --git a/qubes-network-server.spec b/qubes-network-server.spec index 0fcb9ff..d4fd31e 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.1.2 +Version: 0.1.3 Release: %{mybuildnumber}%{?dist} Summary: Turn your Qubes OS into a network server BuildArch: noarch @@ -19,6 +19,9 @@ BuildRequires: findutils BuildRequires: python3 BuildRequires: python3-rpm-macros BuildRequires: systemd-rpm-macros +BuildRequires: python3-tox-current-env +BuildRequires: python3-mypy +BuildRequires: python3-pytest Requires: qubes-core-agent-networking >= 4.2 Conflicts: qubes-core-agent < 4.2 @@ -71,6 +74,9 @@ make install DESTDIR=$RPM_BUILD_ROOT SBINDIR=%{_sbindir} UNITDIR=%{_unitdir} PYT 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/* diff --git a/qubesroutingmanager/__init__.py b/qubesroutingmanager/__init__.py index 1386230..652c7c3 100644 --- a/qubesroutingmanager/__init__.py +++ b/qubesroutingmanager/__init__.py @@ -4,10 +4,10 @@ import json import logging import subprocess -from typing import TypedDict, Any, cast, Literal +from typing import TypedDict, Any, cast, Literal, Union -ADDRESS_FAMILIES = Literal["ip"] | Literal["ip6"] +ADDRESS_FAMILIES = Union[Literal["ip"], Literal["ip6"]] class Chain(TypedDict): @@ -69,7 +69,6 @@ POSTROUTING_CHAIN_NAME = "postrouting" ROUTING_MANAGER_CHAIN_NAME = "qubes-routing-manager" ROUTING_MANAGER_POSTROUTING_CHAIN_NAME = "qubes-routing-manager-postrouting" NFTABLES_CMD = "nft" -ADD_FORWARD_RULE_AFTER_THIS_RULE = "custom-forward" def get_table(address_family: ADDRESS_FAMILIES, table: str) -> NFTablesOutput: @@ -254,12 +253,21 @@ def setup_plain_forwarding_for_address(source: str, enable: bool, family: int) - chain_name, ) - def is_forward_jump_to_custom_forward(rule): + def is_oifgroup_2(rule): return ( rule["chain"] == forward_chain["name"] - and len(rule["expr"]) == 1 - and rule["expr"][0].get("jump", {}).get("target") - == ADD_FORWARD_RULE_AFTER_THIS_RULE + 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): @@ -273,8 +281,8 @@ def setup_plain_forwarding_for_address(source: str, enable: bool, family: int) - ( forward_chain, ROUTING_MANAGER_CHAIN_NAME, - is_forward_jump_to_custom_forward, - append_rule_after, + is_oifgroup_2, + insert_rule_before, ), ( postrouting_chain, diff --git a/qubesroutingmanager/fixtures/no_routing_manager.json b/qubesroutingmanager/fixtures/no_routing_manager.json new file mode 100644 index 0000000..5b96c3d --- /dev/null +++ b/qubesroutingmanager/fixtures/no_routing_manager.json @@ -0,0 +1,1290 @@ +{ + "nftables": [ + { + "metainfo": { + "version": "1.0.7", + "release_name": "Old Doc Yak", + "json_schema_version": 1 + } + }, + { + "table": { + "family": "ip", + "name": "qubes", + "handle": 1 + } + }, + { + "set": { + "family": "ip", + "name": "downstream", + "table": "qubes", + "type": "ipv4_addr", + "handle": 3 + } + }, + { + "set": { + "family": "ip", + "name": "allowed", + "table": "qubes", + "type": [ + "ifname", + "ipv4_addr" + ], + "handle": 4 + } + }, + { + "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 + } + }, + { + "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": "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": 0, + "bytes": 0 + } + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "qubes", + "chain": "forward", + "handle": 76, + "expr": [ + { + "jump": { + "target": "custom-forward" + } + } + ] + } + }, + { + "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": 0, + "bytes": 0 + } + }, + { + "drop": null + } + ] + } + }, + { + "table": { + "family": "ip6", + "name": "qubes", + "handle": 2 + } + }, + { + "set": { + "family": "ip6", + "name": "downstream", + "table": "qubes", + "type": "ipv6_addr", + "handle": 3 + } + }, + { + "set": { + "family": "ip6", + "name": "allowed", + "table": "qubes", + "type": [ + "ifname", + "ipv6_addr" + ], + "handle": 4 + } + }, + { + "chain": { + "family": "ip6", + "table": "qubes", + "name": "antispoof", + "handle": 1 + } + }, + { + "chain": { + "family": "ip6", + "table": "qubes", + "name": "prerouting", + "handle": 2, + "type": "filter", + "hook": "prerouting", + "prio": -300, + "policy": "accept" + } + }, + { + "chain": { + "family": "ip6", + "table": "qubes", + "name": "postrouting", + "handle": 19, + "type": "nat", + "hook": "postrouting", + "prio": 100, + "policy": "accept" + } + }, + { + "chain": { + "family": "ip6", + "table": "qubes", + "name": "_icmpv6", + "handle": 20 + } + }, + { + "chain": { + "family": "ip6", + "table": "qubes", + "name": "input", + "handle": 21, + "type": "filter", + "hook": "input", + "prio": 0, + "policy": "drop" + } + }, + { + "chain": { + "family": "ip6", + "table": "qubes", + "name": "forward", + "handle": 22, + "type": "filter", + "hook": "forward", + "prio": 0, + "policy": "accept" + } + }, + { + "chain": { + "family": "ip6", + "table": "qubes", + "name": "custom-input", + "handle": 23 + } + }, + { + "chain": { + "family": "ip6", + "table": "qubes", + "name": "custom-forward", + "handle": 24 + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "antispoof", + "handle": 5, + "expr": [ + { + "match": { + "op": "==", + "left": { + "concat": [ + { + "meta": { + "key": "iifname" + } + }, + { + "payload": { + "protocol": "ip6", + "field": "saddr" + } + } + ] + }, + "right": "@allowed" + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "antispoof", + "handle": 6, + "expr": [ + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "drop": null + } + ] + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "prerouting", + "handle": 7, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "iifgroup" + } + }, + "right": 2 + } + }, + { + "goto": { + "target": "antispoof" + } + } + ] + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "prerouting", + "handle": 8, + "expr": [ + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "ip6", + "field": "saddr" + } + }, + "right": "@downstream" + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "drop": null + } + ] + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "postrouting", + "handle": 25, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "oifgroup" + } + }, + "right": 2 + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "postrouting", + "handle": 26, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "oif" + } + }, + "right": "lo" + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "postrouting", + "handle": 27, + "expr": [ + { + "masquerade": null + } + ] + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "_icmpv6", + "handle": 28, + "expr": [ + { + "match": { + "op": "!=", + "left": { + "meta": { + "key": "l4proto" + } + }, + "right": "ipv6-icmp" + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "reject": { + "type": "icmpv6", + "expr": "admin-prohibited" + } + } + ] + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "_icmpv6", + "handle": 30, + "expr": [ + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "icmpv6", + "field": "type" + } + }, + "right": { + "set": [ + "nd-router-advert", + "nd-redirect" + ] + } + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "drop": null + } + ] + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "_icmpv6", + "handle": 31, + "expr": [ + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "input", + "handle": 32, + "expr": [ + { + "jump": { + "target": "custom-input" + } + } + ] + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "input", + "handle": 33, + "expr": [ + { + "match": { + "op": "in", + "left": { + "ct": { + "key": "state" + } + }, + "right": "invalid" + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "drop": null + } + ] + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "input", + "handle": 34, + "expr": [ + { + "match": { + "op": "in", + "left": { + "ct": { + "key": "state" + } + }, + "right": [ + "established", + "related" + ] + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "input", + "handle": 35, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "iifgroup" + } + }, + "right": 2 + } + }, + { + "goto": { + "target": "_icmpv6" + } + } + ] + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "input", + "handle": 36, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "iif" + } + }, + "right": "lo" + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "input", + "handle": 37, + "expr": [ + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "ip6", + "field": "saddr" + } + }, + "right": { + "prefix": { + "addr": "fe80::", + "len": 64 + } + } + } + }, + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "ip6", + "field": "daddr" + } + }, + "right": { + "prefix": { + "addr": "fe80::", + "len": 64 + } + } + } + }, + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "udp", + "field": "dport" + } + }, + "right": 546 + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "input", + "handle": 38, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "l4proto" + } + }, + "right": "ipv6-icmp" + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "input", + "handle": 39, + "expr": [ + { + "counter": { + "packets": 0, + "bytes": 0 + } + } + ] + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "forward", + "handle": 40, + "expr": [ + { + "jump": { + "target": "custom-forward" + } + } + ] + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "forward", + "handle": 41, + "expr": [ + { + "match": { + "op": "in", + "left": { + "ct": { + "key": "state" + } + }, + "right": "invalid" + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "drop": null + } + ] + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "forward", + "handle": 42, + "expr": [ + { + "match": { + "op": "in", + "left": { + "ct": { + "key": "state" + } + }, + "right": [ + "established", + "related" + ] + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "ip6", + "table": "qubes", + "chain": "forward", + "handle": 43, + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "oifgroup" + } + }, + "right": 2 + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "drop": null + } + ] + } + } + ] +} diff --git a/qubesroutingmanager/test_firewalling.py b/qubesroutingmanager/test_firewalling.py index eb7014f..0b531eb 100644 --- a/qubesroutingmanager/test_firewalling.py +++ b/qubesroutingmanager/test_firewalling.py @@ -50,13 +50,13 @@ def test_partial_add_completes_the_add(): "counter", ], [ - "add", + "insert", "rule", "ip", "qubes", "postrouting", "position", - "66", + "67", "jump", "qubes-routing-manager-postrouting", ], @@ -78,6 +78,79 @@ def test_partial_add_completes_the_add(): 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 = [ diff --git a/qubesroutingmanager/worker.py b/qubesroutingmanager/worker.py index 24e7eb8..ca0f95a 100644 --- a/qubesroutingmanager/worker.py +++ b/qubesroutingmanager/worker.py @@ -14,7 +14,7 @@ import logging import os import socket -import qubesdb +import qubesdb # type: ignore from qubesroutingmanager import setup_plain_forwarding_for_address diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..49d72de --- /dev/null +++ b/tox.ini @@ -0,0 +1,10 @@ +[tox] +envlist = basepython + +[testenv] +deps = + pytest + mypy +commands = + pytest -vv + mypy -p qubesroutingmanager