From e01f7b97d9e1710d1c4840b7ae18d7e727fa37fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Fri, 9 Sep 2016 03:14:16 +0200 Subject: [PATCH] qubes/vm: plug in new firewall code, create QubesDB entries QubesOS/qubes-issues#1815 --- qubes/firewall.py | 12 +++ qubes/tests/firewall.py | 18 +++++ qubes/vm/__init__.py | 166 ---------------------------------------- qubes/vm/mix/net.py | 33 +++++++- qubes/vm/qubesvm.py | 4 - 5 files changed, 60 insertions(+), 173 deletions(-) diff --git a/qubes/firewall.py b/qubes/firewall.py index aec973f2..16519e5a 100644 --- a/qubes/firewall.py +++ b/qubes/firewall.py @@ -24,6 +24,7 @@ import datetime import subprocess +import itertools import lxml.etree import os import socket @@ -459,6 +460,17 @@ class Firewall(object): self.vm.log.error("save error: {}".format(err)) raise qubes.exc.QubesException('save error: {}'.format(err)) + self.vm.fire_event('firewall-changed') + if expiring_rules_present and not self.vm.app.vmm.offline_mode: subprocess.call(["sudo", "systemctl", "start", "qubes-reload-firewall@%s.timer" % self.vm.name]) + + + def qdb_entries(self): + entries = { + 'policy': str(self.policy) + } + for ruleno, rule in zip(itertools.count(), self.rules): + entries['{:04}'.format(ruleno)] = rule.rule + return entries diff --git a/qubes/tests/firewall.py b/qubes/tests/firewall.py index 8e4be47d..5efd3e04 100644 --- a/qubes/tests/firewall.py +++ b/qubes/tests/firewall.py @@ -532,3 +532,21 @@ class TC_10_Firewall(qubes.tests.QubesTestCase): fw = qubes.firewall.Firewall(self.vm, True) self.assertEqual(fw.rules, rules) + def test_005_qdb_entries(self): + fw = qubes.firewall.Firewall(self.vm, True) + rules = [ + qubes.firewall.Rule(None, action='drop', proto='icmp'), + qubes.firewall.Rule(None, action='drop', proto='tcp', dstports=80), + qubes.firewall.Rule(None, action='accept', proto='udp'), + qubes.firewall.Rule(None, action='accept', specialtarget='dns'), + ] + fw.rules.extend(rules) + fw.policy = qubes.firewall.Action.drop + expected_qdb_entries = { + 'policy': 'drop', + '0000': 'action=drop proto=icmp', + '0001': 'action=drop proto=tcp dstports=80-80', + '0002': 'action=accept proto=udp', + '0003': 'action=accept specialtarget=dns', + } + self.assertEqual(fw.qdb_entries(), expected_qdb_entries) diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index de167770..66542b57 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -273,172 +273,6 @@ class BaseVM(qubes.PropertyHolder): vm=self, prepare_dvm=prepare_dvm) return domain_config - # - # firewall - # SEE:1815 rewrite it, have node under - # and possibly integrate with generic policy framework. - # - - def write_firewall_conf(self, conf): - '''Write firewall config file. - ''' - defaults = self.get_firewall_conf() - expiring_rules_present = False - for item in defaults.keys(): - if item not in conf: - conf[item] = defaults[item] - - root = lxml.etree.Element( - "QubesFirewallRules", - policy=("allow" if conf["allow"] else "deny"), - dns=("allow" if conf["allowDns"] else "deny"), - icmp=("allow" if conf["allowIcmp"] else "deny"), - yumProxy=("allow" if conf["allowYumProxy"] else "deny")) - - for rule in conf["rules"]: - # For backward compatibility - if "proto" not in rule: - if rule["portBegin"] is not None and rule["portBegin"] > 0: - rule["proto"] = "tcp" - else: - rule["proto"] = "any" - element = lxml.etree.Element( - "rule", - address=rule["address"], - proto=str(rule["proto"]), - ) - if rule["netmask"] is not None and rule["netmask"] != 32: - element.set("netmask", str(rule["netmask"])) - if rule.get("portBegin", None) is not None and \ - rule["portBegin"] > 0: - element.set("port", str(rule["portBegin"])) - if rule.get("portEnd", None) is not None and rule["portEnd"] > 0: - element.set("toport", str(rule["portEnd"])) - if "expire" in rule: - element.set("expire", str(rule["expire"])) - expiring_rules_present = True - - root.append(element) - - tree = lxml.etree.ElementTree(root) - - try: - old_umask = os.umask(0o002) - with open(os.path.join(self.dir_path, - self.firewall_conf), 'w') as fd: - tree.write(fd, encoding="UTF-8", pretty_print=True) - fd.close() - os.umask(old_umask) - except EnvironmentError as err: # pylint: disable=broad-except - print >> sys.stderr, "{0}: save error: {1}".format( - os.path.basename(sys.argv[0]), err) - return False - - # Automatically enable/disable 'updates-proxy-setup' service based on - # allowYumProxy - if conf['allowYumProxy']: - self.features['updates-proxy-setup'] = '1' - else: - try: - del self.features['updates-proxy-setup'] - except KeyError: - pass - - if expiring_rules_present: - subprocess.call(["sudo", "systemctl", "start", - "qubes-reload-firewall@%s.timer" % self.name]) - - # SEE:1815 any better idea? some arguments? - self.fire_event('firewall-changed') - - return True - - def has_firewall(self): - ''' Return `True` if there are some vm specific firewall rules set ''' - return os.path.exists(os.path.join(self.dir_path, self.firewall_conf)) - - @staticmethod - def get_firewall_defaults(): - ''' Returns the default firewall rules ''' - return { - 'rules': list(), - 'allow': True, - 'allowDns': True, - 'allowIcmp': True, - 'allowYumProxy': False} - - def get_firewall_conf(self): - ''' Returns the firewall config dictionary ''' - conf = self.get_firewall_defaults() - - try: - tree = lxml.etree.parse(os.path.join(self.dir_path, - self.firewall_conf)) - root = tree.getroot() - - conf["allow"] = (root.get("policy") == "allow") - conf["allowDns"] = (root.get("dns") == "allow") - conf["allowIcmp"] = (root.get("icmp") == "allow") - conf["allowYumProxy"] = (root.get("yumProxy") == "allow") - - for element in root: - rule = {} - attr_list = ("address", "netmask", "proto", "port", "toport", - "expire") - - for attribute in attr_list: - rule[attribute] = element.get(attribute) - - if rule["netmask"] is not None: - rule["netmask"] = int(rule["netmask"]) - else: - rule["netmask"] = 32 - - if rule["port"] is not None: - rule["portBegin"] = int(rule["port"]) - else: - # backward compatibility - rule["portBegin"] = 0 - - # For backward compatibility - if rule["proto"] is None: - if rule["portBegin"] > 0: - rule["proto"] = "tcp" - else: - rule["proto"] = "any" - - if rule["toport"] is not None: - rule["portEnd"] = int(rule["toport"]) - else: - rule["portEnd"] = None - - if rule["expire"] is not None: - rule["expire"] = int(rule["expire"]) - if rule["expire"] <= int(datetime.datetime.now().strftime( - "%s")): - continue - else: - del rule["expire"] - - del rule["port"] - del rule["toport"] - - conf["rules"].append(rule) - - except EnvironmentError as err: # pylint: disable=broad-except - # problem accessing file, like ENOTFOUND, EPERM or sth - # return default config - return conf - - except (xml.parsers.expat.ExpatError, - ValueError, LookupError) as err: - # config is invalid - print("{0}: load error: {1}".format( - os.path.basename(sys.argv[0]), err)) - return None - - return conf - class VMProperty(qubes.property): '''Property that is referring to a VM diff --git a/qubes/vm/mix/net.py b/qubes/vm/mix/net.py index e8c6c90f..c6969c45 100644 --- a/qubes/vm/mix/net.py +++ b/qubes/vm/mix/net.py @@ -24,12 +24,13 @@ # ''' This module contains the NetVMMixin ''' - +import os import re import libvirt # pylint: disable=import-error import qubes import qubes.events +import qubes.firewall import qubes.exc @@ -66,6 +67,9 @@ class NetVMMixin(qubes.events.Emitter): doc='''If this domain can act as network provider (formerly known as NetVM or ProxyVM)''') + firewall_conf = qubes.property('firewall_conf', type=str, + default='firewall.xml') + # # used in networked appvms or proxyvms (netvm is not None) # @@ -136,6 +140,7 @@ class NetVMMixin(qubes.events.Emitter): return None def __init__(self, *args, **kwargs): + self._firewall = None super(NetVMMixin, self).__init__(*args, **kwargs) @qubes.events.handler('domain-start') @@ -256,8 +261,18 @@ class NetVMMixin(qubes.events.Emitter): def reload_firewall_for_vm(self, vm): ''' Reload the firewall rules for the vm ''' - # SEE:1815 - pass + if not self.is_running(): + return + + base_dir = '/qubes-firewall/' + vm.ip + '/' + # remove old entries if any (but don't touch base empty entry - it + # would trigger reload right away + self.qdb.rm(base_dir) + # write new rules + for key, value in vm.firewall.qdb_entries().items(): + self.qdb.write(base_dir + key, value) + # signal its done + self.qdb.write(base_dir[:-1], '') @qubes.events.handler('property-del:netvm') def on_property_del_netvm(self, event, prop, old_netvm=None): @@ -328,3 +343,15 @@ class NetVMMixin(qubes.events.Emitter): # pylint: disable=unused-argument if self.is_running() and self.netvm: self.netvm.reload_firewall_for_vm(self) # pylint: disable=no-member + + # CORE2: swallowed get_firewall_conf, write_firewall_conf, + # get_firewall_defaults + @property + def firewall(self): + if self._firewall is None: + self._firewall = qubes.firewall.Firewall(self) + return self._firewall + + def has_firewall(self): + ''' Return `True` if there are some vm specific firewall rules set ''' + return os.path.exists(os.path.join(self.dir_path, self.firewall_conf)) diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index dc36baab..6d756bba 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -195,10 +195,6 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): doc='''Use full virtualisation (HVM) for this qube, instead of paravirtualisation (PV)''') - # SEE: 1815 this should be part of qubes.xml - firewall_conf = qubes.property('firewall_conf', type=str, - default='firewall.xml') - installed_by_rpm = qubes.property('installed_by_rpm', type=bool, setter=qubes.property.bool, default=False,