diff --git a/qubesadmin/backup/__init__.py b/qubesadmin/backup/__init__.py index 2d935b7..3dad6cd 100644 --- a/qubesadmin/backup/__init__.py +++ b/qubesadmin/backup/__init__.py @@ -442,6 +442,9 @@ class ExtractWorker3(Process): for fname, (data_func, size_func) in self.handlers.items(): if not fname.startswith(dirname + '/'): continue + if not os.path.exists(fname): + # for example firewall.xml + continue if size_func is not None: size_func(os.path.getsize(fname)) with open(fname, 'rb') as input_file: @@ -741,6 +744,10 @@ class BackupVM(object): '''Report whether a VM is included in the backup''' return False + def handle_firewall_xml(self, vm, stream): + '''Import appropriate format of firewall.xml''' + raise NotImplementedError + class BackupRestoreOptions(object): '''Options for restore operation''' # pylint: disable=too-few-public-methods @@ -1717,6 +1724,15 @@ class BackupRestore(object): if retcode != 0: self.log.error("*** Error while setting home directory owner") + def _handle_appmenus_list(self, vm, stream): + '''Handle whitelisted-appmenus.list file''' + try: + subprocess.check_call( + ['qvm-appmenus', '--set-whitelist=-', vm.name], + stdin=stream) + except subprocess.CalledProcessError: + self.log.exception('Failed to set application list for %s', vm.name) + def restore_do(self, restore_info): ''' @@ -1753,8 +1769,11 @@ class BackupRestore(object): size_func = volume.resize handlers[os.path.join(vm_info.subdir, name + '.img')] = \ (data_func, size_func) - # TODO applications whitelist - # TODO firewall + handlers[os.path.join(vm_info.subdir, 'firewall.xml')] = ( + functools.partial(vm_info.vm.handle_firewall_xml, vm), None) + handlers[os.path.join(vm_info.subdir, + 'whitelisted-appmenus.list')] = ( + functools.partial(self._handle_appmenus_list, vm), None) if 'dom0' in restore_info.keys() and \ restore_info['dom0'].good_to_go: diff --git a/qubesadmin/backup/core2.py b/qubesadmin/backup/core2.py index f21a717..a6326a4 100644 --- a/qubesadmin/backup/core2.py +++ b/qubesadmin/backup/core2.py @@ -25,6 +25,7 @@ import xml.parsers import logging import lxml.etree +from qubesadmin.firewall import Rule, Action, Proto, DstHost, SpecialTarget import qubesadmin.backup service_to_feature = { @@ -44,6 +45,102 @@ class Core2VM(qubesadmin.backup.BackupVM): def included_in_backup(self): return self.backup_content + @staticmethod + def rule_from_xml_v1(node, action): + '''Parse single rule in old XML format (pre Qubes 4.0) + + :param node: XML node for the rule + :param action: action to apply (in old format it wasn't part of the + rule itself) + ''' + netmask = node.get('netmask') + if netmask is None: + netmask = 32 + else: + netmask = int(netmask) + address = node.get('address') + if address: + dsthost = DstHost(address, netmask) + else: + dsthost = None + + proto = node.get('proto') + + port = node.get('port') + toport = node.get('toport') + if port and toport: + dstports = port + '-' + toport + elif port: + dstports = port + else: + dstports = None + + # backward compatibility: protocol defaults to TCP if port is specified + if dstports and not proto: + proto = 'tcp' + + if proto == 'any': + proto = None + + expire = node.get('expire') + + kwargs = { + 'action': action, + } + if dsthost: + kwargs['dsthost'] = dsthost + if dstports: + kwargs['dstports'] = dstports + if proto: + kwargs['proto'] = proto + if expire: + kwargs['expire'] = expire + + return Rule(None, **kwargs) + + + def handle_firewall_xml(self, vm, stream): + '''Load old (Qubes < 4.0) firewall XML format''' + try: + tree = lxml.etree.parse(stream) # pylint: disable=no-member + xml_root = tree.getroot() + policy_v1 = xml_root.get('policy') + assert policy_v1 in ('allow', 'deny') + default_policy_is_accept = (policy_v1 == 'allow') + rules = [] + + def _translate_action(key): + '''Translate action name''' + if xml_root.get(key, policy_v1) == 'allow': + return Action.accept + return Action.drop + + rules.append(Rule(None, + action=_translate_action('dns'), + specialtarget=SpecialTarget('dns'))) + + rules.append(Rule(None, + action=_translate_action('icmp'), + proto=Proto.icmp)) + + if default_policy_is_accept: + rule_action = Action.drop + else: + rule_action = Action.accept + + for element in xml_root: + rule = self.rule_from_xml_v1(element, rule_action) + rules.append(rule) + if default_policy_is_accept: + rules.append(Rule(None, action='accept')) + else: + rules.append(Rule(None, action='drop')) + + vm.firewall.rules = rules + except: # pylint: disable=bare-except + vm.log.exception('Failed to set firewall') + + class Core2Qubes(qubesadmin.backup.BackupApp): '''Parsed qubes.xml''' def __init__(self, store=None): diff --git a/qubesadmin/backup/core3.py b/qubesadmin/backup/core3.py index a975d00..7a32ca8 100644 --- a/qubesadmin/backup/core3.py +++ b/qubesadmin/backup/core3.py @@ -25,6 +25,7 @@ import logging import lxml.etree import qubesadmin.backup +import qubesadmin.firewall class Core3VM(qubesadmin.backup.BackupVM): '''VM object''' @@ -33,6 +34,22 @@ class Core3VM(qubesadmin.backup.BackupVM): def included_in_backup(self): return self.backup_path is not None + def handle_firewall_xml(self, vm, stream): + '''Load new (Qubes >= 4.0) firewall XML format''' + try: + tree = lxml.etree.parse(stream) # pylint: disable=no-member + xml_root = tree.getroot() + rules = [] + for rule_node in xml_root.findall('./rules/rule'): + rule_opts = {} + for rule_opt in rule_node.findall('./properties/property'): + rule_opts[rule_opt.get('name')] = rule_opt.text + rules.append(qubesadmin.firewall.Rule(None, **rule_opts)) + + vm.firewall.rules = rules + except: # pylint: disable=bare-except + vm.log.exception('Failed to set firewall') + class Core3Qubes(qubesadmin.backup.BackupApp): '''Parsed qubes.xml''' def __init__(self, store=None):