Skip to content

Commit

Permalink
qubes/vm: plug in new firewall code, create QubesDB entries
Browse files Browse the repository at this point in the history
  • Loading branch information
marmarek committed Sep 12, 2016
1 parent 85bc8b8 commit 7bc4100
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 173 deletions.
12 changes: 12 additions & 0 deletions qubes/firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import datetime
import subprocess

import itertools
import lxml.etree
import os
import socket
Expand Down Expand Up @@ -459,6 +460,17 @@ def save(self):
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
18 changes: 18 additions & 0 deletions qubes/tests/firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,3 +532,21 @@ def test_004_save_skip_expired(self):
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)
166 changes: 0 additions & 166 deletions qubes/vm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,172 +273,6 @@ def create_config_file(self, prepare_dvm=False):
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
Expand Down
33 changes: 30 additions & 3 deletions qubes/vm/mix/net.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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)
#
Expand Down Expand Up @@ -136,6 +140,7 @@ def dns(self):
return None

def __init__(self, *args, **kwargs):
self._firewall = None
super(NetVMMixin, self).__init__(*args, **kwargs)

@qubes.events.handler('domain-start')
Expand Down Expand Up @@ -256,8 +261,18 @@ def cleanup_vifs(self):

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):
Expand Down Expand Up @@ -328,3 +343,15 @@ def on_firewall_changed(self, event, **kwargs):
# 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))
4 changes: 0 additions & 4 deletions qubes/vm/qubesvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 7bc4100

Please sign in to comment.