diff --git a/Makefile b/Makefile index ff2ea57..bde700d 100644 --- a/Makefile +++ b/Makefile @@ -20,3 +20,4 @@ srpm: dist install: install -Dm 755 src/usr/bin/qvm-static-ip -t $(DESTDIR)/$(BINDIR)/ install -Dm 644 src/usr/lib64/python2.7/site-packages/qubes/modules/*.py -t $(DESTDIR)/$(LIBDIR)/python2.7/site-packages/qubes/modules + install -Dm 644 src/usr/lib64/python2.7/site-packages/qubes/modules/qubes-appvm-firewall -t $(DESTDIR)/$(LIBDIR)/python2.7/site-packages/qubes/modules diff --git a/qubes-network-server.spec b/qubes-network-server.spec index f877920..7ec6c35 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.6 +Version: 0.0.7 Release: %{mybuildnumber}%{?dist} Summary: Turn your Qubes OS into a network server BuildArch: noarch @@ -38,6 +38,7 @@ make install DESTDIR=$RPM_BUILD_ROOT BINDIR=%{_bindir} LIBDIR=%{_libdir} %files %attr(0755, root, root) %{_bindir}/qvm-static-ip %attr(0644, root, root) %{_libdir}/python2.7/site-packages/qubes/modules/*.py* +%attr(0644, root, root) %{_libdir}/python2.7/site-packages/qubes/modules/qubes-appvm-firewall %doc README.md TODO %changelog diff --git a/src/usr/lib64/python2.7/site-packages/qubes/modules/001FortressQubesVm.py b/src/usr/lib64/python2.7/site-packages/qubes/modules/001FortressQubesVm.py index 218a040..f319f00 100644 --- a/src/usr/lib64/python2.7/site-packages/qubes/modules/001FortressQubesVm.py +++ b/src/usr/lib64/python2.7/site-packages/qubes/modules/001FortressQubesVm.py @@ -104,15 +104,13 @@ class QubesVm(OriginalQubesVm): else: return None - def start(self, verbose = False, preparing_dvm = False, start_guid = True, - notify_function = None, mem_required = None): - if dry_run: - return - xid = OriginalQubesVm.start(self, verbose, preparing_dvm, start_guid, notify_function, mem_required) - if not preparing_dvm: - self.adjust_proxy_arp(verbose=verbose, notify_function=notify_function) - self.adjust_own_firewall_rules() - return xid + def start_qrexec_daemon(self, verbose=False, notify_function=None): + ret = OriginalQubesVm.start_qrexec_daemon(self, verbose=verbose, notify_function=notify_function) + if self.type not in ['AppVM', 'HVM']: + self.deploy_appvm_firewall(verbose=verbose, notify_function=notify_function) + self.adjust_proxy_arp(verbose=verbose, notify_function=notify_function) + self.adjust_own_firewall_rules() + return ret def unpause(self): self.log.debug('unpause()') @@ -397,4 +395,42 @@ class QubesVm(OriginalQubesVm): finally: f.close() + def deploy_appvm_firewall(self, verbose = False, notify_function=None): + def n(msg): + if notify_function: + notify_function("info", msg) + elif verbose: + print >> sys.stderr, "-->", msg + + n("Deploying AppVM firewall...") + + appvm_firewall_path = os.path.join( + os.path.dirname(__file__), + "qubes-appvm-firewall" + ) + pill = textwrap.dedent( + """ + set -e + tmp=$(mktemp) + trap 'rm -f "$tmp"' EXIT + cat > "$tmp" << "EOF" + %s + EOF + chmod +x "$tmp" + "$tmp" deploy + """ + ) % open(appvm_firewall_path).read() + + try: + p = self.run("bash", user="root", gui=False, wait=True, passio_popen=True, autostart=False) + p.stdin.write(pill) + p.stdin.close() + out = p.stdout.read() + retcode = p.wait() + except Exception as e: + n("Could not deploy the AppVM firewall on the VM: %s" % e) + if retcode != 0: + n("Could not deploy the AppVM firewall on the VM (return status %s): %s" % (retcode, out)) + + register_qubes_vm_class(QubesVm) diff --git a/src/usr/lib64/python2.7/site-packages/qubes/modules/qubes-appvm-firewall b/src/usr/lib64/python2.7/site-packages/qubes/modules/qubes-appvm-firewall new file mode 100755 index 0000000..c2d9363 --- /dev/null +++ b/src/usr/lib64/python2.7/site-packages/qubes/modules/qubes-appvm-firewall @@ -0,0 +1,217 @@ +#!/usr/bin/env python + +import collections +import logging +import os +import shutil +import subprocess +import sys + +UNITPATH = "/usr/lib/systemd/system/qubes-appvm-firewall.service" +DEPPATH = "/run/fortress/qubes-appvm-firewall" +KEY = '/qubes-fortress-iptables-rules' +CHAIN = 'FORTRESS-INPUT' + + +class ReadError(Exception): + pass + + +def watch(key): + subprocess.check_call(['qubesdb-watch', key]) + + +def read(key): + try: + return subprocess.check_output(['qubesdb-read', '-r', key]) + except subprocess.CalledProcessError as e: + logging.error("error reading key %s: %s", key, e) + raise ReadError() + + +class Table(object): + + header = None + chains = None + rules = None + footer = None + original_chains = None + original_rules = None + + def __init__(self, text): + lines = text.splitlines(True) + self.header = '' + self.chains = collections.OrderedDict() + self.rules = [] + self.footer = '' + mode = "header" + for line in lines: + if mode == "header": + if line.startswith(":"): + self.chains.update([line[1:].split(" ", 1)]) + mode = "chains" + else: + self.header += line + elif mode == "chains": + if line.startswith("-"): + self.rules.append(line) + mode = "rules" + else: + self.chains.update([line[1:].split(" ", 1)]) + elif mode == "rules": + if line.startswith("COMMIT"): + self.footer += line + mode = "footer" + else: + self.rules.append(line) + else: # mode == "footer": + self.footer += line + self.original_chains = collections.OrderedDict(self.chains.items()) + self.original_rules = list(self.rules) + + def __str__(self): + return self.render() + + def render(self, old=False): + if old: + chains = self.original_chains + rules = self.original_rules + else: + chains = self.chains + rules = self.rules + return ( + self.header + + "".join(":%s %s" % x for x in chains.items()) + + "".join(rules) + + self.footer + ) + + def dirty(self): + return self.render() != self.render(True) + + def ensure_chain_present(self, name): + if name not in self.chains: + logging.info("Adding chain %s", name) + self.chains[name] = '- [0:0]\n' + + def clear_chain(self, name): + for n, rule in reversed(list(enumerate(self.rules))): + if rule.startswith("-A %s " % name): + self.rules.pop(n) + + def add_rule(self, rule, after_rule): + original_after_rule = after_rule + if not rule.endswith("\n"): + rule += "\n" + if not after_rule.endswith("\n"): + after_rule += "\n" + inserted = False + if rule in self.rules: + return + for n, exrule in enumerate(self.rules): + if exrule == after_rule: + logging.info("Inserting rule %s", rule.strip()) + self.rules.insert(n + 1, rule) + inserted = True + break + if not inserted: + logging.error("Could not insert rule %s", rule.strip()) + raise KeyError(original_after_rule) + + def replace_rules(self, chain, ruletext): + for rule in ruletext.splitlines(): + if not rule.strip(): continue + if not rule.startswith("-A %s " % chain): + raise ValueError( + "rule %s is not for chain %s" % ( + rule.strip(), + chain, + ) + ) + self.ensure_chain_present(chain) + self.clear_chain(chain) + for rule in ruletext.splitlines(): + if rule.startswith("-A %s " % chain): + self.rules.append(rule + "\n") + + def commit(self): + if not self.dirty(): + return + text = self.render() + cmd = ['iptables-restore'] + p = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + out, _ = p.communicate(text) + w = p.wait() + if w != 0: + logging.error("Rule changes commit failed with status %s: %s", w, out) + raise subprocess.CalledProcessError(w, cmd, out) + self.original_chains = collections.OrderedDict(self.chains.items()) + self.original_rules = list(self.rules) + logging.info("Rule changes committed") + + @classmethod + def filter_from_iptables(klass): + r = subprocess.check_output(['iptables-save', '-t', 'filter']) + t = klass(r) + return t + +def deploy(): + if not os.path.isdir(os.path.dirname(DEPPATH)): + os.makedirs(os.path.dirname(DEPPATH)) + shutil.copyfile(__file__, DEPPATH) + os.chmod(DEPPATH, 0755) + service = '''[Unit] +Description=Qubes AppVM firewall updater +After=qubes-iptables.service qubes-firewall.service +Before=qubes-network.service network.target + +[Service] +Type=simple +ExecStart=%s main +''' % DEPPATH + if not os.path.isfile(UNITPATH) or open(UNITPATH, "rb").read() != service: + open(UNITPATH, "wb").write(service) + subprocess.check_call(['systemctl', '--system', 'daemon-reload']) + subprocess.check_call(['systemctl', 'restart', os.path.basename(UNITPATH)]) + + +def main(): + logging.basicConfig(level=logging.INFO) + t = Table.filter_from_iptables() + t.ensure_chain_present(CHAIN) + t.add_rule('-A INPUT -j %s' % CHAIN, '-A INPUT -i lo -j ACCEPT') + try: + newrules = read(KEY) + t.replace_rules(CHAIN, newrules) + except ReadError: + # Key may not exist at this time. + logging.warning("Qubes DB key %s does not yet exist", KEY) + t.commit() + logging.info("Startup complete") + while True: + watch(KEY) + try: + newrules = read(KEY) + except ReadError: + # Key may have been deleted. + logging.warning("Qubes DB key %s could not be read", KEY) + continue + logging.info("Rule changes detected") + try: + t.replace_rules(CHAIN, newrules) + t.commit() + except Exception: + logging.exception("Rule changes could not be committed") + + +try: + cmd = sys.argv[1] +except IndexError: + cmd = 'main' +cmd = locals()[cmd] +sys.exit(cmd())