scripts/gestion/gen_confs/firewall4.py
Vincent Le Gallic 6946ee5627 [firewall4,populate_sshFingerprint] Plutôt que de s'annoncer en 'root', on précise un user qui permet de savoir quel script accès à la base.
Pour firewall, en fait on s'en fout parce qu'il fait pas d'écriture, mais on laisse le
binding en admin parce que les transactions sont plus rapides et qu'il en fait mâss.
2013-05-16 18:51:49 +02:00

1250 lines
50 KiB
Python
Executable file

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
sys.path.append('/usr/scripts/gestion')
sys.path.append('/usr/scripts/')
from config import NETs, blacklist_sanctions, blacklist_sanctions_soft, blacklist_bridage_upload, mac_komaz, mac_titanic, adm_users, accueil_route
import pwd
import config.firewall
import lc_ldap.shortcuts
import lc_ldap.objets
import lc_ldap.attributs
import socket
from ipset import IpsetError, Ipset
from iptools import AddrInNet, NetSubnets, IpSubnet, NetInNets
import subprocess
import syslog
from affich_tools import anim, OK, cprint
squeeze = os.uname()[2] < '3'
#: Nom de la machine exécutant le script
hostname = socket.gethostname()
#: Chaines par défaut d'iptables
default_chains = ['INPUT', 'OUTPUT', 'FORWARD', 'PREROUTING', 'POSTROUTING']
#: Tables d'iptables
tables = ['raw', 'mangle', 'filter', 'nat']
#: Association des interfaces de ``hostname``
dev = hostname in config.firewall.dev.keys() and config.firewall.dev[hostname] or {}
def pretty_print(table, chain):
"""Affiche quelle chaine est en train d'être construite dans quelle table de NetFilter"""
anim('\t%s dans %s' % (chain, table))
class TcError(Exception):
""" Gestion des erreurs de tc """
def __init__(self,cmd,err_code,output):
self.cmd=cmd
self.err_code=err_code
self.output=output
syslog.syslog(syslog.LOG_ERR,"%s : status %s,%s" % (cmd,err_code,output))
def __str__(self):
return "%s\n status : %s\n %s" % (self.cmd,self.err_code,self.output)
def tc(cmd, block=True):
""" Interface de tc """
params = ['/sbin/tc']
params.extend(cmd.split())
p = subprocess.Popen(params , stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if block:
status = p.wait()
stdoutdata, stderrdata = p.communicate()
if status:
raise TcError(' '.join(params), status, stdoutdata + stderrdata)
return stdoutdata + stderrdata
class firewall_base(object) :
"""Classe de base du pare-feu implémentant l'association mac-ip (pour les machines filaires) et les blacklists hard"""
def machines(self):
"""Renvois la liste de toutes les machines"""
if self._machines:
return self._machines
self._machines, self._adherents = conn.allMachinesAdherents()
return self._machines
def adherents(self):
"""Renvois la liste de tous les adhérents"""
if self._adherents:
return self._adherents
self._machines, self._adherents = conn.allMachinesAdherents()
self._adherents = [ adh for adh in self._adherents if adh.paiement_ok ]
return self._adherents
def blacklisted_machines(self):
"""Renvois la liste de toutes les machines ayant une blackliste actives"""
if self._blacklisted_machines:
return self._blacklisted_machines
if self._machines:
self._blacklisted_machines = [ machine for machine in self._machines if machine.blacklist_actif() ]
return self._blacklisted_machines
blacklisted = [ machine for machine in conn.search("blacklist=*",sizelimit=4096) if machine.blacklist_actif() ]
self._blacklisted_machines = set()
for item in blacklisted:
if isinstance(item, lc_ldap.objets.proprio):
self._blacklisted_machines = self._blacklisted_machines.union(item.machines())
elif isinstance(item, lc_ldap.objets.machine):
self._blacklisted_machines.add(item)
else:
print >> sys.stderr, 'Objet %s inconnu blacklisté' % a.__class__.__name__
return self._blacklisted_machines
def blacklisted_adherents(self):
"""Renvois la liste de tous les adhérents ayant une blackliste active"""
if self._blacklisted_adherents:
return self._blacklisted_adherents
self._blacklisted_adherents = filter(lambda adh: adh.blacklist_actif(), self.adherents())
return self._blacklisted_adherents
def add(self, table, chain, rule):
"""Ajoute la règle ``rule`` à la chaine ``chain`` dans la table ``table``"""
if not chain in self.chain_list[table]:
self.chain_list[table].append(chain)
self.rules_list[table][chain]=[]
self.rules_list[table][chain].append(rule)
def delete(self, table=None, chain=None):
"""Supprime ``chain`` de ``table`` si fournis, sinon supprime ``table``, sinon toutes les tables"""
if not table:
for table in tables:
self.delete(table, chain)
if not chain:
for chain in self.chain_list[table]:
self.delete(table, chain)
if not chain in self.chain_list[table]:
return
if not chain in default_chains:
self.chain_list[table].remove(chain)
del(self.rules_list[table][chain])
else:
self.rules_list[table][chain]=[]
def flush(self, table=None, chain=None):
"""Vide ``chain`` dans ``table`` si fournis, sinon vide toutes
les chaines de ``table``, sinon vide toutes les chaines de toutes les tables"""
if not table:
for table in tables:
self.flush(table, chain)
if not chain:
for chain in self.chain_list[table]:
self.flush(table, chain)
if not chain in self.chain_list[table]:
self.chain_list[table].append(chain)
self.rules_list[table][chain]=[]
def restore(self, table=None, chains=[], noflush=False):
"""Restores les règles du pare-feu dans le noyau en appelant ``iptables-restore``.
Si ``table`` est fournis, on ne restore que table.
Si ``noflush`` n'est pas à ``True`` tout le contenu précédent est supprimé"""
str=self.format(chains)
f=open('/tmp/ipt_rules', 'w')
f.write(str)
f.close()
params = ['/sbin/iptables-restore']
if noflush:
params.append('--noflush')
if table and table in ['raw', 'mangle', 'filter', 'nat']:
params.append('--table')
params.append(table)
p = subprocess.Popen(params , stdin=subprocess.PIPE)
p.communicate(input=str)
def apply(self, table, chain):
"""Applique les règles de ``chain`` dans ``table``"""
if not chain in self.chain_list[table]:
return
self.restore(table, [chain], noflush=True)
self.delete(table, chain)
def format(self, chains=[]):
"""Transforme la structure interne des règles du pare-feu en celle comprise par ``iptables-restore``"""
str = ''
for table in self.chain_list.keys():
str += '*%s\n' % table
for chain in self.chain_list[table]:
if not chains or chain in chains or chain in default_chains:
str += ':%s %s [0:0]\n' % (chain, chain in default_chains and 'ACCEPT' or '-')
for chain in self.chain_list[table]:
if not chains or chain in chains :
for rule in self.rules_list[table][chain]:
str += '-A %s %s\n' % (chain, rule)
str += 'COMMIT\n'
return str
def reload(self, func_name):
if squeeze and self.reloadable[func_name] in self.use_ipset:
anim('\tVidage de %s' % self.reloadable[func_name]())
for table in ['raw', 'mangle', 'filter', 'nat']:
self.flush(table, self.reloadable[func_name]())
self.restore(noflush=True)
print OK
for table in ['raw', 'mangle', 'filter', 'nat']:
self.reloadable[func_name](table)
if self.reloadable[func_name] in self.use_ipset:
self.reloadable[func_name](fill_ipset=True)
if self.reloadable[func_name] in self.use_tc:
self.reloadable[func_name](run_tc=True)
anim('\tRestoration d\'iptables')
self.restore(noflush=True)
print OK
def __init__(self):
global conn
#initialisation des structures communes : récupération des ipset
if os.getuid() != 0:
from affich_tools import coul
sys.stderr.write(coul("Il faut être root pour utiliser le firewall\n", 'gras'))
sys.exit(1)
# Connection à la base ldap
conn = lc_ldap.shortcuts.lc_ldap_admin(user=u'firewall')
self.reloadable = {
'blacklist_hard' : self.blacklist_hard,
'test_mac_ip' : self.test_mac_ip,
}
self.use_ipset = [self.blacklist_hard, self.test_mac_ip]
self.use_tc = []
self._machines = None
self._adherents = None
self._blacklisted_machines = None
self._blacklisted_adherents = None
self.chain_list={
'raw':['OUTPUT', 'PREROUTING'],
'mangle':['INPUT', 'OUTPUT', 'FORWARD', 'PREROUTING', 'POSTROUTING'],
'filter':['INPUT', 'OUTPUT', 'FORWARD'],
'nat':['OUTPUT', 'PREROUTING', 'POSTROUTING']
}
self.rules_list = {
'raw': { 'OUTPUT':[], 'PREROUTING':[] },
'mangle':{'INPUT':[], 'OUTPUT':[], 'FORWARD':[], 'PREROUTING':[], 'POSTROUTING':[]},
'filter':{'INPUT':[], 'OUTPUT':[], 'FORWARD':[]},
'nat':{'OUTPUT':[], 'PREROUTING':[], 'POSTROUTING':[]}
}
self.ipset={}
self.ipset['mac_ip']={
'adh' : Ipset("MAC-IP-ADH","macipmap","--from 138.231.136.0 --to 138.231.151.255"),
'adm' : Ipset("MAC-IP-ADM","macipmap","--from 10.231.136.0 --to 10.231.136.255"),
'app' : Ipset("MAC-IP-APP","macipmap","--from 10.2.9.0 --to 10.2.9.255"),
}
self.ipset['blacklist']={
'hard' : Ipset("BLACKLIST-HARD","ipmap","--from 138.231.136.0 --to 138.231.151.255"),
}
def start(self):
"""Démarre le pare-feu : génère les règles, puis les restore"""
anim('\tChargement des machines')
self.machines()
print OK
if squeeze:
anim('\tVidage du pare-feu')
self.restore()
print OK
self.raw_table()
self.mangle_table()
self.filter_table()
self.nat_table()
anim('\tRestoration d\'iptables')
self.restore()
print OK
return
def stop(self):
"""Vide les règles du pare-feu"""
self.delete()
self.restore()
return
def restart(self):
"""Alias de :py:func:`start`"""
self.start()
return
def blacklist_maj(self, ips):
"""Met à jours les blacklists pour les ip présentent dans la liste ``ips``"""
#self.blacklist_hard_maj(ips)
self.reload('blacklist_hard')
def raw_table(self):
"""Génère les règles pour la table ``raw`` et remplis les chaines de la table"""
table = 'raw'
return
def mangle_table(self):
"""Génère les règles pour la table ``mangle`` et remplis les chaines de la table"""
table = 'mangle'
return
def filter_table(self):
"""Génère les règles pour la table ``filter`` et remplis les chaines de la table"""
table = 'filter'
mac_ip_chain = self.test_mac_ip(table, fill_ipset=True)
blacklist_hard_chain = self.blacklist_hard(table, fill_ipset=True)
chain = 'INPUT'
self.add(table, chain, '-i lo -j ACCEPT')
self.add(table, chain, '-p icmp -j ACCEPT')
self.add(table, chain, '-m state --state RELATED,ESTABLISHED -j ACCEPT')
for net in NETs['all'] + NETs['adm'] + NETs['personnel-ens']:
self.add(table, chain, '-s %s -j %s' % (net, mac_ip_chain))
self.add(table, chain, '-j %s' % blacklist_hard_chain)
chain = 'FORWARD'
self.add(table, chain, '-j REJECT')
return
def nat_table(self):
"""Génère les règles pour la table ``nat`` et remplis les chaines de la table"""
table = 'nat'
return
def blacklist_hard_maj(self, ip_list):
"""Met à jour les blacklists hard, est appelée par :py:func:`blacklist_maj`"""
for ip in ip_list:
machine = conn.search("ipHostNumber=%s" % ip)
# Est-ce qu'il y a des blacklists hard parmis les blacklists de la machine
if machine and set([bl.value['type'] for bl in machine[0].blacklist_actif() ]).intersection(blacklist_sanctions):
try: self.ipset['blacklist']['hard'].add(ip)
except IpsetError: pass
else:
try: self.ipset['blacklist']['hard'].delete(ip)
except IpsetError: pass
def blacklist_hard(self, table=None, fill_ipset=False, apply=False):
"""Génère la chaine ``BLACKLIST_HARD``.
Si ``fill_ipset`` est à ``True``, remplis l'ipset ``BLACKLIST-HARD``.
Si ``apply`` est à True, applique directement les règles"""
chain = 'BLACKLIST_HARD'
if fill_ipset:
anim('\tRestoration de l\'ipset %s' % self.ipset['blacklist']['hard'])
# On récupère la liste de toutes les ips blacklistés hard
bl_hard_ips = set(
str(ip) for ips in
[
machine['ipHostNumber'] for machine in self.blacklisted_machines()
if set([bl.value['type'] for bl in machine.blacklist_actif() ]).intersection(blacklist_sanctions)
]
for ip in ips
)
self.ipset['blacklist']['hard'].restore(bl_hard_ips)
print OK
if table == 'filter':
pretty_print(table, chain)
self.add(table, chain, '-m set --match-set %s src -j REJECT' % self.ipset['blacklist']['hard'] )
self.add(table, chain, '-m set --match-set %s dst -j REJECT' % self.ipset['blacklist']['hard'] )
print OK
if apply:
self.apply(table, chain)
return chain
def test_mac_ip_dispatch(self, func, machine):
"""Détermine à quel set de mac-ip appliquer la fonction ``func`` (add, delete, append, ...)"""
ips = machine['ipHostNumber']
for ip in ips:
# Si la machines est sur le réseau des adhérents
if AddrInNet(str(ip), NETs['wifi']):
# Les machines wifi sont vues à travers komaz
func('adh', "%s,%s" % (ip, mac_komaz))
elif AddrInNet(str(ip), NETs['fil']):
func('adh', "%s,%s" % (ip, machine['macAddress'][0]))
# Si la machine est sur le réseau admin
elif AddrInNet(str(ip), NETs['adm']):
func('adm', "%s,%s" % (ip, machine['macAddress'][0]))
def test_mac_ip(self, table=None, fill_ipset=False, apply=False):
"""Génère la chaine ``TEST_MAC-IP``.
Si ``fill_ipset`` est à ``True``, remplis les ipsets ``MAC-IP-ADH``, ``MAC-IP-ADM``, ``MAC-IP-ADM``.
Si ``apply`` est à True, applique directement les règles"""
chain = 'TEST_MAC-IP'
if fill_ipset:
anim('\tRestoration des ipsets %s' % ', '.join(self.ipset['mac_ip'].keys()))
rules={
'adh':[],
'adm':[],
'app':[],
}
for machine in self.machines():
self.test_mac_ip_dispatch(lambda set, data: rules[set].append(data), machine)
for set,rules in rules.items():
self.ipset['mac_ip'][set].restore(rules)
print OK
if table == 'filter':
pretty_print(table, chain)
for key in ['accueil', 'isolement', ]:
for net in NETs[key]:
self.add(table, chain, '-s %s -j RETURN' % net)
for key in self.ipset['mac_ip'].keys():
self.add(table, chain, '-m set --match-set %s src,src -j RETURN' % self.ipset['mac_ip'][key])
# Proxy ARP de Komaz et Titanic pour OVH
ip_ovh = conn.search("host=ovh.adm.crans.org")[0]['ipHostNumber'][0]
self.add(table, chain, '-m mac -s %s --mac-source %s -j RETURN' % (ip_ovh, mac_komaz))
self.add(table, chain, '-m mac -s %s --mac-source %s -j RETURN' % (ip_ovh, mac_titanic))
self.add(table, chain, '-j REJECT')
print OK
if apply:
self.apply(table, chain)
return chain
def mac_ip_maj(self, ip_list):
"""Met à jour la correspondance mac-ip"""
for ip in ip_list:
machine = conn.search("ipHostNumber=%s" % ip)
if machine:
try: self.test_mac_ip_dispatch(lambda set, data: self.ipset['mac_ip'][set].delete(data.split(',',1)[0]), {'ipHostNumber' : [ip], 'macAddress':[''] })
except IpsetError: pass
self.test_mac_ip_dispatch(lambda set, data: self.ipset['mac_ip'][set].add(data), machine[0])
else:
try: self.test_mac_ip_dispatch(lambda set, data: self.ipset['mac_ip'][set].delete(data.split(',',1)[0]), {'ipHostNumber' : [ip], 'macAddress':[''] })
except IpsetError: pass
class firewall_base_routeur(firewall_base):
"""Associe mac-ip pour les machines voyant plusieurs réseaux (wifi, filaire, personnel, ...)"""
def test_mac_ip_dispatch(self, func, machine):
"""Détermine à quel set de mac-ip appliquer la fonction func (add, delete, append, ...)"""
ips = machine['ipHostNumber']
for ip in ips:
# Si la machines est sur le réseau des adhérents
if AddrInNet(str(ip), NETs['wifi']):
func('adh', "%s,%s" % (ip, machine['macAddress'][0]))
elif AddrInNet(str(ip), NETs['fil']):
func('adh', "%s,%s" % (ip, machine['macAddress'][0]))
# Si la machine est sur le réseau admin
elif AddrInNet(str(ip), NETs['adm']):
func('adm', "%s,%s" % (ip, machine['macAddress'][0]))
# Si la machine est sur le réseaux des appartements de l'ENS
elif AddrInNet(str(ip), NETs['personnel-ens']):
func('app', "%s,%s" % (ip, machine['macAddress'][0]))
class firewall_base_wifionly(firewall_base):
"""Associe mac-ip pour les machines wifi only : les machines filaires sont vues avec la mac de komaz"""
def test_mac_ip_dispatch(self, func, machine):
"""Détermine à quel set de mac-ip appliquer la fonction func (add, delete, append, ...)"""
ips = machine['ipHostNumber']
for ip in ips:
# Si la machines est sur le réseau des adhérents
if AddrInNet(str(ip), NETs['wifi']):
func('adh', "%s,%s" % (ip, machine['macAddress'][0]))
elif AddrInNet(str(ip), NETs['fil']):
func('adh', "%s,%s" % (ip, mac_komaz))
# Si la machine est sur le réseau admin
elif AddrInNet(str(ip), NETs['adm']):
func('adm', "%s,%s" % (ip, machine['macAddress'][0]))
class firewall_komaz(firewall_base_routeur):
def __init__(self):
super(self.__class__, self).__init__()
self.reloadable.update({
'log_all' : self.log_all,
'admin_vlan' : self.admin_vlan,
'clamp_mss' : self.clamp_mss,
'ingress_filtering' : self.ingress_filtering,
'ssh_on_https' : self.ssh_on_https,
'connexion_secours' : self.connexion_secours,
'connexion_appartement' : self.connexion_appartement,
'blacklist_soft' : self.blacklist_soft,
'blacklist_upload' : self.blacklist_upload,
'reseaux_non_routable' : self.reseaux_non_routable,
'filtrage_ports' : self.filtrage_ports,
'limitation_debit' : self.limitation_debit,
'limit_ssh_connexion' : self.limit_ssh_connexion,
})
self.use_ipset.extend([self.blacklist_soft, self.blacklist_upload, self.reseaux_non_routable])
self.use_tc.extend([self.limitation_debit])
self.ipset['reseaux_non_routable'] = {
'deny' : Ipset("RESEAUX-NON-ROUTABLE-DENY","nethash"),
'allow' : Ipset("RESEAUX-NON-ROUTABLE-ALLOW","nethash"),
}
self.ipset['blacklist'].update({
'soft' : Ipset("BLACKLIST-SOFT","ipmap","--from 138.231.136.0 --to 138.231.151.255"),
'upload' : Ipset("BLACKLIST-UPLOAD","ipmap","--from 138.231.136.0 --to 138.231.151.255"),
})
def blacklist_maj(self, ips):
#self.blacklist_hard_maj(ips)
#self.blacklist_soft_maj(ips)
#self.blacklist_upload_maj(ips)
self.reload('blacklist_hard')
self.reload('blacklist_soft')
self.reload('blacklist_upload')
def raw_table(self):
return
def mangle_table(self):
table = 'mangle'
super(self.__class__, self).mangle_table()
chain = 'PREROUTING'
self.add(table, chain, '-j %s' % self.log_all(table))
self.add(table, chain, '-j %s' % self.blacklist_soft(table, fill_ipset=True))
self.add(table, chain, '-j %s' % self.connexion_secours(table))
self.add(table, chain, '-p tcp -j CONNMARK --restore-mark')
chain = 'POSTROUTING'
self.add(table, chain, '-j %s' % self.clamp_mss(table))
self.add(table,chain, '-j %s' % self.limitation_debit(table, run_tc=True))
self.add(table, chain, '-j %s' % self.blacklist_upload(table, fill_ipset=True))
return
def filter_table(self):
table = 'filter'
super(self.__class__, self).filter_table()
mac_ip_chain = self.test_mac_ip()
blacklist_hard_chain = self.blacklist_hard()
chain = 'FORWARD'
self.flush(table, chain)
self.add(table, chain, '-i lo -j ACCEPT')
self.add(table, chain, '-p icmp -j ACCEPT')
self.add(table, chain, '-j %s' % self.admin_vlan(table))
self.add(table, chain, '-i %s -j %s' % (dev['out'], blacklist_hard_chain))
self.add(table, chain, '-o %s -j %s' % (dev['out'], blacklist_hard_chain))
self.add(table, chain, '-m state --state RELATED,ESTABLISHED -j ACCEPT')
self.add(table, chain, '-j %s' % self.reseaux_non_routable(table, fill_ipset=True))
self.add(table, chain, '-j %s' % self.blacklist_soft(table))
for net in NETs['all'] + NETs['adm'] + NETs['personnel-ens']:
self.add(table, chain, '-s %s -j %s' % (net, mac_ip_chain))
self.add(table, chain, '-j %s' % self.connexion_secours(table))
self.add(table, chain, '-j %s' % self.connexion_appartement(table))
self.add(table, chain, '-j %s' % self.ingress_filtering(table))
self.add(table, chain, '-j %s' % self.limit_ssh_connexion(table))
self.add(table, chain, '-j %s' % self.filtrage_ports(table))
return
def nat_table(self):
table = 'nat'
super(self.__class__, self).nat_table()
chain = 'PREROUTING'
self.add(table, chain, '-j %s' % self.ssh_on_https(table))
self.add(table, chain, '-j %s' % self.connexion_secours(table))
self.add(table, chain, '-j %s' % self.blacklist_soft(table))
chain = 'POSTROUTING'
self.add(table, chain, '-j %s' % self.connexion_appartement(table))
return
def limit_ssh_connexion(self, table=None, apply=False):
chain = 'LIMIT-SSH-CONNEXION'
if table == 'filter':
pretty_print(table, chain)
self.add(table, chain, '-i %s -p tcp --dport ssh -m state --state NEW -m recent --name SSH --set' % dev['out'])
self.add(table, chain, '-i %s -p tcp --dport ssh -m state --state NEW -m recent --name SSH --update --seconds 30 --hitcount 10 --rttl -j DROP' % dev['out'])
print OK
if apply:
self.apply(table, chain)
return chain
def test_mac_ip(self, table=None, fill_ipset=False, apply=False):
chain = super(self.__class__, self).test_mac_ip()
if table == 'filter':
for key in ['out', 'tun-ovh' ]:
self.add(table, chain, '-i %s -j RETURN' % dev[key])
return super(self.__class__, self).test_mac_ip(table, fill_ipset, apply)
def log_all(self, table=None, apply=False):
chain = 'LOG_ALL'
if table == 'mangle':
pretty_print(table, chain)
for device in dev.values():
self.add(table, chain, '-i %s -m state --state NEW -j LOG --log-prefix "LOG_ALL "' % device)
print OK
if apply:
self.apply(table, chain)
return chain
def admin_vlan(self, table=None, apply=False):
chain = 'VLAN-ADM'
if table == 'filter':
pretty_print(table, chain)
for net in NETs['adm']:
self.add(table, chain, '-o %s -s %s -j ACCEPT' % (dev['tun-ovh'], net))
self.add(table, chain, '-i %s -d %s -j ACCEPT' % (dev['tun-ovh'], net))
self.add(table, chain, '-d %s -j REJECT' % net)
print OK
if apply:
self.apply(table, chain)
return chain
def qos(self, table=None, apply=False):
return
def clamp_mss(self, table=None, apply=False):
"""Force la MSS (Max Segment Size) TCP à rentrer dans la MTU (Max Transfert Unit)"""
chain = 'CLAMP-MSS'
if table == 'mangle':
pretty_print(table, chain)
self.add(table, chain, '-p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu')
print OK
if apply:
self.apply(table, chain)
return chain
def ingress_filtering(self, table=None, apply=False):
"""Pour ne pas router les paquêtes n'appartenant pas à notre plage ip voulant sortir de notre réseau
et empêcher certain type de spoof (cf http://travaux.ovh.net/?do=details&id=5183)"""
chain = 'INGRESS_FILTERING'
if table == 'filter':
pretty_print(table, chain)
for net in NETs['all']:
self.add(table, chain, '-o %s -s %s -j RETURN' % (dev['out'], net))
self.add(table, chain, '-o %s -j LOG --log-prefix BAD_ROUTE' % dev['out'])
self.add(table, chain, '-o %s -j DROP' % dev['out'])
for net_d in NETs['all']:
for net_s in NETs['all']:
self.add(table, chain,'-i %s ! -s %s -d %s -j RETURN' % (dev['out'], net_s, net_d))
self.add(table, chain,'-i %s -j LOG --log-prefix BAD_SRC' % dev['out'])
self.add(table, chain,'-i %s -j DROP' % dev['out'])
print OK
if apply:
self.apply(table, chain)
return chain
def ssh_on_https(self, table=None, apply=False):
"""Pour faire fonctionner ssh2.crans.org"""
chain = 'SSH2'
if table == 'nat':
pretty_print(table, chain)
self.add(table, chain, '-p tcp -d 138.231.136.2 --dport 22 -j DNAT --to-destination 138.231.136.1:22') # redirection du ssh vers zamok
self.add(table, chain, '-p tcp -d 138.231.136.2 --dport 443 -j DNAT --to-destination 138.231.136.1:22') # redirection du ssh vers zamok (pour passer dans un proxy, avec corkscrew)
print OK
if apply:
self.apply(table, chain)
return chain
def connexion_secours(self, table=None, apply=False):
"""Redirige les paquets vers un proxy lorsqu'on est en connexion de secours"""
chain = 'CONNEXION-SECOURS'
if table == 'mangle':
pretty_print(table, chain)
self.add(table, chain, '-p tcp -s 138.231.136.0/16 ! -d 138.231.136.0/16 --destination-port 80 -m condition --condition secours -j MARK --set-mark %s' % (config.firewall.mark['secours']))
self.add(table, chain, '-m mark --mark %s -j ACCEPT' % config.firewall.mark['secours'])
print OK
if table == 'nat':
pretty_print(table, chain)
self.add(table, chain, '-p tcp -m mark --mark %s -j DNAT --to-destination 10.231.136.4:3129' % config.firewall.mark['secours'] )
print OK
if table == 'filter':
pretty_print(table, chain)
self.add(table, chain, '-p tcp -s 138.231.136.0/16 ! -d 138.231.136.0/16 --destination-port 443 -m condition --condition secours -j REJECT')
self.add(table, chain, '-m mark --mark %s -j ACCEPT' % config.firewall.mark['secours'])
print OK
if apply:
self.apply(table, chain)
return chain
def connexion_appartement(self, table=None, apply=False):
"""PNAT les appartements derrière appartement.crans.org"""
chain = 'CONNEXION-APPARTEMENT'
if table == 'nat':
pretty_print(table, chain)
for dev_key in ['out', 'fil', 'wifi']:
for net in NETs['personnel-ens']:
self.add(table, chain, '-o %s -s %s -j SNAT --to 138.231.136.44' % (dev[dev_key], net))
print OK
if table == 'filter':
pretty_print(table, chain)
for net in NETs['personnel-ens']:
self.add(table, chain, '-s %s -j ACCEPT' % net)
self.add(table, chain, '-d %s -j ACCEPT' % net)
print OK
if apply:
self.apply(table, chain)
return chain
def blacklist_soft_maj(self, ip_list):
for ip in ip_list:
machine = conn.search("ipHostNumber=%s" % ip)
# Est-ce qu'il y a des blacklists soft parmis les blacklists de la machine
if machine and set([bl.value['type'] for bl in machine[0].blacklist_actif() ]).intersection(blacklist_sanctions_soft):
try: self.ipset['blacklist']['soft'].add(ip)
except IpsetError: pass
else:
try: self.ipset['blacklist']['soft'].delete(ip)
except IpsetError: pass
def blacklist_soft(self, table=None, fill_ipset=False, apply=False):
"""Redirige les gens blacklisté vers le portail captif"""
chain = 'BLACKLIST_SOFT'
if fill_ipset:
anim('\tRestoration de l\'ipset %s' % self.ipset['blacklist']['soft'])
# On récupère la liste de toutes les ips blacklistés soft
bl_soft_ips = set(
str(ip) for ips in
[
machine['ipHostNumber'] for machine in self.blacklisted_machines()
if set([bl.value['type'] for bl in machine.blacklist_actif() ]).intersection(blacklist_sanctions_soft)
]
for ip in ips
)
self.ipset['blacklist']['soft'].restore(bl_soft_ips)
print OK
if table == 'mangle':
pretty_print(table, chain)
self.add(table, chain, '-p tcp ! --dport 80 -j RETURN')
self.add(table, chain, '! -p tcp -j RETURN')
for net in NETs['all']:
self.add(table, chain, '-d %s -j RETURN' % net)
self.add(table, chain, '-m set --match-set %s src -j MARK --set-mark %s'
% (self.ipset['blacklist']['soft'], config.firewall.mark['proxy']))
print OK
if table == 'filter':
pretty_print(table, chain)
self.add(table, chain, '-m mark --mark %s -j ACCEPT' % config.firewall.mark['proxy'])
print OK
if table == 'nat':
pretty_print(table, chain)
self.add(table, chain, '-p tcp -m mark --mark %s -j DNAT --to-destination 10.231.136.4:3128' % config.firewall.mark['proxy'] )
print OK
if apply:
self.apply(table, chain)
return chain
def blacklist_upload_maj(self, ip_list):
for ip in ip_list:
machine = conn.search("ipHostNumber=%s" % ip)
# Est-ce qu'il y a des blacklists pour upload parmis les blacklists de la machine
if machine and set([bl.value['type'] for bl in machine[0].blacklist_actif() ]).intersection(blacklist_bridage_upload):
try: self.ipset['blacklist']['upload'].add(ip)
except IpsetError: pass
else:
try: self.ipset['blacklist']['upload'].delete(ip)
except IpsetError: pass
def blacklist_upload(self, table=None, fill_ipset=False, apply=False):
"""Redirige les gens blacklisté vers le portail captif"""
chain = 'BLACKLIST_UPLOAD'
if fill_ipset:
anim('\tRestoration de l\'ipset %s' % self.ipset['blacklist']['upload'])
# On récupère la liste de toutes les ips blacklistés pour upload
bl_upload_ips = set(
str(ip) for ips in
[
machine['ipHostNumber'] for machine in self.blacklisted_machines()
if set([bl.value['type'] for bl in machine.blacklist_actif() ]).intersection(blacklist_bridage_upload)
]
for ip in ips
)
self.ipset['blacklist']['upload'].restore(bl_upload_ips)
print OK
if table == 'mangle':
pretty_print(table, chain)
# Classification pour les blacklists upload
self.add(table, chain, '-o %s -m set --match-set %s src -j CLASSIFY --set-class 1:11' % (dev['out'], self.ipset['blacklist']['upload']))
print OK
if apply:
self.apply(table, chain)
return chain
def reseaux_non_routable(self, table=None, fill_ipset=False, apply=False):
"""Bloque les réseaux non routables autres que ceux utilisés par le crans"""
chain = 'RESEAUX_NON_ROUTABLES'
if fill_ipset:
anim('\tRestoration de l\'ipset reseaux_non_routable')
allowed = [ net for nets in NETs.values() for net in nets if NetInNets(net, config.firewall.reseaux_non_routables) ]
self.ipset['reseaux_non_routable']['allow'].restore(allowed)
self.ipset['reseaux_non_routable']['deny'].restore(config.firewall.reseaux_non_routables)
print OK
if table == 'filter':
pretty_print(table, chain)
self.add(table, chain, '-m set --match-set %s src -j RETURN' % self.ipset['reseaux_non_routable']['allow'])
self.add(table, chain, '-m set --match-set %s dst -j RETURN' % self.ipset['reseaux_non_routable']['allow'])
self.add(table, chain, '-m set --match-set %s src -j DROP' % self.ipset['reseaux_non_routable']['deny'])
self.add(table, chain, '-m set --match-set %s dst -j DROP' % self.ipset['reseaux_non_routable']['deny'])
print OK
if apply:
self.apply(table, chain)
return chain
def filtrage_ports_maj(self, ip_lists):
self.filtrage_ports('filter', apply=True)
def filtrage_ports(self, table=None, apply=False):
"""Ouvre les ports vers et depuis les machines du réseau crans"""
chain = 'FILTRAGE-PORTS'
def format_port(port):
port = str(port)
if port.endswith(':'):
port = '%s65535' % port
if port.startswith(':'):
port = '0%s' % port
return port
def add_ports(ip, proto, sens):
self.add(
table,
chain,
'-p %s -%s %s -m multiport --dports %s -j RETURN' % (
proto,
(sens=='out' and 's') or (sens == 'in' and 'd'),
ip,
','.join( format_port(port) for port in machine['portTCP%s' % sens])
)
)
if table == 'filter':
pretty_print(table, chain)
for net in NETs['adherents'] + NETs['wifi-adh'] + NETs['personnel-ens']:
for proto in config.firewall.ports_default.keys():
if config.firewall.ports_default[proto]['output']:
self.add(table, chain, '-p %s -s %s -m multiport --dports %s -j RETURN' % (proto, net, ','.join( format_port(port) for port in config.firewall.ports_default[proto]['output'])))
if config.firewall.ports_default[proto]['input']:
self.add(table, chain, '-p %s -d %s -m multiport --dports %s -j RETURN' % (proto, net, ','.join( format_port(port) for port in config.firewall.ports_default[proto]['input'])))
for machine in self.machines():
for ip in machine['ipHostNumber']:
if 'portTCPout' in machine.attrs.keys():
add_ports(ip,'tcp','out')
if 'portUDPout' in machine.attrs.keys():
add_ports(ip,'udp','out')
if 'portTCPin' in machine.attrs.keys():
add_ports(ip,'tcp','in')
if 'portUDPin' in machine.attrs.keys():
add_ports(ip,'udp','in')
self.add(table, chain, '-j REJECT')
print OK
if apply:
self.apply(table, chain)
return chain
def limitation_debit(self, table=None, run_tc=False, apply=False):
"""Limite le débit de la connexion selon l'agréement avec l'ENS"""
chain = 'LIMITATION-DEBIT'
debit_max = config.firewall.debit_max
uplink_speed = '1024mbit'
if table == 'mangle':
pretty_print(table, chain)
# Pas de QoS vers/depuis la zone ENS
self.add(table, chain, '-d 138.231.0.0/16 -s 138.231.0.0/16 -j RETURN')
# Idem pour le ftp
self.add(table, chain, '-d 138.231.136.98 -j RETURN')
self.add(table, chain, '-s 138.231.136.98 -j RETURN')
# Idem vers OVH pour le test de la connection de secours
self.add(table, chain, '-d 91.121.84.138 -j RETURN')
self.add(table, chain, '-s 91.121.84.138 -j RETURN')
# Classification par defaut pour tous les paquets
for net in NETs['all']:
self.add(table, chain, '-o %s -s %s -j CLASSIFY --set-class 1:10' % (dev['out'], net))
self.add(table, chain, '-o %s -d %s -j CLASSIFY --set-class 1:10' % (dev['fil'], net))
self.add(table, chain, '-o %s -d %s -j CLASSIFY --set-class 1:10' % (dev['wifi'], net))
# Classification pour les appartements
for net in NETs['personnel-ens']:
self.add(table, chain, '-o %s -d %s -j CLASSIFY --set-class 1:3' % (dev['app'], net))
self.add(table, chain, '-o %s -s %s -j CLASSIFY --set-class 1:2' % (dev['out'], net))
# Classification pour la voip
self.add(table, chain, '-d sip.crans.org -j CLASSIFY --set-class 1:12')
self.add(table, chain, '-s sip.crans.org -j CLASSIFY --set-class 1:12')
print OK
if run_tc:
anim('\tApplication des commandes tc')
for int_key in ['out', 'fil', 'wifi']:
try:
tc('qdisc del dev %s root' % dev[int_key])
except TcError:
pass
tc('qdisc add dev %s root handle 1: htb r2q 1' % dev[int_key])
tc("class add dev %s parent 1: classid 1:1 "
"htb rate %s ceil %s" % (dev[int_key], uplink_speed, uplink_speed))
tc("class add dev %s parent 1:1 classid 1:2 "
"htb rate %skbps ceil %skbps" % (dev[int_key], debit_max, debit_max))
# Classe par defaut
tc('class add dev %s parent 1:2 classid 1:10 '
'htb rate %skbps ceil %skbps prio 1' % (dev[int_key], debit_max, debit_max))
tc('qdisc add dev %s parent 1:10 '
'handle 10: sfq perturb 10' % dev[int_key])
# Classe par pour la voip
tc('class add dev %s parent 1:2 classid 1:12 '
'htb rate %skbps ceil %skbps prio 0' % (dev[int_key], debit_max, debit_max))
tc('qdisc add dev %s parent 1:12 '
'handle 12: sfq perturb 10' % dev[int_key])
#Classe des decos upload
tc('class add dev %s parent 1:2 classid 1:11 '
'htb rate 30kbps ceil 30kbps prio 1' % dev['out'])
tc('qdisc add dev %s parent 1:11 '
'handle 11: sfq perturb 10' % dev['out'])
for int_key in ['app']:
try:
tc('qdisc del dev %s root' % dev[int_key])
except TcError:
pass
tc('qdisc add dev %s root handle 1: htb r2q 1' % dev[int_key])
tc("class add dev %s parent 1: classid 1:1 "
"htb rate 128kbps ceil 128kbps" % dev[int_key])
# Classe pour l'upload des appartements
tc("class add dev %s parent 1:1 classid 1:2 "
"htb rate 128kbps ceil 128kbps" % dev[int_key])
tc('qdisc add dev %s parent 1:2 '
'handle 2: sfq perturb 10' % dev[int_key])
# Classe pour le download des apparetments
tc("class add dev %s parent 1: classid 1:3 "
"htb rate %skbps ceil %skbps" % (dev[int_key], debit_max/10, debit_max/2))
tc('qdisc add dev %s parent 1:3 '
'handle 3: sfq perturb 10' % dev[int_key])
print OK
if apply:
self.apply(table, chain)
return chain
class firewall_zamok(firewall_base):
def __init__(self):
super(self.__class__, self).__init__()
self.reloadable.update({
'admin_vlan' : self.admin_vlan,
'blacklist_output' : self.blacklist_output,
})
self.use_ipset.extend([])
self.use_tc.extend([])
def raw_table(self):
table = 'raw'
super(self.__class__, self).raw_table()
return
def mangle_table(self):
table = 'mangle'
super(self.__class__, self).mangle_table()
return
def filter_table(self):
table = 'filter'
super(self.__class__, self).filter_table()
chain = 'OUTPUT'
self.add(table, chain , '-d 224.0.0.0/4 -j DROP')
admin_vlan_chain = self.admin_vlan(table)
self.add(table, chain, '-m state --state RELATED,ESTABLISHED -j ACCEPT')
for net in NETs['adm']:
self.add(table, chain, '-d %s -j %s' % (net, admin_vlan_chain))
self.add(table, chain, '-o lo -j ACCEPT')
self.add(table, chain, '-j %s' % self.blacklist_output(table))
return
def nat_table(self):
table = 'nat'
super(self.__class__, self).raw_table()
return
def admin_vlan(self, table=None, apply=False):
chain='ADMIN-VLAN'
if table == 'filter':
pretty_print(table, chain)
# ldap et dns toujours joinable
self.add(table, chain, '-p tcp --dport ldap -j ACCEPT')
self.add(table, chain, '-p tcp --dport domain -j ACCEPT')
self.add(table, chain, '-p udp --dport domain -j ACCEPT')
# Pour le nfs (le paquet à laisser passer n'a pas d'owner)
self.add(table, chain, '-d nfs.adm.crans.org -j ACCEPT')
for user in adm_users:
try: self.add(table, chain, '-m owner --uid-owner %d -j ACCEPT' % pwd.getpwnam(user)[2])
except KeyError: print "Utilisateur %s inconnu" % user
for nounou in conn.search("droits=%s" % lc_ldap.attributs.nounou):
self.add(table, chain, '-m owner --uid-owner %s -j RETURN' % nounou['uidNumber'][0])
# Rien d'autre ne passe
self.add(table, chain, '-j REJECT --reject-with icmp-net-prohibited')
print OK
if apply:
self.apply(table, chain)
return chain
def blacklist_maj(self, ips):
anim('\tMise à jour des blacklists')
self.blacklist_output('filter', apply=True)
#self.blacklist_hard_maj(ips)
self.reload('blacklist_hard')
print OK
def blacklist_output(self, table=None, apply=False):
"""Empêche les gens blacklisté d'utiliser zamok comme relaie"""
chain='BLACKLIST-OUTPUT'
if table == 'filter':
pretty_print(table, chain)
self.add(table, chain, '-d 127.0.0.1/8 -j RETURN')
for net in NETs['all']:
self.add(table, chain, '-d %s -j RETURN' % net)
for adh in self.blacklisted_adherents():
if 'uidNumber' in adh.attrs.keys():
self.add(table, chain, '-m owner --uid-owner %s -j REJECT' % adh['uidNumber'][0])
print OK
if apply:
self.apply(table, chain)
return chain
class firewall_routeur(firewall_base):
def __init__(self):
super(self.__class__, self).__init__()
self.reloadable.update({
'portail_captif_route' : self.portail_captif_route,
'portail_captif' : self.portail_captif,
})
self.use_ipset.extend([])
self.use_tc.extend([])
def raw_table(self):
table = 'raw'
super(self.__class__, self).raw_table()
return
def mangle_table(self):
table = 'mangle'
super(self.__class__, self).mangle_table()
return
def filter_table(self):
table = 'filter'
super(self.__class__, self).filter_table()
chain = 'FORWARD'
self.flush(table, chain)
self.add(table, chain, '-j %s' % self.portail_captif_route(table))
return
def nat_table(self):
table = 'nat'
super(self.__class__, self).raw_table()
chain = 'PREROUTING'
self.add(table, chain, '-j %s' % self.portail_captif(table))
chain = 'POSTROUTING'
self.add(table, chain, '-j %s' % self.portail_captif_route(table))
return
def portail_captif_route(self, table=None, apply=False):
"""PNAT les (ip,port) à laisser passer à travers le portail captif"""
chain = 'CAPTIF-ROUTE'
if table == 'filter':
pretty_print(table, chain)
for ip in accueil_route.keys():
for type in accueil_route[ip].keys():
if type in ['udp', 'tcp']:
self.add(table, chain, '-p %s -d %s -m multiport --dports %s -j ACCEPT' % (type, ip, ','.join(accueil_route[ip][type])))
self.add(table, chain, '-p %s -s %s -m multiport --sports %s -j ACCEPT' % (type, ip, ','.join(accueil_route[ip][type])))
self.add(table, chain, '-j REJECT')
print OK
if table == 'nat':
pretty_print(table, chain)
#intranet et wiki pour le vlan accueil
for ip in accueil_route.keys():
for type in accueil_route[ip].keys():
if type in ['udp', 'tcp']:
for net in NETs['accueil']:
self.add(table, chain, '-s %s -p %s -d %s -m multiport --dports %s -j MASQUERADE' % (net, type, ip, ','.join(accueil_route[ip][type])))
for net in NETs['isolement']:
self.add(table, chain, '-s %s -p %s -d %s -m multiport --dports %s -j MASQUERADE' % (net, type, ip, ','.join(accueil_route[ip][type])))
for net in NETs['personnel-ens']:
self.add(table, chain, '-i %s -s %s -j MASQUERADE' % (dev['app'], net))
print OK
if apply:
self.apply(table, chain)
return chain
def portail_captif(self, table=None, apply=False):
"""Redirige vers le portail captif"""
chain = 'PORTAIL-CAPTIF'
if table == 'nat':
pretty_print(table, chain)
for ip in accueil_route.keys():
for type in accueil_route[ip].keys():
if type in ['udp', 'tcp']:
self.add(table, chain, '-p %s -d %s -m multiport --dports %s -j RETURN' % (type, ip, ','.join(accueil_route[ip][type])))
for net in NETs['isolement']:
self.add(table, chain, '-p tcp -s %s --destination-port 80 -j DNAT --to-destination 10.52.0.10' % net)
for net in NETs['accueil']:
self.add(table, chain, '-p tcp -s %s --destination-port 80 -j DNAT --to-destination 10.51.0.10' % net)
self.add(table, chain, '-p udp -s %s --dport 53 -j DNAT --to 10.51.0.10' % net)
self.add(table, chain, '-p tcp -s %s --dport 53 -j DNAT --to 10.51.0.10' % net)
print OK
if apply:
self.apply(table, chain)
return chain
#: Associe à un ``hostname`` la classe du pare-feu correspondant
firewall = {
'komaz' : firewall_komaz,
'zamok' : firewall_zamok,
'routeur' : firewall_routeur,
'gordon' : firewall_base_routeur,
'eap' : firewall_base_wifionly,
}
if hostname in firewall.keys():
#: Classe du pare-feu pour la machine ``hostname`` ou :py:class:`firewall_base` si non trouvé
firewall = firewall[hostname]
else:
#: Classe du pare-feu pour la machine ``hostname`` ou :py:class:`firewall_base` si non trouvé
firewall = firewall_base
if __name__ == '__main__' :
fw = firewall()
# Chaînes pouvant être recontruites
chaines = fw.reloadable.keys()
def __usage(txt=None) :
if txt!=None : cprint(txt,'gras')
chaines.sort()
print """Usage:
%(p)s start : Construction du firewall.
%(p)s restart : Reconstruction du firewall.
%(p)s stop : Arrêt du firewall.
%(p)s <noms de chaînes> : reconstruit les chaînes
Les chaînes pouvant être reconstruites sont :
%(chaines)s
Pour reconfiguration d'IPs particulières, utiliser generate. """ % \
{ 'p' : sys.argv[0].split('/')[-1] , 'chaines' : '\n '.join(chaines) }
sys.exit(-1)
# Bons arguments ?
if len(sys.argv) == 1 :
__usage()
for arg in sys.argv[1:] :
if arg in [ 'stop', 'restart', 'start' ] and len(sys.argv) != 2 :
__usage("L'argument %s ne peut être employé que seul." % arg)
if arg not in [ 'stop', 'restart', 'start' ] + chaines :
__usage("L'argument %s est inconnu." % arg)
for arg in sys.argv[1:] :
if arg == 'stop':
fw.stop()
elif arg == 'start':
fw.start()
elif arg == 'restart':
fw.restart()
else:
fw.reload(arg)