diff --git a/qubes/firewall.py b/qubes/firewall.py index b3b0a3af3..ef2904614 100644 --- a/qubes/firewall.py +++ b/qubes/firewall.py @@ -76,12 +76,22 @@ def __init__(self, untrusted_value): class Action(RuleChoice): accept = 'accept' drop = 'drop' + forward = 'forward' @property def rule(self): return 'action=' + str(self) +class ForwardType(RuleChoice): + external = 'external' + internal = 'internal' + + @property + def rule(self): + return 'forwardtype=' + str(self) + + class Proto(RuleChoice): tcp = 'tcp' udp = 'udp' @@ -92,7 +102,7 @@ def rule(self): return 'proto=' + str(self) -class DstHost(RuleOption): +class Host(RuleOption): '''Represent host/network address: either IPv4, IPv6, or DNS name''' def __init__(self, untrusted_value, prefixlen=None): if untrusted_value.count('/') > 1: @@ -107,7 +117,7 @@ def __init__(self, untrusted_value, prefixlen=None): raise ValueError( 'netmask for IPv6 must be between 0 and 128') value += '/' + str(self.prefixlen) - self.type = 'dst6' + self.type = '6' except socket.error: try: socket.inet_pton(socket.AF_INET, untrusted_value) @@ -120,9 +130,9 @@ def __init__(self, untrusted_value, prefixlen=None): raise ValueError( 'netmask for IPv4 must be between 0 and 32') value += '/' + str(self.prefixlen) - self.type = 'dst4' + self.type = '4' except socket.error: - self.type = 'dsthost' + self.type = 'host' self.prefixlen = 0 safe_set = string.ascii_lowercase + string.digits + '-._' if not all(c in safe_set for c in untrusted_value): @@ -139,13 +149,13 @@ def __init__(self, untrusted_value, prefixlen=None): value = untrusted_value if prefixlen > 128: raise ValueError('netmask for IPv6 must be <= 128') - self.type = 'dst6' + self.type = '6' except socket.error: try: socket.inet_pton(socket.AF_INET, untrusted_host) if prefixlen > 32: raise ValueError('netmask for IPv4 must be <= 32') - self.type = 'dst4' + self.type = '4' if untrusted_host.count('.') != 3: raise ValueError( 'Invalid number of dots in IPv4 address') @@ -155,12 +165,20 @@ def __init__(self, untrusted_value, prefixlen=None): super().__init__(value) + +class DstHost(Host): + @property def rule(self): - return self.type + '=' + str(self) + return 'dst' + self.type + '=' + str(self) +class SrcHost(Host): + + @property + def rule(self): + return 'src' + self.type + '=' + str(self) -class DstPorts(RuleOption): +class Ports(RuleOption): def __init__(self, untrusted_value): if isinstance(untrusted_value, int): untrusted_value = str(untrusted_value) @@ -178,11 +196,19 @@ def __init__(self, untrusted_value): str(self.range[0]) if self.range[0] == self.range[1] else '-'.join(map(str, self.range))) + +class DstPorts(Ports): @property def rule(self): return 'dstports=' + '{!s}-{!s}'.format(*self.range) +class SrcPorts(Ports): + @property + def rule(self): + return 'srcports=' + '{!s}-{!s}'.format(*self.range) + + class IcmpType(RuleOption): def __init__(self, untrusted_value): untrusted_value = int(untrusted_value) @@ -259,13 +285,33 @@ def __init__(self, xml=None, **kwargs): if self.icmptype: self.on_set_icmptype('property-set:icmptype', 'icmptype', self.icmptype, None) + # dependencies for forwarding + if self.forwardtype: + self.on_set_forwardtype('property-set:forwardtype', 'forwardtype', + self.forwardtype, None) + if self.srcports: + self.on_set_srcports('property-set:srcports', 'srcports', + self.srcports, None) + if self.srchost: + self.on_set_srcports('property-set:srchost', 'srchost', + self.srcports, None) self.property_require('action', False, True) + if self.action is 'forward': + self.property_require('forwardtype', False, True) + self.property_require('srcports', False, True) + self.property_require('srchost', False, True) action = qubes.property('action', type=Action, order=0, doc='rule action') + forwardtype = qubes.property('forwardtype', + type=ForwardType, + default=None, + order=1, + doc='forwarding type (\'internal\' or \'external\')') + proto = qubes.property('proto', type=Proto, default=None, @@ -278,6 +324,18 @@ def __init__(self, xml=None, **kwargs): order=1, doc='destination host/network') + srchost = qubes.property('srchost', + type=SrcHost, + default=None, + order=2, + doc='allowed inbound hosts for connections (for forwarding only)') + + srcports = qubes.property('srcports', + type=SrcPorts, + default=None, + order=2, + doc='Inbound port(s) (for forwarding only)') + dstports = qubes.property('dstports', type=DstPorts, default=None, @@ -307,6 +365,13 @@ def __init__(self, xml=None, **kwargs): doc='User comment') # noinspection PyUnusedLocal + @qubes.events.handler('property-pre-set:dsthost') + def on_set_dsthost(self, event, name, newvalue, oldvalue=None): + # pylint: disable=unused-argument + if self.action not in ('accept', 'drop'): + raise ValueError( + 'dsthost valid only for \'accept\' and \'drop\' action') + @qubes.events.handler('property-pre-set:dstports') def on_set_dstports(self, event, name, newvalue, oldvalue=None): # pylint: disable=unused-argument @@ -330,6 +395,24 @@ def on_set_proto(self, event, name, newvalue, oldvalue=None): if newvalue not in ('icmp',): self.icmptype = qubes.property.DEFAULT + @qubes.events.handler('property-set:forwardtype') + def on_set_forwardtype(self, event, name, newvalue, oldvalue=None): + if self.action != 'forward': + raise ValueError( + 'forwardtype valid only for forward action') + + @qubes.events.handler('property-set:srcports') + def on_set_srcports(self, event, name, newvalue, oldvalue=None): + if self.action != 'forward': + raise ValueError( + 'srcports valid only for forward action') + + @qubes.events.handler('property-set:srchost') + def on_set_srchost(self, event, name, newvalue, oldvalue=None): + if self.action != 'forward': + raise ValueError( + 'srchost valid only for forward action') + @qubes.events.handler('property-reset:proto') def on_reset_proto(self, event, name, oldvalue): # pylint: disable=unused-argument @@ -438,8 +521,13 @@ def from_api_string(cls, untrusted_rule): raise ValueError('Option \'{}\' already set'.format( 'dsthost')) kwargs['dsthost'] = DstHost(untrusted_value=untrusted_value) + elif untrusted_key in ('src4', 'src6'): + if 'srchost' in kwargs: + raise ValueError('Option \'{}\' already set'.format( + 'srchost')) + kwargs['srchost'] = SrcHost(untrusted_value=untrusted_value) else: - raise ValueError('Unknown firewall option') + raise ValueError('Unknown firewall option {}'.format(untrusted_option)) return cls(**kwargs) @@ -608,11 +696,37 @@ def qdb_entries(self, addr_family=None): exclude_dsttype = None if addr_family is not None: exclude_dsttype = 'dst4' if addr_family == 6 else 'dst6' - for ruleno, rule in zip(itertools.count(), self.rules): + for ruleno, rule in zip(itertools.count(), + filter(lambda x: (x.action != "forward"), self.rules)): if rule.expire and rule.expire.expired: continue # exclude rules for another address family if rule.dsthost and rule.dsthost.type == exclude_dsttype: continue + # exclude forwarding rules, managed separately + if rule.action == "forward": + continue entries['{:04}'.format(ruleno)] = rule.rule return entries + + def qdb_forward_entries(self, addr_family=None): + ''' In order to keep all the 'parsing' logic here and not in net.py, + directly separate forwarding rules from standard rules since they need + to be handled differently later. + ''' + ''' + TODO: missing correct src6/dst4 handling + ''' + entries = {} + if addr_family is not None: + exclude_dsttype = 'dst4' if addr_family == 6 else 'dst6' + exclude_srctype = 'src4' if addr_family == 6 else 'src6' + for ruleno, rule in zip(itertools.count(), + filter(lambda x: (x.action == "forward"), self.rules)): + if rule.expire and rule.expire.expired: + continue + # exclude rules for another address family + if rule.dsthost and rule.dsthost.type == exclude_dsttype: + continue + entries['{:04}:{}'.format(ruleno, rule.forwardtype)] = rule.rule + return entries \ No newline at end of file diff --git a/qubes/vm/mix/net.py b/qubes/vm/mix/net.py index 7919bd1bd..c67dedd49 100644 --- a/qubes/vm/mix/net.py +++ b/qubes/vm/mix/net.py @@ -360,11 +360,26 @@ def is_networked(self): return self.netvm is not None + def resolve_netpath(self): + '''This VM does not have a network path since it has no netvm''' + if self.netvm is None: + return + + '''Recursively resolve netvm until no netvm is set, order is important''' + netpath = list() + netvm = self + while netvm: + netpath.append(netvm) + netvm = netvm.netvm + return netpath + def reload_firewall_for_vm(self, vm): ''' Reload the firewall rules for the vm ''' if not self.is_running(): return + netpath = self.resolve_netpath() + for addr_family in (4, 6): ip = vm.ip6 if addr_family == 6 else vm.ip if ip is None: @@ -373,13 +388,44 @@ def reload_firewall_for_vm(self, vm): # remove old entries if any (but don't touch base empty entry - it # would trigger reload right away self.untrusted_qdb.rm(base_dir) - # write new rules + + # begin write new accept/drop rules for key, value in vm.firewall.qdb_entries( addr_family=addr_family).items(): self.untrusted_qdb.write(base_dir + key, value) + # signal its done self.untrusted_qdb.write(base_dir[:-1], '') + # begin write new forward rules + #clean + if netpath: + for netvm in netpath: + base_dir = '/qubes-firewall-forward/{}/'.format(vm.name) + netvm.untrusted_qdb.rm(base_dir) + + for key, value in vm.firewall.qdb_forward_entries( + addr_family=addr_family).items(): + forwardtype = key.split(":")[1] + key = key.split(":")[0] + if forwardtype == "internal": + base_dir = '/qubes-firewall-forward/{}/{}/'.format(vm.name, ip) + self.untrusted_qdb.write(base_dir + key, value) + self.untrusted_qdb.write(base_dir[:-1], '') + elif forwardtype == "external": + current_ip = ip + for i, netvm in enumerate(netpath): + base_dir = '/qubes-firewall-forward/{}/{}/'.format(vm.name, current_ip) + if i == len(netpath)-1: + value += ' last=1' + netvm.untrusted_qdb.write(base_dir + key, value) + current_ip = netvm.ip + netvm.untrusted_qdb.write(base_dir[:-1], '') + else: + raise ValueError('Invalid forwardtype') + # end forward rules + + def set_mapped_ip_info_for_vm(self, vm): ''' Set configuration to possibly hide real IP from the VM.