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

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


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


Bach Dániel committed
18
class BuildFirewall:
19

Bach Dániel committed
20 21
    def __init__(self):
        self.chains = {}
22

Bach Dániel committed
23 24 25 26 27
    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)
28

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

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

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

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

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

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

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

Bach Dániel committed
79 80 81 82
        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())
83

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

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

Bach Dániel committed
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
    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 = {
117 118 119 120
            '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
121 122 123 124 125 126 127

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

129 130

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


def ipv6_to_octal(ipv6):
Bach Dániel committed
138
    ipv6 = IPAddress(ipv6, version=6)
139
    octets = []
Bach Dániel committed
140 141 142 143 144
    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))
Bach Dániel committed
145
    return ''.join(['\\%03o' % x for x in octets])
146

147

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

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

Bach Dániel committed
160 161 162 163 164
    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())
165

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

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

    return DNS
177 178 179 180 181 182 183


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
    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
199 200 201 202 203 204
            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
205 206 207
        if r.type == 'TXT':
            params['octal'] = txt_to_octal(r.address)
        retval.append(types[r.type] % params)
208

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


def dns():
    DNS = []

    # host PTR record
    DNS += generate_ptr_records()

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

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

    return DNS


def dhcp():
    regex = re.compile(r'^([0-9]+)\.([0-9]+)\.[0-9]+\.[0-9]+\s+'
232
                       r'([0-9]+)\.([0-9]+)\.[0-9]+\.[0-9]+$')
233 234 235 236
    DHCP = []

# /tools/dhcp3/dhcpd.conf.generated

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

                for i_host in i_vlan.host_set.all():
268
                    DHCP.append('''
269 270 271 272 273 274 275 276 277 278
                    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
279 280 281


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