Commit f91c02bf by Czémán Arnold

network, firewall: add multiport rule

parent a8272957
......@@ -24,6 +24,8 @@ logger = logging.getLogger()
ipv4_re = re.compile(
r'(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}')
multiport_re = re.compile(r'^\d+\:\d+$')
class InvalidRuleExcepion(Exception):
pass
......@@ -32,14 +34,20 @@ class InvalidRuleExcepion(Exception):
class IptRule(object):
def __init__(self, priority=1000, action=None, src=None, dst=None,
proto=None, sport=None, dport=None, extra=None,
ipv4_only=False, comment=None):
proto=None, sport=None, sports=None,
dport=None, dports=None,
extra=None, ipv4_only=False, comment=None):
if proto not in ['tcp', 'udp', 'icmp', None]:
raise InvalidRuleExcepion()
if proto not in ['tcp', 'udp'] and (sport is not None or
dport is not None):
sports is not None or
dport is not None or
dports is not None):
raise InvalidRuleExcepion()
self.check_multiport(sports)
self.check_multiport(dports)
self.priority = int(priority)
self.action = action
......@@ -55,8 +63,15 @@ class IptRule(object):
ipv4_only = True
self.proto = proto
self.sport = sport
self.dport = dport
self.sport = sport if sports is None else None
self.dport = dport if dports is None else None
self.sports = sports
self.dports = dports
if sports is not None or dports is not None:
self.multiport = ""
else:
self.multiport = None
self.extra = extra
self.ipv4_only = (ipv4_only or
......@@ -84,6 +99,9 @@ class IptRule(object):
('proto', '-p %s'),
('sport', '--sport %s'),
('dport', '--dport %s'),
('multiport', '-m multiport %s'),
('sports', '--sports %s'),
('dports', '--dports %s'),
('extra', '%s'),
('comment', '-m comment --comment "%s"'),
('action', '-g %s')])
......@@ -92,6 +110,10 @@ class IptRule(object):
if getattr(self, param) is not None]
return ' '.join(params)
def check_multiport(self, ports):
if ports is not None and not multiport_re.match(ports):
raise InvalidRuleExcepion()
class IptChain(object):
nat_chains = ('PREROUTING', 'POSTROUTING')
......
......@@ -106,42 +106,26 @@ class Command(BaseCommand):
if port:
self.validate_port(port)
try:
rule = self.make_rule(dport=port, proto=proto, action=action,
direction=dir, owner=owner,
firewall=firewall, foreign_network=fnet)
rule.save()
except Warning as e:
logger.warning(e)
self.make_rule(dport=port, proto=proto, action=action,
direction=dir, owner=owner,
firewall=firewall, foreign_network=fnet)
else:
lower = min(range)
higher = max(range)
self.validate_port(lower)
self.validate_port(higher)
rules = []
for port in xrange(lower, higher+1):
try:
rule = self.make_rule(port, proto, action, dir,
owner, firewall, fnet)
rules.append(rule)
except Warning as e:
logger.warning(e)
Rule.objects.bulk_create(rules)
self.make_rule(dport=lower, dport_end=higher, proto=proto,
action=action, direction=dir, owner=owner,
firewall=firewall, foreign_network=fnet)
def make_rule(self, **kwargs):
rule, created = Rule.objects.get_or_create(**kwargs)
if not created:
raise Warning(('Rule does exist: %s' %
unicode(rule)).encode('utf-8'))
rule.full_clean()
return rule
logger.warning(('Rule does exist: %s' %
unicode(rule)).encode('utf-8'))
else:
rule.full_clean()
rule.save()
def validate_port(self, port):
if port < 0 or port > 65535:
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.core.validators
class Migration(migrations.Migration):
dependencies = [
('firewall', '0005_auto_20150520_2250'),
]
operations = [
migrations.AddField(
model_name='rule',
name='dport_end',
field=models.IntegerField(blank=True, help_text='End of the destination port range.', null=True, verbose_name='destination port end', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65535)]),
),
migrations.AddField(
model_name='rule',
name='sport_end',
field=models.IntegerField(blank=True, help_text='End of the source port range.', null=True, verbose_name='source port end', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65535)]),
),
migrations.AlterField(
model_name='rule',
name='dport',
field=models.IntegerField(blank=True, help_text='Destination port number of packets that match. It can also be a range.', null=True, verbose_name='destination port', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65535)]),
),
migrations.AlterField(
model_name='rule',
name='sport',
field=models.IntegerField(blank=True, help_text='Source port number of packets that match. It can also be a range.', null=True, verbose_name='source port', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65535)]),
),
]
......@@ -79,13 +79,23 @@ class Rule(models.Model):
"(direction out) or from (in)."),
related_name="ForeignRules")
dport = models.IntegerField(
blank=True, null=True, verbose_name=_("dest. port"),
blank=True, null=True, verbose_name=_("destination port"),
validators=[MinValueValidator(1), MaxValueValidator(65535)],
help_text=_("Destination port number of packets that match."))
help_text=_("Destination port number of packets that match."
" It can also be a range."))
dport_end = models.IntegerField(
blank=True, null=True, verbose_name=_("destination port end"),
validators=[MinValueValidator(1), MaxValueValidator(65535)],
help_text=_("End of the destination port range."))
sport = models.IntegerField(
blank=True, null=True, verbose_name=_("source port"),
validators=[MinValueValidator(1), MaxValueValidator(65535)],
help_text=_("Source port number of packets that match."))
help_text=_("Source port number of packets that match."
" It can also be a range."))
sport_end = models.IntegerField(
blank=True, null=True, verbose_name=_("source port end"),
validators=[MinValueValidator(1), MaxValueValidator(65535)],
help_text=_("End of the source port range."))
weight = models.IntegerField(
verbose_name=_("weight"),
validators=[MinValueValidator(1), MaxValueValidator(65535)],
......@@ -161,6 +171,12 @@ class Rule(models.Model):
raise ValidationError(
_('One of the following fields must be selected: '
'vlan, vlan group, host, host group, firewall.'))
if (self.sport_end is not None and self.sport_end <= self.sport or
self.dport_end is not None and self.dport_end <= self.dport):
raise ValidationError(
_("Destination or Source port: The beginning of a range"
" can't be greater than or equal to the end of that range."))
def get_external_ipv4(self):
return (self.nat_external_ipv4
......@@ -237,6 +253,9 @@ class Rule(models.Model):
if vlan and not vlan.managed:
return retval
sports = self.make_multiport_value(self.sport, self.sport_end)
dports = self.make_multiport_value(self.dport, self.dport_end)
# process foreign vlans
for foreign_vlan in self.foreign_network.vlans.all():
if not foreign_vlan.managed:
......@@ -245,12 +264,18 @@ class Rule(models.Model):
r = IptRule(priority=self.weight, action=action,
proto=self.proto, extra=self.extra,
comment='Rule #%s' % self.pk,
src=src, dst=dst, dport=self.dport, sport=self.sport)
src=src, dst=dst, dport=self.dport, sport=self.sport,
sports=sports, dports=dports)
chain_name = self.get_chain_name(local=vlan, remote=foreign_vlan)
retval[chain_name] = r
return retval
def make_multiport_value(self, port_begin, port_end):
if port_begin is not None and port_end is not None:
return '%s:%s' % (port_begin, port_end)
return None
@classmethod
def portforwards(cls, host=None):
qs = cls.objects.filter(dport__isnull=False, direction='in')
......
......@@ -187,6 +187,21 @@ class IptablesTestCase(TestCase):
IptRule, **{'priority': 5, 'action': 'ACCEPT',
'dst': '127.0.0.5',
'proto': 'icmp', 'dport': 443})
self.assertRaises(InvalidRuleExcepion,
IptRule, **{'priority': 5, 'action': 'ACCEPT',
'proto': 'tcp', 'dports': '442:'})
self.assertRaises(InvalidRuleExcepion,
IptRule, **{'priority': 5, 'action': 'ACCEPT',
'proto': 'tcp', 'dports': ':443'})
self.assertRaises(InvalidRuleExcepion,
IptRule, **{'priority': 5, 'action': 'ACCEPT',
'proto': 'tcp', 'dports': '442-443'})
self.assertRaises(InvalidRuleExcepion,
IptRule, **{'priority': 5, 'action': 'ACCEPT',
'proto': 'tcp', 'dports': '442'})
self.assertRaises(InvalidRuleExcepion,
IptRule, **{'priority': 5, 'action': 'ACCEPT',
'proto': 'tcp', 'dports': ':'})
def test_chain_compile(self):
ch = IptChain(name='test')
......
......@@ -204,46 +204,13 @@ class RecordForm(ModelForm):
class RuleForm(ModelForm):
helper = FormHelper()
helper.layout = Layout(
Div(
Fieldset(
'',
'direction',
'description',
'foreign_network',
'dport',
'sport',
'weight',
'proto',
'extra',
'action',
'owner',
'nat',
'nat_external_port',
'nat_external_ipv4',
),
Fieldset(
_('External'),
'vlan',
'vlangroup',
'host',
'hostgroup',
'firewall'
)
),
FormActions(
Submit('submit', _("Save")),
LinkButton('back', _("Back"), reverse_lazy('network.rule_list'))
)
)
class Meta:
model = Rule
fields = ("direction", "description", "foreign_network", "dport",
"sport", "weight", "proto", "extra", "action", "owner",
"nat", "nat_external_port", "nat_external_ipv4", "vlan",
"vlangroup", "host", "hostgroup", "firewall", )
"vlangroup", "host", "hostgroup", "firewall",
"dport_end", "sport_end")
class SwitchPortForm(ModelForm):
......
......@@ -189,6 +189,11 @@ class RuleTable(Table):
nat_external_port = Column(
verbose_name=_("NAT")
)
dport = TemplateColumn(
template_name="network/columns/rule-destination-port.html",
verbose_name=_("destination port"),
orderable=True,
)
class Meta:
model = Rule
......
{% if record.dport %}
{% if record.dport_end %}
{{ record.dport }} - {{ record.dport_end }}
{% else %}
{{ record.dport }}
{% endif %}
{% else %}
{% endif %}
......@@ -14,7 +14,7 @@
<div class="row">
<div class="col-sm-7">
{% crispy form %}
{% include "network/rule-form.html" %}
</div>
</div>
{% endblock %}
......@@ -3,7 +3,6 @@
{% load i18n %}
{% load l10n %}
{% load staticfiles %}
{% load crispy_forms_tags %}
{% block title-page %}{{ rule.pk }} | {% trans "rule" %}{% endblock %}
......@@ -18,8 +17,8 @@
</div>
<div class="row">
<div class="col-sm-7">
{% crispy form %}
</div>
<div class="col-sm-7">
{% include "network/rule-form.html" %}
</div>
</div>
{% endblock %}
{% load crispy_forms_tags %}
{% load i18n %}
{{ form|as_crispy_errors }}
<form class="uniForm" method="POST">
{% csrf_token %}
<fieldset>
{{ form.direction|as_crispy_field }}
{{ form.description|as_crispy_field }}
{{ form.foreign_network|as_crispy_field }}
<div>
<div class="row">
<div class="col-xs-6">
{{ form.dport|as_crispy_field }}
</div>
<div class="col-xs-6">
{{ form.dport_end|as_crispy_field }}
</div>
</div>
</div>
<div>
<div class="row">
<div class="col-xs-6">
{{ form.sport|as_crispy_field }}
</div>
<div class="col-xs-6">
{{ form.sport_end|as_crispy_field }}
</div>
</div>
</div>
{{ form.weight|as_crispy_field }}
{{ form.proto|as_crispy_field }}
{{ form.extra|as_crispy_field }}
{{ form.action|as_crispy_field }}
{{ form.owner|as_crispy_field }}
{{ form.nat|as_crispy_field }}
{{ form.nat_external_port|as_crispy_field }}
{{ form.nat_external_ipv4|as_crispy_field }}
</fieldset>
<fieldset>
<legend>{% trans "External" %}</legend>
{{ form.vlan |as_crispy_field }}
{{ form.vlangroup |as_crispy_field }}
{{ form.host |as_crispy_field }}
{{ form.hostgroup |as_crispy_field }}
{{ form.firewall |as_crispy_field }}
</fieldset>
<input type="submit" value="{% trans "Save" %}" class="btn btn-primary">
<a href="{% url "network.rule_list" %}" class="btn btn-default">{% trans "Back" %}</a>
</form>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment