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-client] #190

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 92 additions & 14 deletions qubesadmin/firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,23 @@ class Action(RuleChoice):
'''Rule action'''
accept = 'accept'
drop = 'drop'
forward = 'forward'

@property
def rule(self):
'''API representation of this rule element'''
return 'action=' + str(self)


class ForwardType(RuleChoice):
external = 'external'
internal = 'internal'

@property
def rule(self):
return 'forwardtype=' + str(self)


class Proto(RuleChoice):
'''Protocol name'''
tcp = 'tcp'
Expand All @@ -85,7 +95,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, value, prefixlen=None):
# TODO: in python >= 3.3 ipaddress module could be used
Expand All @@ -103,7 +113,7 @@ def __init__(self, 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, value)
Expand All @@ -118,9 +128,9 @@ def __init__(self, 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 value):
Expand All @@ -135,13 +145,13 @@ def __init__(self, value, prefixlen=None):
socket.inet_pton(socket.AF_INET6, host)
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, host)
if prefixlen > 32:
raise ValueError('netmask for IPv4 must be <= 32')
self.type = 'dst4'
self.type = '4'
if host.count('.') != 3:
raise ValueError(
'Invalid number of dots in IPv4 address')
Expand All @@ -150,17 +160,33 @@ def __init__(self, value, prefixlen=None):

super().__init__(value)


class DstHost(Host):

@property
def rule(self):
'''API representation of this rule element'''
if self.prefixlen == 0 and self.type != 'dsthost':
if self.prefixlen == 0 and self.type != 'host':
# 0.0.0.0/0 or ::/0, doesn't limit to any particular host,
# so skip it
return None
return self.type + '=' + str(self)
return 'dst' + self.type + '=' + str(self)


class DstPorts(RuleOption):
class SrcHost(Host):

@property
def rule(self):
'''API representation of this rule element'''
if self.prefixlen == 0 or self.type == 'host':
# drop both hostname based source verification and wildcard /0
# TODO: discuss wether these defaults are too limiting, however since exposing a
# port/service poses a greater threat,
raise ValueError("srchost cannot neither be an hostname nor contain the /0 prefix")
return 'src' + self.type + '=' + str(self)


class Ports(RuleOption):
'''Destination port(s), for TCP/UDP only'''
def __init__(self, value):
if isinstance(value, int):
Expand All @@ -179,11 +205,19 @@ def __init__(self, value):
str(self.range[0]) if self.range[0] == self.range[1]
else '{!s}-{!s}'.format(*self.range))

class DstPorts(Ports):

@property
def rule(self):
'''API representation of this rule element'''
return 'dstports=' + '{!s}-{!s}'.format(*self.range)

class SrcPorts(Ports):

@property
def rule(self):
'''API representation of this rule element'''
return 'srcports=' + '{!s}-{!s}'.format(*self.range)

class IcmpType(RuleOption):
'''ICMP packet type'''
Expand Down Expand Up @@ -251,9 +285,12 @@ def __init__(self, rule, **kwargs):
:param kwargs: rule elements
'''
self._action = None
self._forwardtype = None
self._proto = None
self._dsthost = None
self._srchost = None
self._dstports = None
self._srcports = None
self._icmptype = None
self._specialtarget = None
self._expire = None
Expand All @@ -269,14 +306,17 @@ def __init__(self, rule, **kwargs):
rule_dict['comment'] = comment
rule_dict.update(kwargs)

rule_elements = ('action', 'proto', 'dsthost', 'dst4', 'dst6',
'specialtarget', 'dstports', 'icmptype', 'expire', 'comment')
rule_elements = ('action', 'forwardtype', 'proto', 'dsthost', 'srchost', 'dst4',
'dst6', 'src4', 'src6', 'specialtarget', 'srcports', 'dstports', 'icmptype',
'expire', 'comment')
for rule_opt in rule_elements:
value = rule_dict.pop(rule_opt, None)
if value is None:
continue
if rule_opt in ('dst4', 'dst6'):
rule_opt = 'dsthost'
if rule_opt in ('src4', 'src6'):
rule_opt = 'srchost'
setattr(self, rule_opt, value)

if rule_dict:
Expand All @@ -297,6 +337,17 @@ def action(self, value):
value = Action(value)
self._action = value

@property
def forwardtype(self):
'''type of forwarding (internal or external)'''
return self._forwardtype

@forwardtype.setter
def forwardtype(self, value):
if not isinstance(value, ForwardType):
value = ForwardType(value)
self._forwardtype = value

@property
def proto(self):
'''protocol to match'''
Expand All @@ -323,9 +374,20 @@ def dsthost(self, value):
value = DstHost(value)
self._dsthost = value

@property
def srchost(self):
'''destination host/network'''
return self._srchost

@srchost.setter
def srchost(self, value):
if value is not None and not isinstance(value, SrcHost):
value = SrcHost(value)
self._srchost = value

@property
def dstports(self):
''''Destination port(s) (for \'tcp\' and \'udp\' protocol only)'''
'''Destination port(s) (for \'tcp\' and \'udp\' protocol only)'''
return self._dstports

@dstports.setter
Expand All @@ -338,6 +400,21 @@ def dstports(self, value):
value = DstPorts(value)
self._dstports = value

@property
def srcports(self):
''''Source port(s) (for forwarding only)'''
return self._srcports

@srcports.setter
def srcports(self, value):
if value is not None:
if self.proto not in ('tcp', 'udp'):
raise ValueError(
'srcports valid only for \'tcp\' and \'udp\' protocols')
if not isinstance(value, DstPorts):
value = SrcPorts(value)
self._srcports = value

@property
def icmptype(self):
'''ICMP packet type (for \'icmp\' protocol only)'''
Expand Down Expand Up @@ -390,8 +467,9 @@ def rule(self):
'''API representation of this rule'''
values = []
# comment must be the last one
for prop in ('action', 'proto', 'dsthost', 'dstports', 'icmptype',
'specialtarget', 'expire', 'comment'):
for prop in ('action', 'forwardtype', 'proto', 'dsthost', 'srchost',
'dstports', 'srcports', 'icmptype', 'specialtarget', 'expire',
'comment'):
value = getattr(self, prop)
if value is None:
continue
Expand Down
26 changes: 19 additions & 7 deletions qubesadmin/tools/qvm_firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ def __call__(self, _parser, namespace, values, option_string=None):
if not values:
setattr(namespace, self.dest, None)
return
assumed_order = ['action', 'dsthost', 'proto', 'dstports', 'icmptype']
assumed_order = ['action', 'forwardtype', 'proto', 'dsthost',
'srchost', 'dstports', 'srcports', 'icmptype']
allowed_opts = assumed_order + ['specialtarget', 'comment', 'expire']
kwargs = {}
for opt in values:
Expand All @@ -61,6 +62,8 @@ def __call__(self, _parser, namespace, values, option_string=None):
'invalid rule description: {}'.format(opt))
if key in ['dst4', 'dst6']:
key = 'dsthost'
if key in ['src4', 'src6']:
key = 'srchost'
if key not in allowed_opts:
raise argparse.ArgumentError(None,
'Invalid rule element: {}'.format(opt))
Expand All @@ -83,22 +86,26 @@ def __call__(self, _parser, namespace, values, option_string=None):
<action> [<dsthost> [<proto> [<dstports>|<icmptype>]]]

And as keyword arguments:
action=<action> [specialtarget=dns] [dsthost=<dsthost>]
[proto=<proto>] [dstports=<dstports>] [icmptype=<icmptype>]
[expire=<expire>]
action=<action> [specialtarget=dns] [dsthost=<dsthost>]
[srchost=<srchost>] [proto=<proto>] [srcports=<srcports>]
[dstports=<dstports>] [icmptype=<icmptype>] [expire=<expire>]
[comment=<comment>]

Both formats, positional and keyword arguments, can be used
interchangeably.

Available matches:
action: accept or drop
action: accept, drop or forward
forwardtype internal or external (only with action=forward)
dst4 synonym for dsthost
dst6 synonym for dsthost
dsthost IP, network or hostname
(e.g. 10.5.3.2, 192.168.0.0/16,
www.example.com, fd00::/8)
srchost allowed inbound host (only with action=forward)
dstports port or port range
(e.g. 443 or 1200-1400)
srcports external inbound port range (only with action=forward)
icmptype icmp type number (e.g. 8 for echo requests)
proto icmp, tcp or udp
specialtarget only the value dns is currently supported,
Expand All @@ -107,6 +114,8 @@ def __call__(self, _parser, namespace, values, option_string=None):
expire the rule is automatically removed at the time given as
seconds since 1/1/1970, or +seconds (e.g. +300 for a rule
to expire in 5 minutes)
comment needs to be positional at the end. Free text to comment the
rule, its purpose, etc
"""

parser = qubesadmin.tools.QubesArgumentParser(vmname_nargs=1, epilog=epilog,
Expand Down Expand Up @@ -146,15 +155,18 @@ def rules_list_table(vm):
:param vm: VM object
:return: None
'''
header = ['NO', 'ACTION', 'HOST', 'PROTOCOL', 'PORT(S)',
'SPECIAL TARGET', 'ICMP TYPE', 'EXPIRE', 'COMMENT']
header = ['NO', 'ACTION', 'FWD TYPE', 'DSTHOST', 'SRCHOST', 'PROTOCOL', 'DSTPORT(S)',
'SRCPORT(S)', 'SPECIAL TARGET', 'ICMP TYPE', 'EXPIRE', 'COMMENT']
rows = []
for (rule, rule_no) in zip(vm.firewall.rules, itertools.count()):
row = [x.pretty_value if x is not None else '-' for x in [
rule.action,
rule.forwardtype,
rule.dsthost,
rule.srchost,
rule.proto,
rule.dstports,
rule.srcports,
rule.specialtarget,
rule.icmptype,
rule.expire,
Expand Down