Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Automatic Port Forwarding [core-admin] #434

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
134 changes: 124 additions & 10 deletions qubes/firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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:
Expand All @@ -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)
Expand All @@ -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):
Expand All @@ -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')
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
48 changes: 47 additions & 1 deletion qubes/vm/mix/net.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
Expand Down