fw.py 10.6 KB
Newer Older
1
import re
Bach Dániel committed
2
import logging
3
from collections import OrderedDict
Bach Dániel committed
4
from netaddr import IPAddress, AddrFormatError
5
from datetime import datetime, timedelta
Bach Dániel committed
6 7 8 9 10
from itertools import product

from .models import (Host, Rule, Vlan, Domain, Record, Blacklist, SwitchPort)
from .iptables import IptRule, IptChain
import django.conf
11
from django.db.models import Q
Bach Dániel committed
12
from django.template import loader, Context
13 14 15


settings = django.conf.settings.FIREWALL_SETTINGS
Bach Dániel committed
16
logger = logging.getLogger(__name__)
17 18


Bach Dániel committed
19
class BuildFirewall:
20

Bach Dániel committed
21
    def __init__(self):
22
        self.chains = OrderedDict()
23

Bach Dániel committed
24 25 26 27 28
    def add_rules(self, *args, **kwargs):
        for chain_name, ipt_rule in kwargs.items():
            if chain_name not in self.chains:
                self.create_chain(chain_name)
            self.chains[chain_name].add(ipt_rule)
29

Bach Dániel committed
30 31
    def create_chain(self, chain_name):
        self.chains[chain_name] = IptChain(name=chain_name)
32

Bach Dániel committed
33
    def build_ipt_nat(self):
34
        # portforward
Bach Dániel committed
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
        for rule in Rule.objects.filter(
                nat=True, direction='in').select_related('host'):
            self.add_rules(PREROUTING=IptRule(
                priority=1000,
                dst=(rule.get_external_ipv4(), None),
                proto=rule.proto,
                dport=rule.get_external_port('ipv4'),
                extra='-j DNAT --to-destination %s:%s' % (rule.host.ipv4,
                                                          rule.dport)))

        # default outbound NAT rules for VLANs
        for vl_in in Vlan.objects.exclude(
                snat_ip=None).prefetch_related('snat_to'):
            for vl_out in vl_in.snat_to.all():
                self.add_rules(POSTROUTING=IptRule(
                    priority=1000,
                    src=(vl_in.network4, None),
                    extra='-o %s -j SNAT --to-source %s' % (
                        vl_out.name, vl_in.snat_ip)))
Őry Máté committed
54 55 56 57

    def ipt_filter_firewall(self):
        """Build firewall's own rules."""

Bach Dániel committed
58 59 60
        for rule in Rule.objects.exclude(firewall=None).select_related(
                'foreign_network').prefetch_related('foreign_network__vlans'):
            self.add_rules(**rule.get_ipt_rules())
61

Őry Máté committed
62 63 64
    def ipt_filter_host_rules(self):
        """Build hosts' rules."""

Bach Dániel committed
65 66 67 68 69 70 71 72 73 74 75
        # host rules
        for rule in Rule.objects.exclude(host=None).select_related(
                'foreign_network', 'host',
                'host__vlan').prefetch_related('foreign_network__vlans'):
            self.add_rules(**rule.get_ipt_rules(rule.host))
        # group rules
        for rule in Rule.objects.exclude(hostgroup=None).select_related(
                'hostgroup', 'foreign_network').prefetch_related(
                'hostgroup__host_set__vlan', 'foreign_network__vlans'):
            for host in rule.hostgroup.host_set.all():
                self.add_rules(**rule.get_ipt_rules(host))
76

Őry Máté committed
77 78 79
    def ipt_filter_vlan_rules(self):
        """Enable communication between VLANs."""

Bach Dániel committed
80 81 82 83
        for rule in Rule.objects.exclude(vlan=None).select_related(
                'vlan', 'foreign_network').prefetch_related(
                'foreign_network__vlans'):
            self.add_rules(**rule.get_ipt_rules())
84

Őry Máté committed
85 86 87
    def ipt_filter_vlan_drop(self):
        """Close intra-VLAN chains."""

Bach Dániel committed
88
        for chain in self.chains.values():
89
            close_chain_rule = IptRule(priority=1, action='LOG_DROP')
Bach Dániel committed
90
            chain.add(close_chain_rule)
91

Bach Dániel committed
92 93 94 95 96 97 98 99 100 101 102
    def ipt_filter_vlan_jump(self):
        """Create intra-VLAN jump rules."""

        vlans = Vlan.objects.all().values_list('name', flat=True)
        for vl_in, vl_out in product(vlans, repeat=2):
            name = '%s_%s' % (vl_in, vl_out)
            try:
                chain = self.chains[name]
            except KeyError:
                pass
            else:
103
                jump_rule = IptRule(priority=65535, action=chain.name,
Bach Dániel committed
104 105 106 107 108 109 110 111 112 113 114 115 116 117
                                    extra='-i %s -o %s' % (vl_in, vl_out))
                self.add_rules(FORWARD=jump_rule)

    def build_ipt(self):
        """Build rules."""

        self.ipt_filter_firewall()
        self.ipt_filter_host_rules()
        self.ipt_filter_vlan_rules()
        self.ipt_filter_vlan_jump()
        self.ipt_filter_vlan_drop()
        self.build_ipt_nat()

        context = {
118 119 120 121
            'filter': lambda: (chain for name, chain in self.chains.iteritems()
                               if chain.name not in IptChain.nat_chains),
            'nat': lambda: (chain for name, chain in self.chains.iteritems()
                            if chain.name in IptChain.nat_chains)}
Bach Dániel committed
122 123 124 125 126 127 128

        template = loader.get_template('firewall/iptables.conf')
        context['proto'] = 'ipv4'
        ipv4 = unicode(template.render(Context(context)))
        context['proto'] = 'ipv6'
        ipv6 = unicode(template.render(Context(context)))
        return (ipv4, ipv6)
129

130 131

def ipset():
132 133
    week = datetime.now() - timedelta(days=2)
    filter_ban = (Q(type='tempban', modified_at__gte=week) |
Bach Dániel committed
134 135
                  Q(type='permban'))
    return Blacklist.objects.filter(filter_ban).values('ipv4', 'reason')
136 137 138


def ipv6_to_octal(ipv6):
Bach Dániel committed
139
    ipv6 = IPAddress(ipv6, version=6)
140
    octets = []
Bach Dániel committed
141 142 143 144 145
    for part in ipv6.words:
        # Pad hex part to 4 digits.
        part = '%04x' % part
        octets.append(int(part[:2], 16))
        octets.append(int(part[2:], 16))
146
    return "".join(r"\%03o" % x for x in octets)
147

148

149 150 151 152 153 154 155
# =fqdn:ip:ttl          A, PTR
# &fqdn:ip:x:ttl        NS
# ZfqdnSOA
# +fqdn:ip:ttl          A
# ^                     PTR
# C                     CNAME
# :                     generic
156
# 'fqdn:s:ttl           TXT
157

158
def generate_ptr_records():
159 160
    DNS = []

Bach Dániel committed
161 162 163 164 165
    for host in Host.objects.order_by('vlan').all():
        template = host.vlan.reverse_domain
        i = host.get_external_ipv4().words
        reverse = (host.reverse if host.reverse not in [None, '']
                   else host.get_fqdn())
166

167 168
        # ipv4
        if host.ipv4:
Bach Dániel committed
169 170
            fqdn = template % {'a': i[0], 'b': i[1], 'c': i[2], 'd': i[3]}
            DNS.append("^%s:%s:%s" % (fqdn, reverse, settings['dns_ttl']))
171 172 173

        # ipv6
        if host.ipv6:
Bach Dániel committed
174
            DNS.append("^%s:%s:%s" % (host.ipv6.reverse_dns,
Bach Dániel committed
175
                                      reverse, settings['dns_ttl']))
Bach Dániel committed
176 177

    return DNS
178 179 180 181 182 183 184


def txt_to_octal(txt):
    return '\\' + '\\'.join(['%03o' % ord(x) for x in txt])


def generate_records():
Bach Dániel committed
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
    types = {'A': '+%(fqdn)s:%(address)s:%(ttl)s',
             'AAAA': ':%(fqdn)s:28:%(octal)s:%(ttl)s',
             'NS': '&%(fqdn)s::%(address)s:%(ttl)s',
             'CNAME': 'C%(fqdn)s:%(address)s:%(ttl)s',
             'MX': '@%(fqdn)s::%(address)s:%(dist)s:%(ttl)s',
             'PTR': '^%(fqdn)s:%(address)s:%(ttl)s',
             'TXT': '%(fqdn)s:%(octal)s:%(ttl)s'}

    retval = []

    for r in Record.objects.all():
        params = {'fqdn': r.fqdn, 'address': r.address, 'ttl': r.ttl}
        if r.type == 'MX':
            params['address'], params['dist'] = r.address.split(':', 2)
        if r.type == 'AAAA':
Bach Dániel committed
200 201 202 203 204 205
            try:
                params['octal'] = ipv6_to_octal(r.address)
            except AddrFormatError:
                logger.error('Invalid ipv6 address: %s, record: %s',
                             r.address, r)
                continue
Bach Dániel committed
206 207 208
        if r.type == 'TXT':
            params['octal'] = txt_to_octal(r.address)
        retval.append(types[r.type] % params)
209

Bach Dániel committed
210
    return retval
211 212 213 214 215 216 217 218 219


def dns():
    DNS = []

    # host PTR record
    DNS += generate_ptr_records()

    # domain SOA record
Bach Dániel committed
220
    for domain in Domain.objects.all():
221 222
        DNS.append("Z%s:%s:support.ik.bme.hu::::::%s" %
                   (domain.name, settings['dns_hostname'],
Bach Dániel committed
223
                    settings['dns_ttl']))
224 225 226

    # records
    DNS += generate_records()
227 228 229 230

    return DNS


231 232 233 234 235 236 237 238 239 240 241 242
class UniqueHostname(object):
    """Append vlan id if hostname already exists."""
    def __init__(self):
        self.used_hostnames = set()

    def get(self, hostname, vlan_id):
        if hostname in self.used_hostnames:
            hostname = "%s-%s" % (hostname, vlan_id)
        self.used_hostnames.add(hostname)
        return hostname


243 244
def dhcp():
    regex = re.compile(r'^([0-9]+)\.([0-9]+)\.[0-9]+\.[0-9]+\s+'
245
                       r'([0-9]+)\.([0-9]+)\.[0-9]+\.[0-9]+$')
246
    config = []
247

248
    VLAN_TEMPLATE = '''
249 250 251 252 253 254 255 256 257 258 259
    # %(name)s - %(interface)s
    subnet %(net)s netmask %(netmask)s {
      %(extra)s;
      option domain-name "%(domain)s";
      option routers %(router)s;
      option domain-name-servers %(dnsserver)s;
      option ntp-servers %(ntp)s;
      next-server %(tftp)s;
      authoritative;
      filename \"pxelinux.0\";
      allow bootp; allow booting;
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
    }'''

    HOST_TEMPLATE = '''
    host %(hostname)s {
        hardware ethernet %(mac)s;
        fixed-address %(ipv4)s;
    }'''

    unique_hostnames = UniqueHostname()

    for vlan in Vlan.objects.exclude(
            dhcp_pool=None).select_related(
            'domain').prefetch_related('host_set'):
        m = regex.search(vlan.dhcp_pool)
        if(m or vlan.dhcp_pool == "manual"):
            config.append(VLAN_TEMPLATE % {
                'net': str(vlan.network4.network),
                'netmask': str(vlan.network4.netmask),
                'domain': vlan.domain,
                'router': vlan.network4.ip,
                'ntp': vlan.network4.ip,
                'dnsserver': settings['rdns_ip'],
                'extra': ("range %s" % vlan.dhcp_pool
                          if m else "deny unknown-clients"),
                'interface': vlan.name,
                'name': vlan.name,
                'tftp': vlan.network4.ip})

            for host in vlan.host_set.all():
                config.append(HOST_TEMPLATE % {
                    'hostname': unique_hostnames.get(host.hostname, vlan.vid),
                    'mac': host.mac,
                    'ipv4': host.ipv4,
293 294
                })

295
    return config
296 297 298


def vlan():
Bach Dániel committed
299
    obj = Vlan.objects.values('vid', 'name', 'network4', 'network6')
300 301 302 303 304 305
    retval = {x['name']: {'tag': x['vid'],
                          'type': 'internal',
                          'interfaces': [x['name']],
                          'addresses': [str(x['network4']),
                                        str(x['network6'])]}
              for x in obj}
Bach Dániel committed
306
    for p in SwitchPort.objects.all():
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
        eth_count = p.ethernet_devices.count()
        if eth_count > 1:
            name = 'bond%d' % p.id
        elif eth_count == 1:
            name = p.ethernet_devices.get().name
        else:  # 0
            continue
        tag = p.untagged_vlan.vid
        retval[name] = {'tag': tag}
        if p.tagged_vlans is not None:
            trunk = list(p.tagged_vlans.vlans.values_list('vid', flat=True))
            retval[name]['trunks'] = sorted(trunk)
        retval[name]['interfaces'] = list(
            p.ethernet_devices.values_list('name', flat=True))
    return retval