qubes/vm: plug in new firewall code, create QubesDB entries
QubesOS/qubes-issues#1815
This commit is contained in:
		
							parent
							
								
									1da75a676f
								
							
						
					
					
						commit
						e01f7b97d9
					
				@ -24,6 +24,7 @@
 | 
				
			|||||||
import datetime
 | 
					import datetime
 | 
				
			||||||
import subprocess
 | 
					import subprocess
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import itertools
 | 
				
			||||||
import lxml.etree
 | 
					import lxml.etree
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import socket
 | 
					import socket
 | 
				
			||||||
@ -459,6 +460,17 @@ class Firewall(object):
 | 
				
			|||||||
            self.vm.log.error("save error: {}".format(err))
 | 
					            self.vm.log.error("save error: {}".format(err))
 | 
				
			||||||
            raise qubes.exc.QubesException('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:
 | 
					        if expiring_rules_present and not self.vm.app.vmm.offline_mode:
 | 
				
			||||||
            subprocess.call(["sudo", "systemctl", "start",
 | 
					            subprocess.call(["sudo", "systemctl", "start",
 | 
				
			||||||
                             "qubes-reload-firewall@%s.timer" % self.vm.name])
 | 
					                             "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
 | 
				
			||||||
 | 
				
			|||||||
@ -532,3 +532,21 @@ class TC_10_Firewall(qubes.tests.QubesTestCase):
 | 
				
			|||||||
        fw = qubes.firewall.Firewall(self.vm, True)
 | 
					        fw = qubes.firewall.Firewall(self.vm, True)
 | 
				
			||||||
        self.assertEqual(fw.rules, rules)
 | 
					        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)
 | 
				
			||||||
 | 
				
			|||||||
@ -273,172 +273,6 @@ class BaseVM(qubes.PropertyHolder):
 | 
				
			|||||||
            vm=self, prepare_dvm=prepare_dvm)
 | 
					            vm=self, prepare_dvm=prepare_dvm)
 | 
				
			||||||
        return domain_config
 | 
					        return domain_config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #
 | 
					 | 
				
			||||||
    # firewall
 | 
					 | 
				
			||||||
    # SEE:1815 rewrite it, have <firewall/> node under <domain/>
 | 
					 | 
				
			||||||
    # 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):
 | 
					class VMProperty(qubes.property):
 | 
				
			||||||
    '''Property that is referring to a VM
 | 
					    '''Property that is referring to a VM
 | 
				
			||||||
 | 
				
			|||||||
@ -24,12 +24,13 @@
 | 
				
			|||||||
#
 | 
					#
 | 
				
			||||||
 | 
					
 | 
				
			||||||
''' This module contains the NetVMMixin '''
 | 
					''' This module contains the NetVMMixin '''
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import libvirt  # pylint: disable=import-error
 | 
					import libvirt  # pylint: disable=import-error
 | 
				
			||||||
import qubes
 | 
					import qubes
 | 
				
			||||||
import qubes.events
 | 
					import qubes.events
 | 
				
			||||||
 | 
					import qubes.firewall
 | 
				
			||||||
import qubes.exc
 | 
					import qubes.exc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -66,6 +67,9 @@ class NetVMMixin(qubes.events.Emitter):
 | 
				
			|||||||
        doc='''If this domain can act as network provider (formerly known as
 | 
					        doc='''If this domain can act as network provider (formerly known as
 | 
				
			||||||
            NetVM or ProxyVM)''')
 | 
					            NetVM or ProxyVM)''')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    firewall_conf = qubes.property('firewall_conf', type=str,
 | 
				
			||||||
 | 
					        default='firewall.xml')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #
 | 
					    #
 | 
				
			||||||
    # used in networked appvms or proxyvms (netvm is not None)
 | 
					    # used in networked appvms or proxyvms (netvm is not None)
 | 
				
			||||||
    #
 | 
					    #
 | 
				
			||||||
@ -136,6 +140,7 @@ class NetVMMixin(qubes.events.Emitter):
 | 
				
			|||||||
            return None
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        self._firewall = None
 | 
				
			||||||
        super(NetVMMixin, self).__init__(*args, **kwargs)
 | 
					        super(NetVMMixin, self).__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @qubes.events.handler('domain-start')
 | 
					    @qubes.events.handler('domain-start')
 | 
				
			||||||
@ -256,8 +261,18 @@ class NetVMMixin(qubes.events.Emitter):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def reload_firewall_for_vm(self, vm):
 | 
					    def reload_firewall_for_vm(self, vm):
 | 
				
			||||||
        ''' Reload the firewall rules for the vm '''
 | 
					        ''' Reload the firewall rules for the vm '''
 | 
				
			||||||
        # SEE:1815
 | 
					        if not self.is_running():
 | 
				
			||||||
        pass
 | 
					            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')
 | 
					    @qubes.events.handler('property-del:netvm')
 | 
				
			||||||
    def on_property_del_netvm(self, event, prop, old_netvm=None):
 | 
					    def on_property_del_netvm(self, event, prop, old_netvm=None):
 | 
				
			||||||
@ -328,3 +343,15 @@ class NetVMMixin(qubes.events.Emitter):
 | 
				
			|||||||
        # pylint: disable=unused-argument
 | 
					        # pylint: disable=unused-argument
 | 
				
			||||||
        if self.is_running() and self.netvm:
 | 
					        if self.is_running() and self.netvm:
 | 
				
			||||||
            self.netvm.reload_firewall_for_vm(self)  # pylint: disable=no-member
 | 
					            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))
 | 
				
			||||||
 | 
				
			|||||||
@ -195,10 +195,6 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
 | 
				
			|||||||
        doc='''Use full virtualisation (HVM) for this qube,
 | 
					        doc='''Use full virtualisation (HVM) for this qube,
 | 
				
			||||||
            instead of paravirtualisation (PV)''')
 | 
					            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',
 | 
					    installed_by_rpm = qubes.property('installed_by_rpm',
 | 
				
			||||||
        type=bool, setter=qubes.property.bool,
 | 
					        type=bool, setter=qubes.property.bool,
 | 
				
			||||||
        default=False,
 | 
					        default=False,
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user