#!/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()