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
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 15 16


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


Bach Dániel committed
20
class BuildFirewall:
21

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

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

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

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

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

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

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

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

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

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

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

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

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

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

135 136

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


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

153

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

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

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

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

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

    return DNS
183 184 185 186 187 188 189


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


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

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


def dns():
    DNS = []

    # host PTR record
    DNS += generate_ptr_records()

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

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

    return DNS


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


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

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

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

300
    return config
301 302 303


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