mirror of
https://github.com/Rudd-O/qubes-network-server.git
synced 2025-03-01 14:22:35 +01:00
245 lines
7.6 KiB
Python
Executable File
245 lines
7.6 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
'''
|
|
This code is intended to replace the very fragile firewall generation code
|
|
that currently runs on dom0, by a lightweight daemon that applies the rules
|
|
on the AppVM (with static IP), responding to rule changes made by the
|
|
administrator on-the-fly.
|
|
|
|
This daemon is injected into the VM as soon as qrexec capability becomes
|
|
available on the recently-started VM. The daemon:
|
|
|
|
1. Reads the QubesDB key /qubes-fortress-iptables-rules.
|
|
2. Atomically applies the rules therein saved therein.
|
|
|
|
The rules in /qubes-fortress-iptables-rules are generated by the dom0 code
|
|
in 007FortressQubesProxyVM, which in turn are based on the firewall rules
|
|
that the administrator has configured. These rules are generated and applied
|
|
at the same time as the rules generated and applied on the ProxyVM attached to
|
|
the AppVM, ensuring that the rules in the VM are kept in sync with the rules
|
|
in the ProxyVM at all times.
|
|
|
|
FIXME: The previous paragraph is still a work in progress.
|
|
'''
|
|
|
|
import collections
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
NAME = "qubes-appvm-firewall"
|
|
UNITDIRS = ["/usr/lib/systemd/system", "/lib/systemd/system"]
|
|
DEPDIR = "/run/fortress"
|
|
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():
|
|
deppath = os.path.join(DEPDIR, NAME)
|
|
if not os.path.isdir(DEPDIR):
|
|
os.makedirs(DEPDIR)
|
|
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
|
|
for unitdir in UNITDIRS:
|
|
if os.path.isdir(unitdir): break
|
|
unitpath = os.path.join(unitdir, NAME + ".service")
|
|
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())
|