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