fw.py 17 KB
Newer Older
1 2 3 4 5 6 7 8 9
from firewall import models
import django.conf

import re
from datetime import datetime, timedelta
from django.db.models import Q


settings = django.conf.settings.FIREWALL_SETTINGS
10 11


12 13 14 15 16 17 18 19 20
class Firewall:
    def dportsport(self, rule, repl=True):
        retval = ' '
        if rule.proto == 'tcp' or rule.proto == 'udp':
            retval = '-p %s ' % rule.proto
            if rule.sport:
                retval += ' --sport %s ' % rule.sport
            if rule.dport:
                retval += ' --dport %s ' % (rule.nat_dport
21 22
                          if (repl and rule.nat and rule.direction == '1')
                          else rule.dport)
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
        elif rule.proto == 'icmp':
            retval = '-p %s ' % rule.proto
        return retval

    def iptables(self, s):
        """Append rule to filter table."""
        self.RULES.append(s)

    def iptablesnat(self, s):
        """Append rule to NAT table."""
        self.RULES_NAT.append(s)

    def host2vlan(self, host, rule):
        if not rule.foreign_network:
            return

39
        if self.proto == 6 and host.ipv6:
40
            ipaddr = str(host.ipv6) + '/112'
41
        else:
42
            ipaddr = str(host.ipv4)
43 44 45 46 47 48 49 50

        dport_sport = self.dportsport(rule)

        for vlan in rule.foreign_network.vlans.all():
            if rule.accept:
                if rule.direction == '0' and vlan.name == 'PUB':
                    if rule.dport == 25:
                        self.iptables('-A PUB_OUT -s %s %s -p tcp '
51 52
                                      '--dport 25 -j LOG_ACC' %
                                      (ipaddr, rule.extra))
53 54 55 56 57 58 59
                        break
                    action = 'PUB_OUT'
                else:
                    action = 'LOG_ACC'
            else:
                action = 'LOG_DROP'

60 61
            if rule.direction == '1':  # going TO host
                self.iptables('-A %s_%s -d %s %s %s -g %s' %
62
                              (vlan.name, host.vlan.name, ipaddr, dport_sport,
63
                               rule.extra, action))
64
            else:
65
                self.iptables('-A %s_%s -s %s %s %s -g %s' %
66
                              (host.vlan.name, vlan.name, ipaddr, dport_sport,
67
                               rule.extra, action))
68 69 70 71 72 73 74 75

    def fw2vlan(self, rule):
        if not rule.foreign_network:
            return

        dport_sport = self.dportsport(rule)

        for vlan in rule.foreign_network.vlans.all():
76
            if rule.direction == '1':  # going TO host
77
                self.iptables('-A INPUT -i %s %s %s -g %s' %
78
                              (vlan.name, dport_sport, rule.extra,
79
                               'LOG_ACC' if rule.accept else 'LOG_DROP'))
80 81
            else:
                self.iptables('-A OUTPUT -o %s %s %s -g %s' %
82
                              (vlan.name, dport_sport, rule.extra,
83
                               'LOG_ACC' if rule.accept else 'LOG_DROP'))
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99

    def vlan2vlan(self, l_vlan, rule):
        if not rule.foreign_network:
            return

        dport_sport = self.dportsport(rule)

        for vlan in rule.foreign_network.vlans.all():
            if rule.accept:
                if rule.direction == '0' and vlan.name == 'PUB':
                    action = 'PUB_OUT'
                else:
                    action = 'LOG_ACC'
            else:
                action = 'LOG_DROP'

100 101
            if rule.direction == '1':  # going TO host
                self.iptables('-A %s_%s %s %s -g %s' %
102 103
                              (vlan.name, l_vlan.name, dport_sport,
                               rule.extra, action))
104
            else:
105
                self.iptables('-A %s_%s %s %s -g %s' % (l_vlan.name, vlan.name,
106 107
                                                        dport_sport,
                                                        rule.extra, action))
108 109 110 111 112 113 114 115 116 117 118 119 120

    def prerun(self):
        self.iptables('*filter')
        self.iptables(':INPUT DROP [88:6448]')
        self.iptables(':FORWARD DROP [0:0]')
        self.iptables(':OUTPUT DROP [50:6936]')

        # initialize logging
        self.iptables('-N LOG_DROP')
        # windows port scan are silently dropped
        self.iptables('-A LOG_DROP -p tcp --dport 445 -j DROP')
        self.iptables('-A LOG_DROP -p udp --dport 137 -j DROP')
        self.iptables('-A LOG_DROP -j LOG --log-level 7 '
121
                      '--log-prefix "[ipt][drop]"')
122 123 124
        self.iptables('-A LOG_DROP -j DROP')
        self.iptables('-N LOG_ACC')
        self.iptables('-A LOG_ACC -j LOG --log-level 7 '
125
                      '--log-prefix "[ipt][isok]"')
126 127 128 129
        self.iptables('-A LOG_ACC -j ACCEPT')

        self.iptables('-N PUB_OUT')

130 131
        self.iptables('-A FORWARD -m set --match-set blacklist src,dst '
                      '-j DROP')
132 133
        self.iptables('-A FORWARD -m state --state INVALID -g LOG_DROP')
        self.iptables('-A FORWARD -m state --state ESTABLISHED,RELATED '
134
                      '-j ACCEPT')
135
        self.iptables('-A FORWARD -p icmp --icmp-type echo-request '
136
                      '-g LOG_ACC')
137 138 139 140 141

        self.iptables('-A INPUT -m set --match-set blacklist src -j DROP')
        self.iptables('-A INPUT -m state --state INVALID -g LOG_DROP')
        self.iptables('-A INPUT -i lo -j ACCEPT')
        self.iptables('-A INPUT -m state --state ESTABLISHED,RELATED '
142
                      '-j ACCEPT')
143 144 145 146

        self.iptables('-A OUTPUT -m state --state INVALID -g LOG_DROP')
        self.iptables('-A OUTPUT -o lo -j ACCEPT')
        self.iptables('-A OUTPUT -m state --state ESTABLISHED,RELATED '
147
                      '-j ACCEPT')
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172

    def postrun(self):
        self.iptables('-A PUB_OUT -p tcp --dport 25 -j LOG_DROP')
        self.iptables('-A PUB_OUT -p tcp --dport 445 -j LOG_DROP')
        self.iptables('-A PUB_OUT -p udp --dport 445 -j LOG_DROP')

        self.iptables('-A PUB_OUT -g LOG_ACC')
        self.iptables('-A FORWARD -g LOG_DROP')
        self.iptables('-A INPUT -g LOG_DROP')
        self.iptables('-A OUTPUT -g LOG_DROP')
        self.iptables('COMMIT')

    def ipt_nat(self):
        self.iptablesnat('*nat')
        self.iptablesnat(':PREROUTING ACCEPT [0:0]')
        self.iptablesnat(':INPUT ACCEPT [0:0]')
        self.iptablesnat(':OUTPUT ACCEPT [1:708]')
        self.iptablesnat(':POSTROUTING ACCEPT [1:708]')

        # portforward
        for host in self.hosts.exclude(pub_ipv4=None):
            for rule in host.rules.filter(nat=True, direction='1'):
                dport_sport = self.dportsport(rule, False)
                if host.vlan.snat_ip:
                    self.iptablesnat('-A PREROUTING -d %s %s %s -j DNAT '
173 174 175
                                     '--to-destination %s:%s' %
                                     (host.pub_ipv4, dport_sport, rule.extra,
                                      host.ipv4, rule.nat_dport))
176 177 178 179 180

        # rules for machines with dedicated public IP
        for host in self.hosts.exclude(shared_ip=True):
            if host.pub_ipv4:
                self.iptablesnat('-A PREROUTING -d %s -j DNAT '
181 182
                                 '--to-destination %s' %
                                 (host.pub_ipv4, host.ipv4))
183
                self.iptablesnat('-A POSTROUTING -s %s -j SNAT '
184 185
                                 '--to-source %s' %
                                 (host.ipv4, host.pub_ipv4))
186 187 188 189 190 191

        # default NAT rules for VLANs
        for s_vlan in self.vlans:
            if s_vlan.snat_ip:
                for d_vlan in s_vlan.snat_to.all():
                    self.iptablesnat('-A POSTROUTING -s %s -o %s -j SNAT '
192
                                     '--to-source %s' %
193
                                     (str(s_vlan.network4), d_vlan.name,
194
                                      s_vlan.snat_ip))
195 196 197 198 199 200 201

        self.iptablesnat('COMMIT')

    def ipt_filter(self):

        self.prerun()

Őry Máté committed
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
        self.ipt_filter_firewall()
        self.ipt_filter_zones()
        self.ipt_filter_host_rules()
        self.ipt_filter_vlan_rules()
        self.ipt_filter_vlan_drop()

        self.postrun()

        if self.proto == 6:  # remove ipv4-specific rules
            ipv4_re = re.compile('([0-9]{1,3}\.){3}[0-9]{1,3}')
            self.RULES = [x for x in self.RULES if not ipv4_re.search(x)]
            self.RULES = [x.replace('icmp', 'icmpv6') for x in self.RULES]

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

218 219 220 221
        for f in self.fw:
            for rule in f.rules.all():
                self.fw2vlan(rule)

Őry Máté committed
222 223 224
    def ipt_filter_zones(self):
        """Jumping to chains between zones."""

225 226
        for s_vlan in self.vlans:
            for d_vlan in self.vlans:
227
                self.iptables('-N %s_%s' % (s_vlan.name, d_vlan.name))
228
                self.iptables('-A FORWARD -i %s -o %s -g %s_%s' %
229 230
                              (s_vlan.name, d_vlan.name, s_vlan.name,
                               d_vlan.name))
231

Őry Máté committed
232 233 234
    def ipt_filter_host_rules(self):
        """Build hosts' rules."""

235 236 237 238 239 240 241 242
        for i_vlan in self.vlans:
            for i_host in i_vlan.host_set.all():
                for group in i_host.groups.all():
                    for rule in group.rules.all():
                        self.host2vlan(i_host, rule)
                for rule in i_host.rules.all():
                    self.host2vlan(i_host, rule)

Őry Máté committed
243 244 245
    def ipt_filter_vlan_rules(self):
        """Enable communication between VLANs."""

246 247 248 249
        for s_vlan in self.vlans:
            for rule in s_vlan.rules.all():
                self.vlan2vlan(s_vlan, rule)

Őry Máté committed
250 251 252
    def ipt_filter_vlan_drop(self):
        """Close intra-VLAN chains."""

253 254
        for s_vlan in self.vlans:
            for d_vlan in self.vlans:
255 256
                self.iptables('-A %s_%s -g LOG_DROP' % (s_vlan.name,
                                                        d_vlan.name))
257

258
    def __init__(self, proto=4):
259 260
        self.RULES = []
        self.RULES_NAT = []
261
        self.proto = proto
262 263 264 265
        self.vlans = models.Vlan.objects.all()
        self.hosts = models.Host.objects.all()
        self.fw = models.Firewall.objects.all()
        self.ipt_filter()
266
        if self.proto != 6:
267 268 269
            self.ipt_nat()

    def get(self):
270
        if self.proto == 6:
271
            return {'filter': self.RULES, }
272
        else:
273
            return {'filter': self.RULES, 'nat': self.RULES_NAT}
274 275

    def show(self):
276
        if self.proto == 6:
277 278 279
            return '\n'.join(self.RULES) + '\n'
        else:
            return ('\n'.join(self.RULES) + '\n' +
280 281
                    '\n'.join(self.RULES_NAT) + '\n')

282 283

def ipset():
284 285 286 287
    week = datetime.now() - timedelta(days=2)
    filter_ban = (Q(type='tempban', modified_at__gte=week) |
                  Q(type='permban')).values('ipv4', 'reason')
    return models.Blacklist.objects.filter(filter_ban)
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303


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

304

305 306 307 308
def ipv4_to_arpa(ipv4, cname=False):
    m2 = re.search(r'^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$', ipv4)
    if cname:
        return ('%s.dns1.%s.%s.%s.in-addr.arpa' %
309
                (m2.group(4), m2.group(3), m2.group(2), m2.group(1)))
310 311
    else:
        return ('%s.%s.%s.%s.in-addr.arpa' %
312 313
                (m2.group(4), m2.group(3), m2.group(2), m2.group(1)))

314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338

def ipv6_to_arpa(ipv6):
    while len(ipv6.split(':')) < 8:
        ipv6 = ipv6.replace('::', ':::')
    octets = []
    for part in ipv6.split(':'):
        if not part:
            octets.extend([0, 0, 0, 0])
        else:
            # Pad hex part to 4 digits.
            part = '%04x' % int(part, 16)
            octets.insert(0, int(part[0], 16))
            octets.insert(0, int(part[1], 16))
            octets.insert(0, int(part[2], 16))
            octets.insert(0, int(part[3], 16))
    return '.'.join(['%1x' % x for x in octets]) + '.ip6.arpa'


# =fqdn:ip:ttl          A, PTR
# &fqdn:ip:x:ttl        NS
# ZfqdnSOA
# +fqdn:ip:ttl          A
# ^                     PTR
# C                     CNAME
# :                     generic
339
# 'fqdn:s:ttl           TXT
340

341
def generate_ptr_records():
342 343
    DNS = []

344 345 346 347 348 349 350
    for host in models.Host.objects.order_by('vlan').all():
        rev = host.vlan.reverse_domain
        ipv4 = str(host.pub_ipv4 if host.pub_ipv4 and
                   not host.shared_ip else host.ipv4)
        i = ipv4.split('.', 4)
        reverse = (host.reverse if host.reverse and
                   len(host.reverse) else host.get_fqdn())
351

352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
        # ipv4
        if host.ipv4:
            DNS.append("^%s:%s:%s" % (
                (rev % {'a': int(i[0]), 'b': int(i[1]), 'c': int(i[2]),
                        'd': int(i[3])}),
                reverse, models.settings['dns_ttl']))

        # ipv6
        if host.ipv6:
            DNS.append("^%s:%s:%s" % (ipv6_to_arpa(str(host.ipv6)),
                                      reverse, models.settings['dns_ttl']))
        return DNS


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


def generate_records():
    DNS = []
372 373

    for r in models.Record.objects.all():
374 375 376
        if r.type == 'A':
            DNS.append("+%s:%s:%s" % (r.fqdn, r.address, r.ttl))
        elif r.type == 'AAAA':
377
            DNS.append(":%s:28:%s:%s" %
378 379 380 381 382 383 384
                       (r.fqdn, ipv6_to_octal(r.address), r.ttl))
        elif r.type == 'NS':
            DNS.append("&%s::%s:%s" % (r.fqdn, r.address, r.ttl))
        elif r.type == 'CNAME':
            DNS.append("C%s:%s:%s" % (r.fqdn, r.address, r.ttl))
        elif r.type == 'MX':
            mx = r.address.split(':', 2)
385
            DNS.append("@%(fqdn)s::%(mx)s:%(dist)s:%(ttl)s" %
386 387 388 389
                       {'fqdn': r.fqdn, 'mx': mx[1], 'dist': mx[0],
                        'ttl': r.ttl})
        elif r.type == 'PTR':
            DNS.append("^%s:%s:%s" % (r.fqdn, r.address, r.ttl))
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410
        elif r.type == 'TXT':
            DNS.append("'%s:%s:%s" % (r.fqdn,
                                      txt_to_octal(r.address), r.ttl))

    return DNS


def dns():
    DNS = []

    # host PTR record
    DNS += generate_ptr_records()

    # domain SOA record
    for domain in models.Domain.objects.all():
        DNS.append("Z%s:%s:support.ik.bme.hu::::::%s" %
                   (domain.name, settings['dns_hostname'],
                    models.settings['dns_ttl']))

    # records
    DNS += generate_records()
411 412 413 414 415 416

    return DNS


def dhcp():
    regex = re.compile(r'^([0-9]+)\.([0-9]+)\.[0-9]+\.[0-9]+\s+'
417
                       r'([0-9]+)\.([0-9]+)\.[0-9]+\.[0-9]+$')
418 419 420 421
    DHCP = []

# /tools/dhcp3/dhcpd.conf.generated

422
    for i_vlan in models.Vlan.objects.all():
423 424 425
        if(i_vlan.dhcp_pool):
            m = regex.search(i_vlan.dhcp_pool)
            if(m or i_vlan.dhcp_pool == "manual"):
426
                DHCP.append('''
427 428 429 430 431 432 433 434 435 436 437
    # %(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;
438
    }''' % {
439 440
                    'net': str(i_vlan.network4.network),
                    'netmask': str(i_vlan.network4.netmask),
441 442 443 444 445
                    'domain': i_vlan.domain,
                    'router': i_vlan.ipv4,
                    'ntp': i_vlan.ipv4,
                    'dnsserver': settings['rdns_ip'],
                    'extra': ("range %s" % i_vlan.dhcp_pool
446
                              if m else "deny unknown-clients"),
447
                    'interface': i_vlan.name,
448 449 450 451 452
                    'name': i_vlan.name,
                    'tftp': i_vlan.ipv4
                })

                for i_host in i_vlan.host_set.all():
453
                    DHCP.append('''
454 455 456 457 458 459 460 461 462 463
                    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
464 465 466 467


def vlan():
    obj = models.Vlan.objects.values('vid', 'name', 'network4', 'network6')
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489
    retval = {x['name']: {'tag': x['vid'],
                          'type': 'internal',
                          'interfaces': [x['name']],
                          'addresses': [str(x['network4']),
                                        str(x['network6'])]}
              for x in obj}
    for p in models.SwitchPort.objects.all():
        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