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