#!/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() 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 : 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)