qubes/vm: plug in new firewall code, create QubesDB entries

QubesOS/qubes-issues#1815
This commit is contained in:
Marek Marczykowski-Górecki 2016-09-09 03:14:16 +02:00
parent 1da75a676f
commit e01f7b97d9
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
5 changed files with 60 additions and 173 deletions

View File

@ -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

View File

@ -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)

View File

@ -273,172 +273,6 @@ class BaseVM(qubes.PropertyHolder):
vm=self, prepare_dvm=prepare_dvm)
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):
'''Property that is referring to a VM

View File

@ -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))

View File

@ -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,