fw.py 10.9 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 timedelta
Bach Dániel committed
6 7
from itertools import product

8 9
from .models import (Host, Rule, Vlan, Domain, Record, BlacklistItem,
                     SwitchPort)
Bach Dániel committed
10 11
from .iptables import IptRule, IptChain
import django.conf
12
from django.db.models import Q
Bach Dániel committed
13
from django.template import loader, Context
14
from django.utils import timezone
15 16 17


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


Bach Dániel committed
21
class BuildFirewall:
22

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

Bach Dániel committed
26 27 28 29 30
    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)
31

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

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

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

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

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

Bach Dániel committed
69
        # host rules
70 71 72 73
        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
74 75
            self.add_rules(**rule.get_ipt_rules(rule.host))
        # group rules
76
        for rule in rules.exclude(hostgroup=None).select_related(
Bach Dániel committed
77 78 79 80
                '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))
81

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

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

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

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

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

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

136 137

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


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

154

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

164
def generate_ptr_records():
165 166
    DNS = []

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

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

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

    return DNS
184 185 186 187 188 189 190


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


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

Bach Dániel committed
216
    return retval
217 218 219 220 221 222 223 224 225


def dns():
    DNS = []

    # host PTR record
    DNS += generate_ptr_records()

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

    # records
    DNS += generate_records()
233 234 235 236

    return DNS


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


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

254
    VLAN_TEMPLATE = '''
255 256 257 258 259 260 261 262 263 264 265
    # %(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;
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 297 298
    }'''

    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,
299 300
                })

301
    return config
302 303 304


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