fw.py 10.8 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
        for rule in Rule.objects.filter(
36
                action__in=['accept', 'drop'],
Bach Dániel committed
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
                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
55 56 57 58

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

59 60
        rules = Rule.objects.filter(action__in=['accept', 'drop'])
        for rule in rules.exclude(firewall=None).select_related(
Bach Dániel committed
61 62
                'foreign_network').prefetch_related('foreign_network__vlans'):
            self.add_rules(**rule.get_ipt_rules())
63

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

Bach Dániel committed
67
        # host rules
68 69 70 71
        rules = Rule.objects.filter(action__in=['accept', 'drop'])
        for rule in rules.exclude(host=None).select_related(
                'foreign_network', 'host', 'host__vlan').prefetch_related(
                'foreign_network__vlans'):
Bach Dániel committed
72 73
            self.add_rules(**rule.get_ipt_rules(rule.host))
        # group rules
74
        for rule in rules.exclude(hostgroup=None).select_related(
Bach Dániel committed
75 76 77 78
                '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))
79

Őry Máté committed
80 81 82
    def ipt_filter_vlan_rules(self):
        """Enable communication between VLANs."""

83 84
        rules = Rule.objects.filter(action__in=['accept', 'drop'])
        for rule in rules.exclude(vlan=None).select_related(
Bach Dániel committed
85 86 87
                'vlan', 'foreign_network').prefetch_related(
                'foreign_network__vlans'):
            self.add_rules(**rule.get_ipt_rules())
88

Őry Máté committed
89 90 91
    def ipt_filter_vlan_drop(self):
        """Close intra-VLAN chains."""

Bach Dániel committed
92
        for chain in self.chains.values():
93
            close_chain_rule = IptRule(priority=1, action='LOG_DROP')
Bach Dániel committed
94
            chain.add(close_chain_rule)
95

Bach Dániel committed
96 97 98 99 100 101 102 103 104 105 106
    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:
107
                jump_rule = IptRule(priority=65535, action=chain.name,
Bach Dániel committed
108 109 110 111 112 113 114 115 116 117 118 119 120 121
                                    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 = {
122 123 124 125
            '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
126 127 128 129 130 131 132

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

134 135

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


def ipv6_to_octal(ipv6):
Bach Dániel committed
143
    ipv6 = IPAddress(ipv6, version=6)
144
    octets = []
Bach Dániel committed
145 146 147 148 149
    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))
150
    return "".join(r"\%03o" % x for x in octets)
151

152

153 154 155 156 157 158 159
# =fqdn:ip:ttl          A, PTR
# &fqdn:ip:x:ttl        NS
# ZfqdnSOA
# +fqdn:ip:ttl          A
# ^                     PTR
# C                     CNAME
# :                     generic
160
# 'fqdn:s:ttl           TXT
161

162
def generate_ptr_records():
163 164
    DNS = []

Bach Dániel committed
165 166 167 168 169
    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())
170

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

        # ipv6
        if host.ipv6:
Bach Dániel committed
178
            DNS.append("^%s:%s:%s" % (host.ipv6.reverse_dns,
Bach Dániel committed
179
                                      reverse, settings['dns_ttl']))
Bach Dániel committed
180 181

    return DNS
182 183 184 185 186 187 188


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


def generate_records():
Bach Dániel committed
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
    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
204 205 206 207 208 209
            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
210 211 212
        if r.type == 'TXT':
            params['octal'] = txt_to_octal(r.address)
        retval.append(types[r.type] % params)
213

Bach Dániel committed
214
    return retval
215 216 217 218 219 220 221 222 223


def dns():
    DNS = []

    # host PTR record
    DNS += generate_ptr_records()

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

    # records
    DNS += generate_records()
231 232 233 234

    return DNS


235 236 237 238 239 240 241 242 243 244 245 246
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


247 248
def dhcp():
    regex = re.compile(r'^([0-9]+)\.([0-9]+)\.[0-9]+\.[0-9]+\s+'
249
                       r'([0-9]+)\.([0-9]+)\.[0-9]+\.[0-9]+$')
250
    config = []
251

252
    VLAN_TEMPLATE = '''
253 254 255 256 257 258 259 260 261 262 263
    # %(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;
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 293 294 295 296
    }'''

    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,
297 298
                })

299
    return config
300 301 302


def vlan():
Bach Dániel committed
303
    obj = Vlan.objects.values('vid', 'name', 'network4', 'network6')
304 305 306 307 308 309
    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
310
    for p in SwitchPort.objects.all():
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
        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