mirror of
https://github.com/Rudd-O/qubes-network-server.git
synced 2025-03-01 14:22:35 +01:00
Qubes 4.2 support.
This commit is contained in:
parent
06c5b1b0ae
commit
cf2945e742
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,3 +9,4 @@ pkgs/
|
|||||||
build
|
build
|
||||||
*.egg-info
|
*.egg-info
|
||||||
src/*.service
|
src/*.service
|
||||||
|
.mypy_cache
|
||||||
|
5
Makefile
5
Makefile
@ -16,7 +16,7 @@ ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
|||||||
clean:
|
clean:
|
||||||
cd $(ROOT_DIR) || exit $$? ; find -name '*.pyc' -o -name '*~' -print0 | xargs -0 rm -f
|
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 *.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
|
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 ; }
|
@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
|
cd $(ROOT_DIR) ; mv -f builddir.rpm/*/* . && rm -rf builddir.rpm
|
||||||
|
|
||||||
install-template: all
|
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)/
|
install -Dm 755 src/qubes-routing-manager -t $(DESTDIR)/$(SBINDIR)/
|
||||||
sed -i "s,^#!.*,#!$(PYTHON)," $(DESTDIR)/$(SBINDIR)/qubes-routing-manager
|
sed -i "s,^#!.*,#!$(PYTHON)," $(DESTDIR)/$(SBINDIR)/qubes-routing-manager
|
||||||
install -Dm 644 src/qubes-routing-manager.service -t $(DESTDIR)/$(UNITDIR)/
|
install -Dm 644 src/qubes-routing-manager.service -t $(DESTDIR)/$(UNITDIR)/
|
||||||
|
|
||||||
# Python 3 is always used for Qubes admin package.
|
# Python 3 is always used for Qubes admin package.
|
||||||
install-dom0:
|
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
|
install: install-dom0 install-template
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
# Qubes network server
|
# 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
|
a network server, enjoying all the benefits of Qubes OS (isolation, secure
|
||||||
inter-VM process communication, ease of use) with none of the drawbacks
|
inter-VM process communication, ease of use) with none of the drawbacks
|
||||||
of setting up your own Xen server.
|
of setting up your own Xen server.
|
||||||
|
|
||||||
This release is only intended for use with Qubes OS 4.1. Older Qubes OS releases
|
This release is only intended for use with Qubes OS 4.2. Older Qubes OS releases
|
||||||
will not support it. For Qubes OS 4.0, check branch `r4.0`.
|
will not support it. For Qubes OS 4.1, check branch `r4.1`.
|
||||||
|
|
||||||
## Why?
|
## Why?
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
["RELEASE": "q4.1 37"]
|
["RELEASE": "q4.2 37 38 39"]
|
||||||
|
26
networkserversetup.py
Normal file
26
networkserversetup.py
Normal 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",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
@ -3,7 +3,7 @@
|
|||||||
%define mybuildnumber %{?build_number}%{?!build_number:1}
|
%define mybuildnumber %{?build_number}%{?!build_number:1}
|
||||||
|
|
||||||
Name: qubes-network-server
|
Name: qubes-network-server
|
||||||
Version: 0.0.19
|
Version: 0.1.0
|
||||||
Release: %{mybuildnumber}%{?dist}
|
Release: %{mybuildnumber}%{?dist}
|
||||||
Summary: Turn your Qubes OS into a network server
|
Summary: Turn your Qubes OS into a network server
|
||||||
BuildArch: noarch
|
BuildArch: noarch
|
||||||
@ -20,9 +20,10 @@ BuildRequires: python3
|
|||||||
BuildRequires: python3-rpm-macros
|
BuildRequires: python3-rpm-macros
|
||||||
BuildRequires: systemd-rpm-macros
|
BuildRequires: systemd-rpm-macros
|
||||||
|
|
||||||
Requires: qubes-core-agent-networking >= 4.1
|
Requires: qubes-core-agent-networking >= 4.2
|
||||||
Requires: python3
|
Requires: python3
|
||||||
Requires: python3-qubesdb
|
Requires: python3-qubesdb
|
||||||
|
Requires: nftables
|
||||||
|
|
||||||
%description
|
%description
|
||||||
This package lets you turn your Qubes OS into a network server. Install this
|
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
|
BuildRequires: python3-setuptools
|
||||||
|
|
||||||
Requires: python3
|
Requires: python3
|
||||||
Requires: qubes-core-dom0 >= 4.1
|
Requires: qubes-core-dom0 >= 4.2
|
||||||
|
|
||||||
%description -n qubes-core-admin-addon-network-server
|
%description -n qubes-core-admin-addon-network-server
|
||||||
This package lets you turn your Qubes OS into a network server. Install this
|
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
|
%files
|
||||||
%attr(0755, root, root) %{_sbindir}/qubes-routing-manager
|
%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
|
%attr(0644, root, root) %{_presetdir}/75-%{name}.preset
|
||||||
%config %attr(0644, root, root) %{_unitdir}/qubes-routing-manager.service
|
%config %attr(0644, root, root) %{_unitdir}/qubes-routing-manager.service
|
||||||
%doc README.md TODO
|
%doc README.md TODO
|
||||||
|
310
qubesroutingmanager/__init__.py
Normal file
310
qubesroutingmanager/__init__.py
Normal file
@ -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"])
|
1578
qubesroutingmanager/test_firewalling.py
Normal file
1578
qubesroutingmanager/test_firewalling.py
Normal file
File diff suppressed because it is too large
Load Diff
135
qubesroutingmanager/worker.py
Normal file
135
qubesroutingmanager/worker.py
Normal 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
|
||||||
|
|
||||||
|
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
21
routingmanagersetup.py
Normal 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",),
|
||||||
|
)
|
24
setup.py
24
setup.py
@ -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',
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
@ -1,265 +1,8 @@
|
|||||||
#!/usr/bin/python3
|
#!/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__":
|
if __name__ == "__main__":
|
||||||
|
from qubesroutingmanager.worker import AdjunctWorker
|
||||||
|
|
||||||
w = AdjunctWorker()
|
w = AdjunctWorker()
|
||||||
w.main()
|
w.main()
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Configure the network to allow network server VMs
|
Description=Configure the network to allow network server VMs
|
||||||
Documentation=https://github.com/Rudd-O/qubes-network-server
|
Documentation=https://github.com/Rudd-O/qubes-network-server
|
||||||
ConditionPathExists=/var/run/qubes-service/qubes-firewall
|
After=qubes-iptables.service
|
||||||
After=qubes-firewall.service
|
BindsTo=qubes-iptables.service
|
||||||
BindsTo=qubes-firewall.service
|
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=notify
|
Type=notify
|
||||||
ExecStart=@SBINDIR@/qubes-routing-manager
|
ExecStart=@SBINDIR@/qubes-routing-manager
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=qubes-iptables.service
|
Loading…
x
Reference in New Issue
Block a user