models.py 40.7 KB
Newer Older
1 2
# -*- coding: utf-8 -*-

3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE.  If not, see <http://www.gnu.org/licenses/>.

20
from itertools import islice, ifilter
21
import logging
22
from netaddr import IPSet, EUI, IPNetwork
23

24 25
from django.contrib.auth.models import User
from django.db import models
26
from django.forms import ValidationError
27
from django.utils.translation import ugettext_lazy as _
28
from firewall.fields import (MACAddressField, val_alfanum, val_reverse_domain,
29
                             val_ipv6_template, val_domain, val_ipv4,
30
                             val_domain_wildcard,
31 32
                             val_ipv6, val_mx, convert_ipv4_to_ipv6,
                             IPNetworkField, IPAddressField)
33 34
from django.core.validators import MinValueValidator, MaxValueValidator
import django.conf
35
from django.db.models.signals import post_save, post_delete
36 37
import random

38
from common.models import HumanSortField
Bach Dániel committed
39
from firewall.tasks.local_tasks import reloadtask
40
from .iptables import IptRule
41
from acl.models import AclBase
42
logger = logging.getLogger(__name__)
43
settings = django.conf.settings.FIREWALL_SETTINGS
44 45


46 47
class Rule(models.Model):

48 49 50
    """
    A rule of a packet filter, changing the behavior of a host, vlan or
    firewall.
51

52 53
    Some rules accept or deny packets matching some criteria.
    Others set address translation or other free-form iptables parameters.
54 55
    """
    CHOICES_type = (('host', 'host'), ('firewall', 'firewall'),
Bach Dániel committed
56
                    ('vlan', 'vlan'))
57
    CHOICES_proto = (('tcp', 'tcp'), ('udp', 'udp'), ('icmp', 'icmp'))
58 59 60
    CHOICES_dir = (('out', _('out')), ('in', _('in')))
    CHOICES_action = (('accept', _('accept')), ('drop', _('drop')),
                      ('ignore', _('ignore')))
61

62
    direction = models.CharField(max_length=3, choices=CHOICES_dir,
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
                                 blank=False, verbose_name=_("direction"),
                                 help_text=_("If the rule matches egress "
                                             "or ingress packets."))
    description = models.TextField(blank=True, verbose_name=_('description'),
                                   help_text=_("Why is the rule needed, "
                                               "or how does it work."))
    foreign_network = models.ForeignKey(
        'VlanGroup', verbose_name=_("foreign network"),
        help_text=_("The group of vlans the matching packet goes to "
                    "(direction out) or from (in)."),
        related_name="ForeignRules")
    dport = models.IntegerField(
        blank=True, null=True, verbose_name=_("dest. port"),
        validators=[MinValueValidator(1), MaxValueValidator(65535)],
        help_text=_("Destination port number of packets that match."))
    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."))
82 83
    weight = models.IntegerField(
        verbose_name=_("weight"),
84
        validators=[MinValueValidator(1), MaxValueValidator(65535)],
85 86
        help_text=_("Rule weight"),
        default=30000)
87
    proto = models.CharField(max_length=10, choices=CHOICES_proto,
88 89 90 91 92
                             blank=True, null=True, verbose_name=_("protocol"),
                             help_text=_("Protocol of packets that match."))
    extra = models.TextField(blank=True, verbose_name=_("extra arguments"),
                             help_text=_("Additional arguments passed "
                                         "literally to the iptables-rule."))
93 94 95 96
    action = models.CharField(max_length=10, choices=CHOICES_action,
                              default='drop', verbose_name=_('action'),
                              help_text=_("Accept, drop or ignore the "
                                          "matching packets."))
97 98 99 100
    owner = models.ForeignKey(User, blank=True, null=True,
                              verbose_name=_("owner"),
                              help_text=_("The user responsible for "
                                          "this rule."))
101

102 103
    nat = models.BooleanField(default=False, verbose_name=_("NAT"),
                              help_text=_("If network address translation "
104
                                          "should be done."))
105 106 107 108 109 110 111 112 113
    nat_external_port = models.IntegerField(
        blank=True, null=True,
        help_text=_("Rewrite destination port number to this if NAT is "
                    "needed."),
        validators=[MinValueValidator(1), MaxValueValidator(65535)])
    nat_external_ipv4 = IPAddressField(
        version=4, blank=True, null=True,
        verbose_name=_('external IPv4 address'))

114 115 116 117 118 119
    created_at = models.DateTimeField(
        auto_now_add=True,
        verbose_name=_("created at"))
    modified_at = models.DateTimeField(
        auto_now=True,
        verbose_name=_("modified at"))
120 121

    vlan = models.ForeignKey('Vlan', related_name="rules", blank=True,
122 123 124
                             null=True, verbose_name=_("vlan"),
                             help_text=_("Vlan the rule applies to "
                                         "(if type is vlan)."))
125
    vlangroup = models.ForeignKey('VlanGroup', related_name="rules",
126 127 128 129
                                  blank=True, null=True, verbose_name=_(
                                      "vlan group"),
                                  help_text=_("Group of vlans the rule "
                                              "applies to (if type is vlan)."))
130
    host = models.ForeignKey('Host', related_name="rules", blank=True,
131 132 133 134 135 136 137 138 139 140 141 142
                             verbose_name=_('host'), null=True,
                             help_text=_("Host the rule applies to "
                                         "(if type is host)."))
    hostgroup = models.ForeignKey(
        'Group', related_name="rules", verbose_name=_("host group"),
        blank=True, null=True, help_text=_("Group of hosts the rule applies "
                                           "to (if type is host)."))
    firewall = models.ForeignKey(
        'Firewall', related_name="rules", verbose_name=_("firewall"),
                                 help_text=_("Firewall the rule applies to "
                                             "(if type is firewall)."),
        blank=True, null=True)
143 144 145 146 147 148

    def __unicode__(self):
        return self.desc()

    def clean(self):
        fields = [self.vlan, self.vlangroup, self.host, self.hostgroup,
149
                  self.firewall]
150 151 152 153
        selected_fields = [field for field in fields if field]
        if len(selected_fields) > 1:
            raise ValidationError(_('Only one field can be selected.'))

154 155 156 157 158 159 160 161 162 163 164
    def get_external_ipv4(self):
        return (self.nat_external_ipv4
                if self.nat_external_ipv4 else self.host.get_external_ipv4())

    def get_external_port(self, proto='ipv4'):
        assert proto in ('ipv4', 'ipv6')
        if proto == 'ipv4' and self.nat_external_port:
            return self.nat_external_port
        else:
            return self.dport

165
    def desc(self):
166 167
        """Return a short string representation of the current rule.
        """
168 169
        return u'[%(type)s] %(src)s ▸ %(dst)s %(para)s %(desc)s' % {
            'type': self.r_type,
170
            'src': (unicode(self.foreign_network) if self.direction == 'in'
171
                    else self.r_type),
172
            'dst': (self.r_type if self.direction == 'out'
173
                    else unicode(self.foreign_network)),
174 175 176 177 178
            'para': ((("proto=%s " % self.proto) if self.proto else '') +
                     (("sport=%s " % self.sport) if self.sport else '') +
                     (("dport=%s " % self.dport) if self.dport else '')),
            'desc': self.description}

179 180 181 182 183 184 185 186 187
    @property
    def r_type(self):
        fields = [self.vlan, self.vlangroup, self.host, self.hostgroup,
                  self.firewall]
        for field in fields:
            if field is not None:
                return field.__class__.__name__.lower()
        return None

188 189 190 191
    @models.permalink
    def get_absolute_url(self):
        return ('network.rule', None, {'pk': self.pk})

192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
    def get_chain_name(self, local, remote):
        if local:  # host or vlan
            if self.direction == 'in':
                # remote -> local
                return '%s_%s' % (remote.name, local.name)
            else:
                # local -> remote
                return '%s_%s' % (local.name, remote.name)
            # firewall rule
        elif self.firewall_id:
            return 'INPUT' if self.direction == 'in' else 'OUTPUT'

    def get_dport_sport(self):
        if self.direction == 'in':
            return self.dport, self.sport
207
        else:
208
            return self.sport, self.dport
209 210 211

    def get_ipt_rules(self, host=None):
        # action
212
        action = 'LOG_ACC' if self.action == 'accept' else 'LOG_DROP'
213

214 215 216
        # 'chain_name': rule dict
        retval = {}

217 218 219 220 221 222 223 224 225 226
        # src and dst addresses
        src = None
        dst = None

        if host:
            ip = (host.ipv4, host.ipv6_with_prefixlen)
            if self.direction == 'in':
                dst = ip
            else:
                src = ip
227 228 229
            vlan = host.vlan
        elif self.vlan_id:
            vlan = self.vlan
230
        else:
231
            vlan = None
232

233 234 235 236 237
        if vlan and not vlan.managed:
            return retval

        # src and dst ports
        dport, sport = self.get_dport_sport()
238 239 240

        # process foreign vlans
        for foreign_vlan in self.foreign_network.vlans.all():
241 242 243
            if not foreign_vlan.managed:
                continue

244
            r = IptRule(priority=self.weight, action=action,
245
                        proto=self.proto, extra=self.extra,
246
                        comment='Rule #%s' % self.pk,
247
                        src=src, dst=dst, dport=dport, sport=sport)
248
            chain_name = self.get_chain_name(local=vlan, remote=foreign_vlan)
249 250 251 252
            retval[chain_name] = r

        return retval

253 254 255 256 257 258 259 260
    class Meta:
        verbose_name = _("rule")
        verbose_name_plural = _("rules")
        ordering = (
            'direction',
            'proto',
            'sport',
            'dport',
261
            'nat_external_port',
262 263 264 265
            'host',
        )


266
class Vlan(AclBase, models.Model):
267 268 269 270 271 272 273 274 275 276 277 278

    """
    A vlan of the network,

    Networks controlled by this framework are split into separated subnets.
    These networks are izolated by the vlan (virtual lan) technology, which is
    commonly used by managed network switches to partition the network.

    Each vlan network has a unique identifier, a name, a unique IPv4 and IPv6
    range. The gateway also has an IP address in each range.
    """

279 280 281 282
    ACL_LEVELS = (
        ('user', _('user')),
        ('operator', _('operator')),
    )
283
    CHOICES_NETWORK_TYPE = (('public', _('public')),
284
                            ('portforward', _('portforward')))
285 286 287 288 289 290 291 292 293 294
    vid = models.IntegerField(unique=True,
                              verbose_name=_('VID'),
                              help_text=_('The vlan ID of the subnet.'),
                              validators=[MinValueValidator(1),
                                          MaxValueValidator(4095)])
    name = models.CharField(max_length=20,
                            unique=True,
                            verbose_name=_('Name'),
                            help_text=_('The short name of the subnet.'),
                            validators=[val_alfanum])
295 296 297 298 299
    network4 = IPNetworkField(unique=False,
                              version=4,
                              verbose_name=_('IPv4 address/prefix'),
                              help_text=_(
                                  'The IPv4 address and the prefix length '
300
                                  'of the gateway. '
301 302 303 304
                                  'Recommended value is the last '
                                  'valid address of the subnet, '
                                  'for example '
                                  '10.4.255.254/16 for 10.4.0.0/16.'))
305 306 307 308 309 310
    host_ipv6_prefixlen = models.IntegerField(
        verbose_name=_('IPv6 prefixlen/host'),
        help_text=_('The prefix length of the subnet assigned to a host. '
                    'For example /112 = 65536 addresses/host.'),
        default=112,
        validators=[MinValueValidator(1), MaxValueValidator(128)])
311 312 313 314 315 316 317 318
    network6 = IPNetworkField(unique=False,
                              version=6,
                              null=True,
                              blank=True,
                              verbose_name=_('IPv6 address/prefix'),
                              help_text=_(
                                  'The IPv6 address and the prefix length '
                                  'of the gateway.'))
319
    snat_ip = models.GenericIPAddressField(protocol='ipv4', blank=True,
320 321 322 323 324 325 326 327
                                           null=True,
                                           verbose_name=_('NAT IP address'),
                                           help_text=_(
                                               'Common IPv4 address used for '
                                               'address translation of '
                                               'connections to the networks '
                                               'selected below '
                                               '(typically to the internet).'))
328
    snat_to = models.ManyToManyField('self', symmetrical=False, blank=True,
329 330 331 332 333 334 335
                                     null=True, verbose_name=_('NAT to'),
                                     help_text=_(
                                         'Connections to these networks '
                                         'should be network address '
                                         'translated, i.e. their source '
                                         'address is rewritten to the value '
                                         'of NAT IP address.'))
336 337
    network_type = models.CharField(choices=CHOICES_NETWORK_TYPE,
                                    verbose_name=_('network type'),
338
                                    default='portforward',
339
                                    max_length=20)
340
    managed = models.BooleanField(default=True, verbose_name=_('managed'))
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
    description = models.TextField(blank=True, verbose_name=_('description'),
                                   help_text=_(
                                       'Description of the goals and elements '
                                       'of the vlan network.'))
    comment = models.TextField(blank=True, verbose_name=_('comment'),
                               help_text=_(
                                   'Notes, comments about the network'))
    domain = models.ForeignKey('Domain', verbose_name=_('domain name'),
                               help_text=_('Domain name of the members of '
                                           'this network.'))
    reverse_domain = models.TextField(
        validators=[val_reverse_domain],
        verbose_name=_('reverse domain'),
        help_text=_('Template of the IPv4 reverse domain name that '
                    'should be generated for each host. The template '
                    'should contain four tokens: "%(a)d", "%(b)d", '
                    '"%(c)d", and "%(d)d", representing the four bytes '
                    'of the address, respectively, in decimal notation. '
                    'For example, the template for the standard reverse '
                    'address is: "%(d)d.%(c)d.%(b)d.%(a)d.in-addr.arpa".'),
        default="%(d)d.%(c)d.%(b)d.%(a)d.in-addr.arpa")
362 363 364 365
    ipv6_template = models.TextField(
        validators=[val_ipv6_template],
        verbose_name=_('ipv6 template'),
        default="2001:738:2001:4031:%(b)d:%(c)d:%(d)d:0")
366 367 368 369 370 371 372 373 374 375 376 377 378
    dhcp_pool = models.TextField(blank=True, verbose_name=_('DHCP pool'),
                                 help_text=_(
                                     'The address range of the DHCP pool: '
                                     'empty for no DHCP service, "manual" for '
                                     'no DHCP pool, or the first and last '
                                     'address of the range separated by a '
                                     'space.'))
    created_at = models.DateTimeField(auto_now_add=True,
                                      verbose_name=_('created at'))
    owner = models.ForeignKey(User, blank=True, null=True,
                              verbose_name=_('owner'))
    modified_at = models.DateTimeField(auto_now=True,
                                       verbose_name=_('modified at'))
379 380

    def __unicode__(self):
381 382
        return "%s - %s" % ("managed" if self.managed else "unmanaged",
                            self.name)
383

384 385 386 387
    @models.permalink
    def get_absolute_url(self):
        return ('network.vlan', None, {'vid': self.vid})

388 389 390 391 392 393
    def get_random_addresses(self, used_v4, buffer_size=100, max_hosts=10000):
        addresses = islice(self.network4.iter_hosts(), max_hosts)
        unused_addresses = list(islice(
            ifilter(lambda x: x not in used_v4, addresses), buffer_size))
        random.shuffle(unused_addresses)
        return unused_addresses
394

395
    def get_new_address(self):
396
        hosts = self.host_set
397 398 399
        used_v4 = IPSet(hosts.values_list('ipv4', flat=True))
        used_v6 = IPSet(hosts.exclude(ipv6__isnull=True)
                        .values_list('ipv6', flat=True))
400

401 402 403 404 405 406 407 408 409 410
        for ipv4 in self.get_random_addresses(used_v4):
            logger.debug("Found unused IPv4 address %s.", ipv4)
            ipv6 = None
            if self.network6 is not None:
                ipv6 = convert_ipv4_to_ipv6(self.ipv6_template, ipv4)
                if ipv6 in used_v6:
                    continue
                else:
                    logger.debug("Found unused IPv6 address %s.", ipv6)
            return {'ipv4': ipv4, 'ipv6': ipv6}
411 412
        else:
            raise ValidationError(_("All IP addresses are already in use."))
413

414

415
class VlanGroup(models.Model):
416 417 418 419 420 421
    """
    A group of Vlans.
    """

    name = models.CharField(max_length=20, unique=True, verbose_name=_('name'),
                            help_text=_('The name of the group.'))
422
    vlans = models.ManyToManyField('Vlan', symmetrical=False, blank=True,
423 424 425 426 427 428 429 430 431 432 433
                                   null=True, verbose_name=_('vlans'),
                                   help_text=_('The vlans which are members '
                                               'of the group.'))
    description = models.TextField(blank=True, verbose_name=_('description'),
                                   help_text=_('Description of the group.'))
    owner = models.ForeignKey(User, blank=True, null=True,
                              verbose_name=_('owner'))
    created_at = models.DateTimeField(auto_now_add=True,
                                      verbose_name=_('created at'))
    modified_at = models.DateTimeField(auto_now=True,
                                       verbose_name=_('modified at'))
434 435 436 437

    def __unicode__(self):
        return self.name

438 439 440 441 442
    @models.permalink
    def get_absolute_url(self):
        return ('network.vlan_group', None, {'pk': self.pk})


443
class Group(models.Model):
444 445 446 447 448 449 450 451 452 453 454 455 456
    """
    A group of hosts.
    """
    name = models.CharField(max_length=20, unique=True, verbose_name=_('name'),
                            help_text=_('The name of the group.'))
    description = models.TextField(blank=True, verbose_name=_('description'),
                                   help_text=_('Description of the group.'))
    owner = models.ForeignKey(User, blank=True, null=True,
                              verbose_name=_('owner'))
    created_at = models.DateTimeField(auto_now_add=True,
                                      verbose_name=_('created at'))
    modified_at = models.DateTimeField(auto_now=True,
                                       verbose_name=_('modified at'))
457 458 459 460

    def __unicode__(self):
        return self.name

461 462 463 464 465
    @models.permalink
    def get_absolute_url(self):
        return ('network.group', None, {'pk': self.pk})


466
class Host(models.Model):
467 468 469 470
    """
    A host of the network.
    """

471
    hostname = models.CharField(max_length=40,
472 473 474 475 476
                                verbose_name=_('hostname'),
                                help_text=_('The alphanumeric hostname of '
                                            'the host, the first part of '
                                            'the FQDN.'),
                                validators=[val_alfanum])
477
    normalized_hostname = HumanSortField(monitor='hostname', max_length=80)
478
    reverse = models.CharField(max_length=40, validators=[val_domain],
479 480 481 482 483 484 485 486 487
                               verbose_name=_('reverse'),
                               help_text=_('The fully qualified reverse '
                                           'hostname of the host, if '
                                           'different than hostname.domain.'),
                               blank=True, null=True)
    mac = MACAddressField(unique=True, verbose_name=_('MAC address'),
                          help_text=_('The MAC (Ethernet) address of the '
                                      'network interface. For example: '
                                      '99:AA:BB:CC:DD:EE.'))
488 489 490 491
    ipv4 = IPAddressField(version=4, unique=True,
                          verbose_name=_('IPv4 address'),
                          help_text=_('The real IPv4 address of the '
                                      'host, for example 10.5.1.34.'))
492
    external_ipv4 = IPAddressField(
493
        version=4, blank=True, null=True,
494 495 496
        verbose_name=_('WAN IPv4 address'),
        help_text=_('The public IPv4 address of the host on the wide '
                    'area network, if different.'))
497 498 499 500 501
    ipv6 = IPAddressField(version=6, unique=True,
                          blank=True, null=True,
                          verbose_name=_('IPv6 address'),
                          help_text=_('The global IPv6 address of the host'
                                      ', for example 2001:db:88:200::10.'))
502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519
    shared_ip = models.BooleanField(default=False, verbose_name=_('shared IP'),
                                    help_text=_(
                                        'If the given WAN IPv4 address is '
                                        'used by multiple hosts.'))
    description = models.TextField(blank=True, verbose_name=_('description'),
                                   help_text=_('What is this host for, what '
                                               'kind of machine is it.'))
    comment = models.TextField(blank=True,
                               verbose_name=_('Notes'))
    location = models.TextField(blank=True, verbose_name=_('location'),
                                help_text=_(
                                    'The physical location of the machine.'))
    vlan = models.ForeignKey('Vlan', verbose_name=_('vlan'),
                             help_text=_(
                                 'Vlan network that the host is part of.'))
    owner = models.ForeignKey(User, verbose_name=_('owner'),
                              help_text=_(
                                  'The person responsible for this host.'))
520
    groups = models.ManyToManyField('Group', symmetrical=False, blank=True,
521 522 523 524 525 526 527
                                    null=True, verbose_name=_('groups'),
                                    help_text=_(
                                        'Host groups the machine is part of.'))
    created_at = models.DateTimeField(auto_now_add=True,
                                      verbose_name=_('created at'))
    modified_at = models.DateTimeField(auto_now=True,
                                       verbose_name=_('modified at'))
528

529 530
    class Meta(object):
        unique_together = ('hostname', 'vlan')
531
        ordering = ('normalized_hostname', 'vlan')
532

533 534 535
    def __unicode__(self):
        return self.hostname

536 537
    @property
    def incoming_rules(self):
538
        return self.rules.filter(direction='in')
539 540

    @property
541 542 543
    def ipv6_with_prefixlen(self):
        try:
            net = IPNetwork(self.ipv6)
544
            net.prefixlen = self.vlan.host_ipv6_prefixlen
545 546 547 548 549 550 551 552 553 554
            return net
        except TypeError:
            return None

    def get_external_ipv4(self):
        return self.external_ipv4 if self.external_ipv4 else self.ipv4

    @property
    def behind_nat(self):
        return self.vlan.network_type != 'public'
555

556
    def clean(self):
557 558 559
        if (self.external_ipv4 and not self.shared_ip and self.behind_nat
                and Host.objects.exclude(id=self.id).filter(
                    external_ipv4=self.external_ipv4)):
560
            raise ValidationError(_("If shared_ip has been checked, "
561 562
                                    "external_ipv4 has to be unique."))
        if Host.objects.exclude(id=self.id).filter(external_ipv4=self.ipv4):
563
            raise ValidationError(_("You can't use another host's NAT'd "
564
                                    "address as your own IPv4."))
565 566 567

    def save(self, *args, **kwargs):
        if not self.id and self.ipv6 == "auto":
568 569
            self.ipv6 = convert_ipv4_to_ipv6(self.vlan.ipv6_template,
                                             self.ipv4)
570
        self.full_clean()
571

572
        super(Host, self).save(*args, **kwargs)
573

Bach Dániel committed
574
        # IPv4
575
        if self.ipv4 is not None:
Bach Dániel committed
576 577 578 579 580 581
            # update existing records
            affected_records = Record.objects.filter(
                host=self, name=self.hostname,
                type='A').update(address=self.ipv4)
            # create new record
            if affected_records == 0:
582 583 584 585 586
                Record(host=self,
                       name=self.hostname,
                       domain=self.vlan.domain,
                       address=self.ipv4,
                       owner=self.owner,
Bach Dániel committed
587
                       description='created by host.save()',
588 589
                       type='A').save()

Bach Dániel committed
590 591 592 593 594 595 596 597
        # IPv6
        if self.ipv6 is not None:
            # update existing records
            affected_records = Record.objects.filter(
                host=self, name=self.hostname,
                type='AAAA').update(address=self.ipv6)
            # create new record
            if affected_records == 0:
598 599 600 601 602
                Record(host=self,
                       name=self.hostname,
                       domain=self.vlan.domain,
                       address=self.ipv6,
                       owner=self.owner,
Bach Dániel committed
603
                       description='created by host.save()',
604
                       type='AAAA').save()
605 606

    def enable_net(self):
607 608
        for i in settings.get('default_host_groups', []):
            self.groups.add(Group.objects.get(name=i))
609

610 611 612 613
    def _get_ports_used(self, proto):
        """
        Gives a list of port numbers used for the public IP address of current
        host for the given protocol.
614

615 616 617 618
        :param proto: The transport protocol of the generated port (tcp|udp).
        :type proto: str.
        :returns: list -- list of int port numbers used.
        """
619 620 621 622 623
        if self.behind_nat:
            ports = Rule.objects.filter(
                host__external_ipv4=self.external_ipv4,
                nat=True,
                proto=proto).values_list('nat_external_port', flat=True)
624
        else:
625 626 627
            ports = self.rules.filter(proto=proto).values_list(
                'dport', flat=True)
        return set(ports)
628 629 630 631 632 633 634 635

    def _get_random_port(self, proto, used_ports=None):
        """
        Get a random unused port for given protocol for current host's public
        IP address.

        :param proto: The transport protocol of the generated port (tcp|udp).
        :type proto: str.
636
        :param used_ports: Optional set of used ports returned by
637 638 639 640 641 642 643 644 645 646 647 648 649
                           _get_ports_used.
        :returns: int -- the generated port number.
        :raises: ValidationError
        """
        if used_ports is None:
            used_ports = self._get_ports_used(proto)

        public = random.randint(1024, 21000)  # pick a random port
        if public in used_ports:  # if it's in use, select smallest free one
            for i in range(1024, 21000) + range(24000, 65535):
                if i not in used_ports:
                    public = i
                    break
650
            else:
651 652
                raise ValidationError(
                    _("All %s ports are already in use.") % proto)
653
        return public
654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673

    def add_port(self, proto, public=None, private=None):
        """
        Allow inbound traffic to a port.

        If the host uses a shared IP address, also set up port forwarding.

        :param proto: The transport protocol (tcp|udp).
        :type proto: str.
        :param public: Preferred public port number for forwarding (optional).
        :param private: Port number of host in subject.
        """
        assert proto in ('tcp', 'udp', )
        if public:
            if public in self._get_ports_used(proto):
                raise ValidationError(
                    _("Port %(proto)s %(public)s is already in use.") %
                    {'proto': proto, 'public': public})
        else:
            public = self._get_random_port(proto)
674

675 676 677 678 679 680
        try:
            vgname = settings["default_vlangroup"]
            vg = VlanGroup.objects.get(name=vgname)
        except VlanGroup.DoesNotExist as e:
            logger.error('Host.add_port: default_vlangroup %s missing. %s',
                         vgname, unicode(e))
681
        else:
682
            rule = Rule(direction='in', owner=self.owner, dport=private,
683
                        proto=proto, nat=False, action='accept',
684 685
                        host=self, foreign_network=vg)
            if self.behind_nat:
686 687 688
                if public < 1024:
                    raise ValidationError(
                        _("Only ports above 1024 can be used."))
689 690
                rule.nat_external_port = public
                rule.nat = True
691 692
            rule.full_clean()
            rule.save()
693 694

    def del_port(self, proto, private):
695 696 697 698 699 700 701 702 703 704
        """
        Remove rules about inbound traffic to a given port.

        If the host uses a shared IP address, also set up port forwarding.

        :param proto: The transport protocol (tcp|udp).
        :type proto: str.
        :param private: Port number of host in subject.
        """

705
        self.rules.filter(proto=proto, dport=private).delete()
706

707
    def get_hostname(self, proto, public=True):
708
        """
709
        Get a private or public hostname for host.
710 711 712 713 714

        :param proto: The IP version (ipv4|ipv6).
        :type proto: str.
        """
        assert proto in ('ipv6', 'ipv4', )
715 716
        try:
            if proto == 'ipv6':
717 718
                res = self.record_set.filter(type='AAAA',
                                             address=self.ipv6)
719
            elif proto == 'ipv4':
720 721 722
                if self.behind_nat and public:
                    res = Record.objects.filter(
                        type='A', address=self.get_external_ipv4())
723
                    if res.count() < 1:
724
                        return unicode(self.get_external_ipv4())
725
                else:
726 727 728
                    res = self.record_set.filter(type='A',
                                                 address=self.ipv4)
            return unicode(res[0].fqdn)
729
        except:
730
            return None
731 732

    def list_ports(self):
733 734 735
        """
        Return a list of ports with forwarding rules set.
        """
736
        retval = []
737
        for rule in self.rules.all():
738 739
            forward = {
                'proto': rule.proto,
740
                'private': rule.dport,
741 742 743 744 745
            }

            if True:      # ipv4
                forward['ipv4'] = {
                    'host': self.get_hostname(proto='ipv4'),
746
                    'port': rule.get_external_port(proto='ipv4'),
747
                    'pk': rule.pk,
748
                }
749
            if self.ipv6:  # ipv6
750 751
                forward['ipv6'] = {
                    'host': self.get_hostname(proto='ipv6'),
752
                    'port': rule.get_external_port(proto='ipv6'),
753
                    'pk': rule.pk,
754 755 756 757 758
                }
            retval.append(forward)
        return retval

    def get_fqdn(self):
759 760 761
        """
        Get fully qualified host name of host.
        """
762
        return self.get_hostname('ipv4', public=False)
763

764 765 766 767 768 769 770
    def get_public_endpoints(self, port, protocol='tcp'):
        """Get public IPv4 and IPv6 endpoints for local port.

        Optionally the required protocol (e.g. TCP, UDP) can be specified.
        """
        endpoints = {}
        # IPv4
771
        ports = self.incoming_rules.filter(action='accept', dport=port,
772 773 774 775 776
                                           proto=protocol)
        public_port = (ports[0].get_external_port(proto='ipv4')
                       if ports.exists() else None)
        endpoints['ipv4'] = ((self.get_external_ipv4(), public_port)
                             if public_port else
777 778
                             None)
        # IPv6
779
        endpoints['ipv6'] = (self.ipv6, port) if public_port else None
780 781
        return endpoints

782 783 784 785
    @models.permalink
    def get_absolute_url(self):
        return ('network.host', None, {'pk': self.pk})

786 787 788 789 790 791 792 793 794 795 796
    @property
    def eui(self):
        return EUI(self.mac)

    @property
    def hw_vendor(self):
        try:
            return self.eui.oui.registration().org
        except:
            return None

797 798

class Firewall(models.Model):
799 800
    name = models.CharField(max_length=20, unique=True,
                            verbose_name=_('name'))
801 802 803 804

    def __unicode__(self):
        return self.name

805

806
class Domain(models.Model):
807 808 809 810 811 812 813 814 815
    name = models.CharField(max_length=40, validators=[val_domain],
                            verbose_name=_('name'))
    owner = models.ForeignKey(User, verbose_name=_('owner'))
    created_at = models.DateTimeField(auto_now_add=True,
                                      verbose_name=_('created_at'))
    modified_at = models.DateTimeField(auto_now=True,
                                       verbose_name=_('modified_at'))
    ttl = models.IntegerField(default=600, verbose_name=_('ttl'))
    description = models.TextField(blank=True, verbose_name=_('description'))
816 817 818 819

    def __unicode__(self):
        return self.name

820 821 822 823 824
    @models.permalink
    def get_absolute_url(self):
        return ('network.domain', None, {'pk': self.pk})


825 826
class Record(models.Model):
    CHOICES_type = (('A', 'A'), ('CNAME', 'CNAME'), ('AAAA', 'AAAA'),
Bach Dániel committed
827
                    ('MX', 'MX'), ('NS', 'NS'), ('PTR', 'PTR'), ('TXT', 'TXT'))
828
    name = models.CharField(max_length=40, validators=[val_domain_wildcard],
829 830 831 832 833 834
                            blank=True, null=True, verbose_name=_('name'))
    domain = models.ForeignKey('Domain', verbose_name=_('domain'))
    host = models.ForeignKey('Host', blank=True, null=True,
                             verbose_name=_('host'))
    type = models.CharField(max_length=6, choices=CHOICES_type,
                            verbose_name=_('type'))
835
    address = models.CharField(max_length=200,
836 837 838 839 840 841 842 843
                               verbose_name=_('address'))
    ttl = models.IntegerField(default=600, verbose_name=_('ttl'))
    owner = models.ForeignKey(User, verbose_name=_('owner'))
    description = models.TextField(blank=True, verbose_name=_('description'))
    created_at = models.DateTimeField(auto_now_add=True,
                                      verbose_name=_('created_at'))
    modified_at = models.DateTimeField(auto_now=True,
                                       verbose_name=_('modified_at'))
844 845 846 847 848

    def __unicode__(self):
        return self.desc()

    def desc(self):
849
        return u' '.join([self.fqdn, self.type, self.address])
850 851 852 853 854

    def save(self, *args, **kwargs):
        self.full_clean()
        super(Record, self).save(*args, **kwargs)

855 856
    def _validate_record(self):
        """Validate a record."""
857 858
        if not self.address:
            raise ValidationError(_("Address must be specified!"))
859 860 861 862 863 864 865 866 867 868 869 870

        try:
            validator = {
                'A': val_ipv4,
                'AAAA': val_ipv6,
                'CNAME': val_domain,
                'MX': val_mx,
                'NS': val_domain,
                'PTR': val_domain,
                'TXT': None,
            }[self.type]
        except KeyError:
871
            raise ValidationError(_("Unknown record type."))
872 873 874
        else:
            if validator:
                validator(self.address)
875

876
    def clean(self):
877 878
        """Validate the Record to be saved.
        """
879 880 881
        if self.name:
            self.name = self.name.rstrip(".")    # remove trailing dots

882
        self._validate_record()
883

884 885
    @property
    def fqdn(self):
886 887 888 889
        if self.name:
            return '%s.%s' % (self.name, self.domain.name)
        else:
            return self.domain.name
890

891 892 893 894
    @models.permalink
    def get_absolute_url(self):
        return ('network.record', None, {'pk': self.pk})

895 896 897 898 899 900
    class Meta:
        ordering = (
            'domain',
            'name',
        )

901

902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922
class SwitchPort(models.Model):
    untagged_vlan = models.ForeignKey('Vlan',
                                      related_name='untagged_ports',
                                      verbose_name=_('untagged vlan'))
    tagged_vlans = models.ForeignKey('VlanGroup', blank=True, null=True,
                                     related_name='tagged_ports',
                                     verbose_name=_('tagged vlans'))
    description = models.TextField(blank=True, verbose_name=_('description'))
    created_at = models.DateTimeField(auto_now_add=True,
                                      verbose_name=_('created_at'))
    modified_at = models.DateTimeField(auto_now=True,
                                       verbose_name=_('modified_at'))

    def __unicode__(self):
        devices = ','.join(self.ethernet_devices.values_list('name',
                                                             flat=True))
        tagged_vlans = self.tagged_vlans.name if self.tagged_vlans else ''
        return 'devices=%s untagged=%s tagged=%s' % (devices,
                                                     self.untagged_vlan,
                                                     tagged_vlans)

923 924 925 926
    @models.permalink
    def get_absolute_url(self):
        return ('network.switch_port', None, {'pk': self.pk})

927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946

class EthernetDevice(models.Model):
    name = models.CharField(max_length=20,
                            unique=True,
                            verbose_name=_('interface'),
                            help_text=_('The name of network interface the '
                                        'gateway should serve this network '
                                        'on. For example eth2.'))
    switch_port = models.ForeignKey('SwitchPort',
                                    related_name='ethernet_devices',
                                    verbose_name=_('switch port'))
    created_at = models.DateTimeField(auto_now_add=True,
                                      verbose_name=_('created_at'))
    modified_at = models.DateTimeField(auto_now=True,
                                       verbose_name=_('modified_at'))

    def __unicode__(self):
        return self.name


947
class BlacklistItem(models.Model):
948 949
    CHOICES_type = (('permban', 'permanent ban'), ('tempban', 'temporary ban'),
                    ('whitelist', 'whitelist'), ('tempwhite', 'tempwhite'))
950
    ipv4 = models.GenericIPAddressField(protocol='ipv4', unique=True)
951 952 953 954 955 956 957 958 959 960 961 962 963 964 965
    host = models.ForeignKey('Host', blank=True, null=True,
                             verbose_name=_('host'))
    reason = models.TextField(blank=True, verbose_name=_('reason'))
    snort_message = models.TextField(blank=True,
                                     verbose_name=_('short message'))
    type = models.CharField(
        max_length=10,
        choices=CHOICES_type,
        default='tempban',
        verbose_name=_('type')
    )
    created_at = models.DateTimeField(auto_now_add=True,
                                      verbose_name=_('created_at'))
    modified_at = models.DateTimeField(auto_now=True,
                                       verbose_name=_('modified_at'))
966 967 968

    def save(self, *args, **kwargs):
        self.full_clean()
969
        super(BlacklistItem, self).save(*args, **kwargs)
970

971 972 973
    def __unicode__(self):
        return self.ipv4

974 975 976 977
    class Meta(object):
        verbose_name = _('blacklist item')
        verbose_name_plural = _('blacklist')

978 979 980 981 982
    @models.permalink
    def get_absolute_url(self):
        return ('network.blacklist', None, {'pk': self.pk})


983
def send_task(sender, instance, created=False, **kwargs):
984
    reloadtask.apply_async(queue='localhost.man', args=[sender.__name__])
985 986


987 988
for sender in [Host, Rule, Domain, Record, Vlan, Firewall, Group,
               BlacklistItem, SwitchPort, EthernetDevice]:
989 990
    post_save.connect(send_task, sender=sender)
    post_delete.connect(send_task, sender=sender)