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 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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user