fw.py 10.1 KB
Newer Older
1 2
import re
from datetime import datetime, timedelta
Bach Dániel committed
3 4 5 6 7
from itertools import product

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


settings = django.conf.settings.FIREWALL_SETTINGS
13 14


Bach Dániel committed
15
class BuildFirewall:
16

Bach Dániel committed
17 18
    def __init__(self):
        self.chains = {}
19

Bach Dániel committed
20 21 22 23 24
    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)
25

Bach Dániel committed
26 27
    def create_chain(self, chain_name):
        self.chains[chain_name] = IptChain(name=chain_name)
28

Bach Dániel committed
29
    def build_ipt_nat(self):
30
        # portforward
Bach Dániel committed
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
        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
50 51 52 53

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

Bach Dániel committed
54 55 56
        for rule in Rule.objects.exclude(firewall=None).select_related(
                'foreign_network').prefetch_related('foreign_network__vlans'):
            self.add_rules(**rule.get_ipt_rules())
57

Őry Máté committed
58 59 60
    def ipt_filter_host_rules(self):
        """Build hosts' rules."""

Bach Dániel committed
61 62 63 64 65 66 67 68 69 70 71
        # 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))
72

Őry Máté committed
73 74 75
    def ipt_filter_vlan_rules(self):
        """Enable communication between VLANs."""

Bach Dániel committed
76 77 78 79
        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())
80

Őry Máté committed
81 82 83
    def ipt_filter_vlan_drop(self):
        """Close intra-VLAN chains."""

Bach Dániel committed
84 85 86
        for chain in self.chains.values():
            close_chain_rule = IptRule(priority=65534, action='LOG_DROP')
            chain.add(close_chain_rule)
87

Bach Dániel committed
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
    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:
                jump_rule = IptRule(priority=1, action=chain.name,
                                    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 = {
114 115 116 117
            '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
118 119 120 121 122 123 124

        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)
125

126 127

def ipset():
128 129 130
    week = datetime.now() - timedelta(days=2)
    filter_ban = (Q(type='tempban', modified_at__gte=week) |
                  Q(type='permban')).values('ipv4', 'reason')
Bach Dániel committed
131
    return Blacklist.objects.filter(filter_ban)
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147


def ipv6_to_octal(ipv6):
    while len(ipv6.split(':')) < 8:
        ipv6 = ipv6.replace('::', ':::')
    octets = []
    for part in ipv6.split(':'):
        if not part:
            octets.extend([0, 0])
        else:
            # Pad hex part to 4 digits.
            part = '%04x' % int(part, 16)
            octets.append(int(part[:2], 16))
            octets.append(int(part[2:], 16))
    return '\\' + '\\'.join(['%03o' % x for x in octets])

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']))
176 177 178 179 180 181 182 183
        return DNS


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


def generate_records():
Bach Dániel committed
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
    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':
            params['octal'] = ipv6_to_octal(r.address)
        if r.type == 'TXT':
            params['octal'] = txt_to_octal(r.address)
        retval.append(types[r.type] % params)
203

Bach Dániel committed
204
    return retval
205 206 207 208 209 210 211 212 213


def dns():
    DNS = []

    # host PTR record
    DNS += generate_ptr_records()

    # domain SOA record
Bach Dániel committed
214
    for domain in Domain.objects.all():
215 216
        DNS.append("Z%s:%s:support.ik.bme.hu::::::%s" %
                   (domain.name, settings['dns_hostname'],
Bach Dániel committed
217
                    settings['dns_ttl']))
218 219 220

    # records
    DNS += generate_records()
221 222 223 224 225 226

    return DNS


def dhcp():
    regex = re.compile(r'^([0-9]+)\.([0-9]+)\.[0-9]+\.[0-9]+\s+'
227
                       r'([0-9]+)\.([0-9]+)\.[0-9]+\.[0-9]+$')
228 229 230 231
    DHCP = []

# /tools/dhcp3/dhcpd.conf.generated

Bach Dániel committed
232
    for i_vlan in Vlan.objects.all():
233 234 235
        if(i_vlan.dhcp_pool):
            m = regex.search(i_vlan.dhcp_pool)
            if(m or i_vlan.dhcp_pool == "manual"):
236
                DHCP.append('''
237 238 239 240 241 242 243 244 245 246 247
    # %(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;
248
    }''' % {
249 250
                    'net': str(i_vlan.network4.network),
                    'netmask': str(i_vlan.network4.netmask),
251 252 253 254 255
                    'domain': i_vlan.domain,
                    'router': i_vlan.ipv4,
                    'ntp': i_vlan.ipv4,
                    'dnsserver': settings['rdns_ip'],
                    'extra': ("range %s" % i_vlan.dhcp_pool
256
                              if m else "deny unknown-clients"),
257
                    'interface': i_vlan.name,
258 259 260 261 262
                    'name': i_vlan.name,
                    'tftp': i_vlan.ipv4
                })

                for i_host in i_vlan.host_set.all():
263
                    DHCP.append('''
264 265 266 267 268 269 270 271 272 273
                    host %(hostname)s {
                      hardware ethernet %(mac)s;
                      fixed-address %(ipv4)s;
                    }''' % {
                        'hostname': i_host.hostname,
                        'mac': i_host.mac,
                        'ipv4': i_host.ipv4,
                    })

    return DHCP
274 275 276


def vlan():
Bach Dániel committed
277
    obj = Vlan.objects.values('vid', 'name', 'network4', 'network6')
278 279 280 281 282 283
    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
284
    for p in SwitchPort.objects.all():
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
        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