firewall6.py 13.4 KB
Newer Older
1
#!/usr/bin/python
2
# -*- mode: python; coding: utf-8 -*-
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
#
# FIREWALL6.PY -- Gestion du firewall pour l'ipv6
#
# Copyright (C) 2010 Olivier Huber
# Authors: Olivier Huber <huber@crans.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


23 24 25 26
import sys
import re
import os
import pwd
27 28 29 30

sys.path.append('/usr/scripts/gestion')

from ldap_crans import hostname
31
from config import rid, prefix, role, file_pickle, open_ports, p2p
Daniel STAN's avatar
Daniel STAN committed
32
from config import authorized_icmpv6, adm_only, adm_users
33 34 35 36 37 38
from ipt import *

# On invoque Ip6tables
ip6tables = Ip6tables()

def aide():
Nicolas Dandrimont's avatar
Nicolas Dandrimont committed
39
    """ Affiche l'aide pour utiliser le script"""
40 41 42 43 44 45 46
    print """
Usage:
%(script)s start        : Démarrage du firewall
%(script)s stop         : Arrêt du firewall
%(script)s restart      : Redémarrage du firewall
""" % { 'script' : sys.argv[0].split('/')[-1] }

47
def ports(dev_ip6, dev_list):
48 49
    ''' Ouvre les ports '''
    for machine in machines :
Daniel STAN's avatar
Daniel STAN committed
50 51
        if not machine.rid():
            continue
52
        for type_machine in ['fil', 'adherents-v6', 'wifi', 'wifi-adh-v6', 'serveurs']:
53 54 55 56
            for plage in rid[type_machine]:
                if int(machine.rid()) in range(plage[0], plage[1]):
                    for dev in dev_list:
                        ports_io(ip6tables, machine, type_machine, dev_ip6, dev)
57 58 59 60 61 62

    #Protection contre les attaques brute-force
    # XXX FIXIT !!!
    # Il semble qu'il faille un kernel >= .29 et iptables >= 1.4.3
    # http://netfilter.org/projects/iptables/files/changes-iptables-1.4.3.txt

63 64
    ip6tables.filter.forward('-i %s -p tcp --dport ssh -m state --state NEW -m recent --name SSH2 --set ' % dev_ip6)
    ip6tables.filter.forward('-i %s -p tcp --dport ssh -m state --state NEW -m recent --name SSH2 --update --seconds 30 --hitcount 10 --rttl -j DROP' % dev_ip6)
65 66 67
    ip6tables.filter.input('-i %s -p tcp --dport ssh -m state --state NEW -m recent --name SSH --set ' % dev_ip6)
    ip6tables.filter.input('-i %s -p tcp --dport ssh -m state --state NEW -m recent --name SSH --update --seconds 120 --hitcount 10 --rttl -j DROP' % dev_ip6)
    #ip6tables.filter.forward('-i %s -p tcp --dport ssh -m state --state NEW -j ACCEPT' % dev_ip6)
68

69
    for proto in open_ports.keys():
Nicolas Dandrimont's avatar
Nicolas Dandrimont committed
70
        ip6tables.filter.forward('-i %s -p %s -m multiport --dports %s -j ACCEPT' % (dev_ip6, proto, open_ports[proto]))
71
    for type_machine in ['fil', 'adherents-v6', 'wifi', 'wifi-adh-v6']:
72 73 74
        ip6tables.filter.forward('-i %s -d %s -j %s' % (dev_ip6,
            prefix[dprefix[type_machine]][0], 'EXT' + re.sub('-', '',
                type_machine.upper())))
Nicolas Dandrimont's avatar
Nicolas Dandrimont committed
75
        eval('ip6tables.filter.ext' + re.sub('-', '', type_machine))('-j REJECT --reject-with icmp6-port-unreachable')
76

77
    # Port ouvert CRANS->EXT
78 79 80 81
    for dev in dev_list:
        ip6tables.filter.forward('-i %s -p udp -m multiport --dports 0:136,140:65535 -j ACCEPT' % dev)
        # FIXME: proxy transparent -> port 80
        ip6tables.filter.forward('-i %s -p tcp -m multiport --dports 0:24,26:79,80,81:134,136,140:444,446:65535 -j ACCEPT' % dev)
82

83
    for type_machine in ['fil', 'adherents-v6', 'wifi', 'wifi-adh-v6']:
84
        ip6tables.filter.forward('-i %s -s %s -j %s' % (iface6(type_machine),
85 86
            prefix[dprefix[type_machine]][0], 'CRANS' + re.sub('-', '',
                type_machine.upper())))
Nicolas Dandrimont's avatar
Nicolas Dandrimont committed
87
        eval('ip6tables.filter.crans' + re.sub('-', '', type_machine))('-j REJECT --reject-with icmp6-port-unreachable')
88 89 90 91 92 93 94




def basic_fw():
    ''' Met en place un firewall de base commun à tous les serveurs'''
    # On rejete les ra.
Valentin Samir's avatar
Valentin Samir committed
95
    ip6tables.filter.input('-p icmpv6 -m icmp6 --icmpv6-type router-advertisement -j REJECT')
96

97
    # On accepte NDP sauf les RA, sinon, les REJECT ne fonctionnent pas
98 99 100
    for icmpv6 in ['neighbour-solicitation','neighbour-advertisement','redirect','router-solicitation']:
        ip6tables.filter.input('-p icmpv6 -m icmp6 --icmpv6-type %s -j ACCEPT' % icmpv6)
        ip6tables.filter.output('-p icmpv6 -m icmp6 --icmpv6-type %s -j ACCEPT' % icmpv6)
101

102 103 104 105
    # on accepte les ping
    for icmpv6 in authorized_icmpv6:
        ip6tables.filter.forward('-p icmpv6 -m icmp6 --icmpv6-type %s -j ACCEPT' % icmpv6)
    ip6tables.filter.forward('-p icmpv6 -j REJECT')
106

107 108 109
    # On ne vérifie rien sur les ip qui ne sont pas dans notre prefix
    for net in prefix['subnet']:
        ip6tables.filter.ieui64('! -s %s -j RETURN' % net)
110

111
    # Correspondance MAC-IP
112
    mac_ip(ip6tables, machines, ['fil', 'adherents-v6', 'adm', 'wifi', 'wifi-adh-v6', 'serveurs'])
113 114 115 116


def main_router():
    ''' Firewall pour le router principal '''
117

118 119 120 121 122 123
    #TODO : réseaux non routable, interaction avec generate
    # il faut aussi voir les conditions pour passer la ctstate avant MAC-IP
    # (normalement, il n'y a pas de problèmes.
    # et peut être aussi avant blackliste (il faut prévoir un script qui
    # enlève les entrées dans la conntract lors de la mise en place de la
    # blackliste
124

125

126
    dev_crans = iface6('fil')
127
    dev_wifi = iface6('wifi')
Daniel STAN's avatar
Daniel STAN committed
128
    dev_ip6 = iface6('sixxs2')
129

130 131 132 133
    ip6tables.mangle.forward("-o %s -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu" % dev_ip6)
    ip6tables.mangle.forward("-o %s -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu" % dev_wifi)
    ip6tables.mangle.forward("-o %s -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu" % dev_crans)

134 135 136
    ip6tables.mangle.postrouting('-o %s -m state --state NEW -j LOG --log-prefix "LOG_ALL "' % dev_crans)
    ip6tables.mangle.postrouting('-o %s -m state --state NEW -j LOG --log-prefix "LOG_ALL "' % dev_wifi)
    ip6tables.mangle.postrouting('-o %s -m state --state NEW -j LOG --log-prefix "LOG_ALL "' %  dev_ip6 )
137

138
    # On force le /32 de google à passer en ipv4 pour tester si ça soulage le tunnel ipv6
139
    ip6tables.filter.forward('-o %s -p tcp -d 2a00:1450:4006::/32 -j REJECT --reject-with icmp6-addr-unreachable' % dev_ip6)
140

141 142 143
    # Ipv6 sur évènementiel, on ne laisse sortir que si ça vient de la mac d'ytrap-llatsni
    ip6tables.filter.forward('-o %s -d 2a01:240:fe3d:d2::/64 -j ACCEPT' % dev_crans)
    ip6tables.filter.forward('-o %s -m mac --mac-source 00:00:6c:69:69:01 -s 2a01:240:fe3d:d2::/64 -j ACCEPT' % dev_ip6)
144

145
    # Les blacklistes
huber's avatar
huber committed
146 147 148 149 150
    # Si on les met après la règle conntrack, une connexion existante ne sera
    # pas sevrée et dinc avec un tunnel ssh idoine, la blacklist aurait aucun
    # effet.
    # Alternative : flusher la table conntrack des entrées concernant cette
    # machine.
151
    blacklist(ip6tables)
152
    ip6tables.filter.forward('-i %s -j BLACKLIST_SRC' % dev_crans)
153
    ip6tables.filter.forward('-i %s -j BLACKLIST_SRC' % dev_wifi)
154
    ip6tables.filter.forward('-i %s -j BLACKLIST_DST' % dev_ip6)
155

156 157 158 159
    #tracker_torrent(ip6tables)
    #ip6tables.filter.forward('-o %s -p udp -j TRACKER_TORRENT' % dev_ip6 )
    #ip6tables.filter.forward('-o %s -p tcp -m string --algo kmp --string "GET /" -j TRACKER_TORRENT' % dev_ip6)
    #ip6tables.filter.forward('-o %s -p tcp -m string --algo kmp --string "get /" -j TRACKER_TORRENT' % dev_ip6)
160

161

Nicolas Dandrimont's avatar
Nicolas Dandrimont committed
162
    ip6tables.filter.forward('-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT')
163

164 165 166 167 168
    # On filtre les réseaux non routable et aussi on accepte en entrée
    # que les paquets dont la source n'est pas notre plage, pour éviter
    # http://travaux.ovh.net/?do=details&id=5183
    ingress_filtering(ip6tables)
    ip6tables.filter.forward('-j INGRESS_FILTERING')
169

170
    # Pour les autres connections
171
    for type_m in [i for i in ['fil', 'adherents-v6', 'wifi', 'wifi-adh-v6'] if not 'v6' in i]:
172 173 174
        ip6tables.filter.mac('-s %s -j %s' % (prefix[type_m][0], 'MAC' +
            type_m.upper()))
    ip6tables.filter.forward('-i %s -j MAC' % dev_crans)
175
    ip6tables.filter.forward('-i %s -j MAC' % dev_wifi)
176 177 178

    # Rien ne passe vers adm
    # est ce que du local est gêné par le règle ?
Nicolas Dandrimont's avatar
Nicolas Dandrimont committed
179
    ip6tables.filter.forward('-d %s -j REJECT --reject-with icmp6-addr-unreachable' % (prefix['adm'][0]))
180

huber's avatar
huber committed
181
    # cf https://www.sixxs.net/faq/connectivity/?faq=filters
Valentin Samir's avatar
Valentin Samir committed
182
    ip6tables.filter.forward('-m rt --rt-type 0 -j REJECT')
183 184

    # Ouverture des ports
185
    ports(dev_ip6, [dev_crans, dev_wifi])
186

187 188 189
    # On met en place le forwarding
    enable_forwarding(6)

190 191 192

def routeur_nat64():
    ''' Firewall pour le nat64 '''
193

194 195 196
    dev_crans = iface6('fil')
    dev_adm = iface6('adm')
    dev_v6only = iface6('v6only')
197

198 199 200 201 202 203 204 205 206
    # Les blacklistes
    # Si on les met après la règle conntrack, une connexion existante ne sera
    # pas sevrée et dinc avec un tunnel ssh idoine, la blacklist aurait aucun
    # effet.
    # Alternative : flusher la table conntrack des entrées concernant cette
    # machine.
    blacklist(ip6tables)
    ip6tables.filter.forward('-i %s -j BLACKLIST_SRC' % dev_v6only)
    ip6tables.filter.forward('-i %s -j BLACKLIST_DST' % dev_crans)
207 208


209
    ip6tables.filter.forward('-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT')
210

211 212 213 214 215 216 217 218 219 220 221 222 223
    # Pour les autres connections
    for type_m in [i for i in ['fil', 'adherents-v6', 'wifi', 'wifi-adh-v6'] if not 'v6' in i]:
        ip6tables.filter.mac('-s %s -j %s' % (prefix[type_m][0], 'MAC' +
            type_m.upper()))
    ip6tables.filter.forward('-i %s -j MAC' % dev_crans)

    # Rien ne passe vers adm
    # est ce que du local est gêné par le règle ?
    ip6tables.filter.forward('-d %s -j REJECT --reject-with icmp6-addr-unreachable' % (prefix['adm'][0]))

    # On met en place le forwarding
    enable_forwarding(6)

224 225
def wifi_router():
    ''' Firewall pour le router du wifi '''
226 227 228
    # Le wifi est maintenant routé directement sur komaz.
    # On utilise donc directement main_router
    pass
229 230 231 232 233 234

def adherents_server():
    ''' Firewall pour le serveur des adhérents '''

    dev_adm = iface6('adm')
    # On fait attention à ce qui sort
235
    ip6tables.filter.output('-o lo -j ACCEPT')
Nicolas Dandrimont's avatar
Nicolas Dandrimont committed
236
    ip6tables.filter.output('-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT')
237
    ip6tables.filter.output('-o %s -j SRV_OUT_ADM' % dev_adm)
238 239 240 241 242 243 244

    # Chaîne SRV_OUT_ADM
    # Seul certains users ont le droit de communiquer sur le vlan adm
    for user in adm_users:
        try:
            u_uid = pwd.getpwnam(user)[2]
        except KeyError:
245 246
#            raise UnknowUserError(user)
            continue
247 248 249 250 251 252 253 254
        ip6tables.filter.srv_out_adm('-m owner --uid-owner %d -j ACCEPT' %
                pwd.getpwnam(user)[2])

    # LDAP et DNS toujours joignable
    ip6tables.filter.srv_out_adm('-p tcp --dport ldap -j ACCEPT')
    ip6tables.filter.srv_out_adm('-p tcp --dport domain -j ACCEPT')
    ip6tables.filter.srv_out_adm('-p udp --dport domain -j ACCEPT')
    # Pour le nfs (le paquet à laisser passer n'a pas d'owner)
255 256 257 258
#    ip6tables.filter.srv_out_adm('-d nfs.adm.crans.org  -m owner ! \
#--uid-owner 0 -j REJECT --reject-with icmp6-adm-prohibited')
#    ip6tables.filter.srv_out_adm('-d nfs.adm.crans.org -j ACCEPT')
    ## A corriger, le nfs a pas l'air de faire de l'ipv6 de toute façon.
259 260

    # On arrête tout
261
    ip6tables.filter.srv_out_adm('-j REJECT --reject-with icmp6-adm-prohibited')
262

263 264
def appt_proxy():
    pass
265

266 267
def start():
    ''' Démarre le firewall sur la machine considérée '''
268 269 270 271 272 273 274 275

    # On véifie si le serveur n'est pas que sur le vlan adm. Dans ce cas on ne
    # fait rien

    if hostname in adm_only:
        print "Rien n'a faire, ce serveur est adm-only"
        exit(0)

276 277 278 279
    # On rempli machines
    global machines
    machines = db.all_machines(graphic = True)
    print hostname
280

281 282 283 284 285 286 287 288 289
    # On supprime les anciennes règles si elles existent.
    try:
        os.remove(output_file[6])
        os.remove(file_pickle[6])
    except:
        pass

    # On recherche si la machine a des attributs particuliers.
    if hostname in role.keys():
290 291 292 293
        if 'external' in role[hostname]:
            print "Il faut encore implémenter un firewall pour les serveurs à \
            l'extérieur du réseau Cr@ns"
            exit(0)
294 295
        if 'sniffer' not in role[hostname]:
            basic_fw()
296
        for func in role[hostname]:
297 298 299 300
            if func != 'sniffer':
                eval(re.sub('-', '_', func))()
    else:
        basic_fw()
301 302 303 304 305 306 307 308 309 310 311 312 313 314

    # On écrit les données dans un fichier
    write_rules(ip6tables)

    # On les applique
    apply_rules(6)

    # Sauvegarde de l'instance
    save_pickle(ip6tables)
    return 0

def stop():
    ''' Vide les tables '''
    # TODO
315
    # il manque une gestion des règles de routage spéciales réalisées à
316 317 318 319 320 321 322 323 324 325 326
    # l'aide de ip rule et ip route
    # idée faire une classe et la stocker en pickle pour savoir ce qui a été
    # ajouté


    # On fixe comme règle juste les déclarations de police par défaut
    write_rules(ip6tables)

    # On les applique
    apply_rules(6)

327
    disable_forwarding(6)
328 329 330 331 332 333

    return 0

def restart():
    ''' Redémarre le firewall '''

334
    # On remplace les règles en place
335 336 337 338
    start()

    return 0

339 340 341 342 343 344 345

def blacklist_main():
    fw6 = Update()
    fw6.blacklist(6)

    return 0

346 347 348 349 350 351 352
if __name__ ==  '__main__':
    if len(sys.argv) != 2:
        aide()
        sys.exit(-1)

    if sys.argv[1] in [ 'start', 'stop', 'restart' ]:
        eval(sys.argv[1])()
353 354
    elif sys.argv[1] == 'blacklist':
        blacklist_main()
355 356 357
    else:
        aide()
        sys.exit(-1)