diff --git a/gestion/config/config.py b/gestion/config/config.py index c11f6c87..0753eea1 100644 --- a/gestion/config/config.py +++ b/gestion/config/config.py @@ -177,7 +177,7 @@ sshfp_algo = { } sshfs_ralgo = {} -for key,value in sshfp_algo.items(): +for key, value in sshfp_algo.items(): sshfs_ralgo[value[1]] = (value[0], key) sshfp_hash = { diff --git a/gestion/config/dns.py b/gestion/config/dns.py index 349a5b30..ee244905 100644 --- a/gestion/config/dns.py +++ b/gestion/config/dns.py @@ -3,8 +3,10 @@ """ Variables de configuration pour la gestion du DNS """ +import os + # import des variables génériques -import config +import gestion.config as config #: ariane et ariane2 pour la zone parente parents = [ @@ -28,38 +30,116 @@ slaves_tv = slaves zone_tv = 'tv.crans.org' #: DNS en connexion de secours -secours_relay='10.231.136.14'; +secours_relay = '10.231.136.14'; #: Serveurs autoritaires pour les zones crans, le master doit être le premier -DNSs = ['sable.crans.org', 'freebox.crans.org', 'soyouz.crans.org'] +DNSs = [ + 'sable.crans.org', + 'freebox.crans.org', + 'soyouz.crans.org', +] + +MXs = { + 'redisdead.crans.org': { + 'prio': 10, + 'spf': 'v=spf1 ptr ~all', + }, + 'freebox.crans.org': { + 'prio': 25, + 'spf': 'v=spf1 ptr ~all', + }, + 'soyouz.crans.org': { + 'prio': 15, + 'spf': 'v=spf1', + }, +} #: Résolution DNS directe, liste de toutes les zones crans hors reverse -zones_direct = [ 'crans.org', 'crans.ens-cachan.fr', 'wifi.crans.org', 'clubs.ens-cachan.fr', 'adm.crans.org','crans.eu','wifi.crans.eu', 'tv.crans.org', 'ap.crans.org' ] +zones_direct = [ + 'crans.org', + 'crans.ens-cachan.fr', + 'wifi.crans.org', + 'clubs.ens-cachan.fr', + 'adm.crans.org', + 'crans.eu', + 'wifi.crans.eu', + 'tv.crans.org', + 'ap.crans.org', +] #: Les zones apparaissant dans des objets lc_ldap -zones_ldap = [ 'crans.org', 'crans.ens-cachan.fr', 'wifi.crans.org', 'clubs.ens-cachan.fr', 'adm.crans.org', 'tv.crans.org' ] +zones_ldap = [ + 'crans.org', + 'crans.ens-cachan.fr', + 'wifi.crans.org', + 'clubs.ens-cachan.fr', + 'adm.crans.org', + 'tv.crans.org', +] #: Zones signée par opendnssec sur le serveur master -zones_dnssec = ['crans.org', 'wifi.crans.org', 'adm.crans.org', 'tv.crans.org', 'crans.eu'] +zones_dnssec = [ + 'crans.org', + 'wifi.crans.org', + 'adm.crans.org', + 'tv.crans.org', + 'crans.eu', +] #: Zones alias : copie les valeur des enregistrement pour la racine de la zone et utilise un enregistemenr DNAME pour les sous domaines zone_alias = { - 'crans.org' : ['crans.eu'], + 'crans.org' : [ + 'crans.eu', + ], } #: Résolution inverse v4 zones_reverse = config.NETs["all"] + config.NETs["adm"] + config.NETs["personnel-ens"] + config.NETs['multicast'] #: Résolution inverse v6 -zones_reverse_v6 = config.prefix['fil'] + config.prefix['wifi'] + config.prefix ['adm'] + config.prefix['personnel-ens'] # à modifier aussi dans bind.py +zones_reverse_v6 = config.prefix['fil'] + config.prefix['wifi'] + config.prefix['adm'] + config.prefix['personnel-ens'] # à modifier aussi dans bind.py #: Serveurs DNS récursifs : recursiv = { - 'fil' : ['138.231.136.98', '138.231.136.152'], - 'wifi' : ['138.231.136.98', '138.231.136.152'], - 'evenementiel' : ['138.231.136.98', '138.231.136.152'], - 'adm' : ['10.231.136.98', '10.231.136.152'], - 'gratuit' : ['10.42.0.164'], - 'accueil' : ['10.51.0.10'], - 'isolement' : ['10.52.0.10'], - 'personnel-ens' : ['10.2.9.10', '138.231.136.98', '138.231.136.152'], + 'fil' : [ + '138.231.136.98', + '138.231.136.152', + ], + 'wifi' : [ + '138.231.136.98', + '138.231.136.152', + ], + 'evenementiel' : [ + '138.231.136.98', + '138.231.136.152', + ], + 'adm' : [ + '10.231.136.98', + '10.231.136.152', + ], + 'gratuit' : [ + '10.42.0.164', + ], + 'accueil' : [ + '10.51.0.10', + ], + 'isolement' : [ + '10.52.0.10', + ], + 'personnel-ens' : [ + '10.2.9.10', + '138.231.136.98', + '138.231.136.152', + ], } #: Les ip/net des vlans limité vue par les récursifs -menteur_clients = [ "138.231.136.210", "138.231.136.10" ] + config.prefix['evenementiel'] +menteur_clients = [ + "138.231.136.210", + "138.231.136.10", +] + config.prefix['evenementiel'] + +# Chemins de fichiers/dossiers utiles. +DNS_DIR = '/etc/bind/generated/' +DNSSEC_DIR = '/etc/bind/signed/' +# Fichier de définition des zones pour le maître +DNS_CONF = os.path.join(DNS_DIR, 'zones_crans') + +# Fichier de définition des zones pour les esclaves géré par BCfg2 +DNS_CONF_BCFG2 = "/var/lib/bcfg2/Cfg/etc/bind/generated/zones_crans/zones_crans" diff --git a/gestion/gen_confs/bind.py b/gestion/gen_confs/bind.py index 76718eb4..fce9b011 100755 --- a/gestion/gen_confs/bind.py +++ b/gestion/gen_confs/bind.py @@ -6,6 +6,7 @@ Copyright (C) Valentin Samir Licence : GPLv3 """ +import os import sys import ssl import time @@ -37,24 +38,31 @@ def short_name(fullhostname): return fullhostname.split(".")[0] class ResourceRecord(object): - def __init__(self, type, name, value, ttl=None): - self._type=type - self._name=name - self._value=value - self._ttl=ttl + """Classe standard définissant une ressource DNS""" + + def __init__(self, r_type, name, value, ttl=None): + """Affecte les valeurs de base de l'enregistrement""" + self.r_type = r_type + self.name = name + self.value = value + self._ttl = ttl def __str__(self): + """Retourne une chaîne printable dans un fichier bind""" if self._ttl: - return "%s\t%s\tIN\t%s\t%s" % (self._name, self._ttl, self._type, self._value) + return "%s\t%s\tIN\t%s\t%s" % (self.name, self._ttl, self.r_type, self.value) else: - return "%s\tIN\t%s\t%s" % (self._name, self._type, self._value) + return "%s\tIN\t%s\t%s" % (self.name, self.r_type, self.value) + def __repr__(self): + """__repr__ == __str__""" return str(self) class TLSA(ResourceRecord): - def __init__(self, name, port, proto, cert, certtype, reftype, selector=0, compat=True, format='pem', ttl=None): - """ - name: nom du domaine du certificat + """Enregistrement TLSA pour stocker des certifs dans un enregistrement DNS""" + + def __init__(self, name, port, proto, cert, certtype, reftype, selector=0, compat=True, r_format='pem', ttl=None): + """ name: nom du domaine du certificat port: port où écoute le service utilisant le certificat proto: udp ou tcp cert: le certificat au format ``format`` (pem ou der) (selector est donc toujours à 0) @@ -62,8 +70,9 @@ class TLSA(ResourceRecord): reftype: 0 = plain cert, 1 = sha256, 2 = sha512 compat: on génère un enregistement compris même par les serveurs dns n'implémentant pas TLSA """ - if not format in ['pem', 'der']: + if not r_format in ['pem', 'der']: raise ValueError("format should be pem or der") + if cert is None and proto == 'tcp' and name[-1] == '.': try: cert = ssl.get_server_certificate((name[:-1], port), ca_certs='/etc/ssl/certs/ca-certificates.crt') @@ -71,12 +80,15 @@ class TLSA(ResourceRecord): raise ValueError("Unable de retrieve cert dynamically: %s" % e) elif cert is None: raise ValueError("cert can only be retrive if proto is tcp and name fqdn") - if format is not 'der': + + if r_format is not 'der': dercert = ssl.PEM_cert_to_DER_cert(cert) else: dercert = cert + if not dercert: raise ValueError("Impossible de convertir le certificat au format DER %s %s %s\n%s" % (name, port, proto, cert)) + certhex = TLSA.hashCert(reftype, str(dercert)) if compat: super(TLSA, self).__init__( @@ -87,15 +99,15 @@ class TLSA(ResourceRecord): ) else: super(TLSA, self).__init__( - 'TLSA', - '_%s._%s%s' % (port, proto, '.' + name if name else ''), - "%s %s %s %s"% (certtype, selector, reftype, certhex), - ttl + 'TLSA', + '_%s._%s%s' % (port, proto, '.' + name if name else ''), + "%s %s %s %s"% (certtype, selector, reftype, certhex), + ttl ) @staticmethod def hashCert(reftype, certblob): - """ + """Retourne un hash d'un certif DER en MAJUSCULES. certblob: un certificat au format DER """ if reftype == 0: @@ -111,98 +123,164 @@ class TLSA(ResourceRecord): return hashobj.hexdigest().upper() class SOA(ResourceRecord): + """Ressource pour une entrée DNS SOA""" def __init__(self, master, email, serial, refresh, retry, expire, ttl): super(SOA, self).__init__('SOA', '@', '%s. %s. (\n %s ; numero de serie\n %s ; refresh (s)\n %s ; retry (s)\n %s ; expire (s)\n %s ; TTL (s)\n )' % (master, email, serial, refresh, retry, expire, ttl)) + class A(ResourceRecord): + """Entrée DNS pour une IPv4""" def __init__(self, name, value, ttl=None): super(A, self).__init__('A', name, value, ttl) + class DS(ResourceRecord): + """Entrée DNS pour l'empreinte d'une clef DNSSEC""" def __init__(self, name, value, ttl=None): super(DS, self).__init__('DS', name, value, ttl) + class PTR(ResourceRecord): + """Entrée DNS inverse (pour obtenir l'IP à partir du NDD""" def __init__(self, name, value, ttl=None): super(PTR, self).__init__('PTR', name, value, ttl) + class AAAA(ResourceRecord): + """Entrée DNS pour une IPv6""" def __init__(self, name, value, ttl=None): super(AAAA, self).__init__('AAAA', name, value, ttl) + class TXT(ResourceRecord): + """Entrée DNS pour un champ TXT""" def __init__(self, name, value, ttl=None): super(TXT, self).__init__('TXT', name, value, ttl) + class CNAME(ResourceRecord): + """Entrée DNS pour un alias (toto -> redisdead)""" def __init__(self, name, value, ttl=None): super(CNAME, self).__init__('CNAME', name, value, ttl) + class DNAME(ResourceRecord): + """Entrée DNS pour un alias de domaine (crans.eu -> crans.org)""" def __init__(self, name, value, ttl=None): super(DNAME, self).__init__('DNAME', name, value, ttl) + class MX(ResourceRecord): + """Entrée DNS pour un serveur mail. crans.org IN MX 5 redisdead.crans.org veut dire + que redisdead est responsable de recevoir les mails destinés à toto@crans.org avec + une priorité 5 (plus c'est faible, plus c'est prioritaire. + """ def __init__(self, name, priority, value, ttl=None): super(MX, self).__init__('MX', name, '%s\t%s' % (priority, value), ttl) + class NS(ResourceRecord): + """Entrée DNS pour donner les serveurs autoritaires pour un nom de domaine""" def __init__(self, name, value, ttl=None): super(NS, self).__init__('NS', name, value, ttl) + class SPF(ResourceRecord): + """Entrée DNS pour les champs SPF""" def __init__(self, name, value, ttl=None): super(SPF, self).__init__('SPF', name, value, ttl) + class SRV(ResourceRecord): + """Entrée DNS pour les champs SRV""" def __init__(self, service, proto, priority, weight, port, target, ttl=None): super(SRV, self).__init__('SRV', '_%s._%s' % (service, proto), '%s\t%s\t%s\t%s' % (priority, weight, port, target), ttl) + class NAPTR(ResourceRecord): + """Entrée DNS pour les NAPTR""" def __init__(self, name, order, preference, flag, service, replace_regexpr, value, ttl=None): super(NAPTR, self).__init__('NAPTR', name, '%s\t%s\t"%s"\t"%s"\t"%s"\t%s' % (order, preference, flag, service, replace_regexpr, value), ttl) + class SSHFP(ResourceRecord): - def __init__(self, name, hash, algo, key, ttl=None): - if not hash in config.sshfp_hash.keys(): - raise ValueError('Hash %s invalid, valid hash are %s' % (hash, ', '.join(config.sshfp_host.keys()))) + """Entrée DNS stockant une fingerprint SSH""" + def __init__(self, name, r_hash, algo, key, ttl=None): + """Vérifie que hash/algo sont supportés dans la config""" + if not r_hash in config.sshfp_hash.keys(): + raise ValueError('Hash %s invalid, valid hash are %s' % (r_hash, ', '.join(config.sshfp_hash.keys()))) + if not algo in config.sshfp_algo.keys(): raise ValueError('Algo %s unknown, valid values are %s' % (algo, ', '.join(config.sshfp_algo.keys()))) - super(SSHFP, self).__init__('SSHFP', name, '%s\t%s\t%s' % (config.sshfp_algo[algo][0], config.sshfp_hash[hash], getattr(hashlib, hash)(base64.b64decode(key)).hexdigest()), ttl) -class ZoneBase(object): + super(SSHFP, self).__init__('SSHFP', name, '%s\t%s\t%s' % (config.sshfp_algo[algo][0], config.sshfp_hash[r_hash], getattr(hashlib, r_hash)(base64.b64decode(key)).hexdigest()), ttl) + +class ZoneBase(list): + """Classe abstraite décrivant une zone. + + Elle surcharge une liste, car l'ensemble des enregistrements de cette + zone sera contenu en elle-même.""" def __init__(self, zone_name): - self._rrlist=[] + """Affecte un nom de zone""" + super(ZoneBase, self).__init__() self.zone_name = zone_name - + self.ttl = 3600 def __repr__(self): return "<%s %s>" % (self.__class__.__name__, self.zone_name) + def __str__(self): - ret="%s\n$ORIGIN %s.\n$TTL %s\n" % (disclamer.replace('//', ';'), self.zone_name, self.ttl) - for rr in self._rrlist: - ret+="%s\n" % rr - return ret + """Version enregistrable en fichier d'une zone.""" + _ret = "%s\n$ORIGIN %s.\n$TTL %s\n" % (disclamer.replace('//', ';'), self.zone_name, self.ttl) + for rr in self: + _ret += "%s\n" % rr + return _ret def add(self, rr): + """Ajout d'un enregistrement DNS""" if isinstance(rr, ResourceRecord): - self._rrlist.append(rr) + self.append(rr) else: raise ValueError("You can only add ResourceRecords to a Zone") - def extend(self, rr_list): - for rr in rr_list: - self.add(rr) def write(self, path): + """Pour dumper le tout dans le fichier idoine.""" with open(path, 'w') as f: f.write("%s" % self) - - class ZoneClone(ZoneBase): + """Zone clone d'une autre zone.""" def __init__(self, zone_name, zone_clone, soa): + """La zone clone possède, outre son nom, un pointeur vers + la zone qu'elle duplique. + + Le SOA est fourni manuellement, et la première entrée de la zone clonée + est ignorée. (c'est a priori le SOA de celle-ci) + """ super(ZoneClone, self).__init__(zone_name) self.zone_clone = zone_clone self.ttl = zone_clone.ttl + # On met un SOA custom. self.add(soa) + + # On ajoute un DNAME, qui indique que la zone est un clone. self.add(DNAME('', "%s." % self.zone_clone.zone_name)) - for rr in self.zone_clone._rrlist[1:]: - if rr._name in ['', '@']: + + # Et on extrait les données nécessaires de la zone clônée + # à savoir, celles de l'apex (la base du domaine, qui elle + # n'est pas clônée, seuls les sous-domaines le sont) + for rr in self.zone_clone[1:]: + # Si pas de nom ou si le nom est @, on duplique bêtement l'enregistrement + if rr.name in ['', '@']: self.add(rr) - if rr._name in ["%s." % self.zone_clone.zone_name]: - self.add(ResourceRecord(rr._type, "%s." % self.zone_name, rr._value)) + + # Si le nom de domaine concerné est celui de la zone clonée, pareil, on + # "duplique", en créant un enregistrement idoine. + if rr.name in ["%s." % self.zone_clone.zone_name]: + self.add(ResourceRecord(rr.r_type, "%s." % self.zone_name, rr.value)) class Zone(ZoneBase): - def __init__(self, zone_name, ttl, soa, ns_list, ipv6=True, ipv4=True, other_zones=[]): + """Une zone standard""" + def __init__(self, zone_name, ttl, soa, ns_list, ipv6=True, ipv4=True, other_zones=None): + """Héritage, plus quelques propriétés en plus + + On définit ici si la zone comporte des ipv4/ipv6, + ainsi que des données utiles pour le comportement de celles-ci. + + other_zones contient la liste de sous-zones "indépendantes". + (exemple avec wifi.crans.org qui est une sous-zone de crans.org)""" + + if other_zones is None: + other_zones = [] super(Zone, self).__init__(zone_name) self.ttl = ttl self.ipv4 = ipv4 @@ -215,15 +293,26 @@ class Zone(ZoneBase): self.add(NS('@', '%s.' % ns)) def name_in_subzone(self, hostname): + """Teste si le nom qu'on observe est dans une + sous-zone (toto.wifi.crans.org. est dans wifi.crans.org., et non + dans crans.org.. + """ for zone in self.subzones: if str(hostname).endswith(".%s" % zone): return True return False def get_name(self, hostname): - # le hostname fini bien par la zone courante, et il n'appartient pas à une sous-zone + """Retourne la base du nom d'un hôte. Teste si celui-ci appartient bien + à la zone courante et s'il n'est pas lié à une sous-zone. + + Si tout est bon, le nom peut valoir "", auquel cas, l'entrée concerne le domaine + courant, donc @. + + Dans le cas où ce nom ne devrait pas être là, on retourne None. + """ if str(hostname) == self.zone_name or str(hostname).endswith(".%s" % self.zone_name) and not self.name_in_subzone(hostname): - ret=str(hostname)[0:- len(self.zone_name) -1] + ret = str(hostname)[0:-len(self.zone_name)-1] if ret == "": return "@" else: @@ -232,65 +321,90 @@ class Zone(ZoneBase): return None def get_name_vi(self, nom, i): + """Kludge foireux pour retourner toto.v4.crans.org à partir + de toto.crans.org (sous-zones v4/v6).""" if not i in [4, 6]: raise ValueError("i should be 4 or 6") if nom == '@': return 'v%s' % i + # On considère que le "vrai" nom est la partie avant le premier . elif '.' in nom: nom_1, nom_2 = nom.split('.', 1) return "%s.v%s.%s" % (nom_1, i, nom_2) else: return "%s.v%s" % (nom, i) - def add_delegation(zone, server): - zone = self.het_name(zone) + def add_delegation(self, zone, server): + """Lorsqu'on veut offrir une délégation DNS à une machine + pour un nom de domaine""" + zone = self.get_name(zone) if zone: self.add(NS('@', '%s.' % server)) def add_a_record(self, nom, machine): + """Ajout d'une entrée A.""" + # Fait-on de l'IPv4 dans cette zone ? if self.ipv4: for ip in machine.get('ipHostNumber', []): self.add(A(nom, ip)) + # Fait-on aussi de l'IPv6 ? if self.ipv6: + # Bon bah alors on ajoute nom.v4.crans.org en plus. self.add(A(self.get_name_vi(nom, 4), ip)) def add_aaaa_record(self, nom, machine): + """Ajout d'une entrée AAAA (for the AAAAAAAAwesome).""" + # Fait-on de l'IPv6 dans cette zone ? if self.ipv6: for ip in machine.get('ip6HostNumber', []): - if machine.get('dnsIpv6', [True])[0]: + # Si dnsIpv6 est à True dans la base LDAP, on ajoute l'entrée. + # On l'ajoute quand même si la zone ne fait pas d'IPv4, parce que + # ça semble assez dommage d'avoir une machine qui a une IPv6, pas + # d'IPv4, et pas d'entrée DNS pour la contacter, non mais oh. + dnsipv6 = machine.get('dnsIpv6', [True])[0] + if dnsipv6 or not self.ipv4: self.add(AAAA(nom, ip)) + # Si on fait aussi de l'IPv4... if self.ipv4: self.add(AAAA(self.get_name_vi(nom, 6), ip)) def add_sshfp_record(self, nom, machine): + """Ajoute une fingerprint SSH""" for sshkey in machine.get('sshFingerprint', []): try: algo_txt, key = str(sshkey).split()[:2] - algo=config.sshfs_ralgo[algo_txt][1] - for hash in config.sshfp_hash.keys(): - self.add(SSHFP(nom, hash, algo, key)) - if self.ipv4 and self.ipv6: - self.add(SSHFP(self.get_name_vi(nom, 4), hash, algo, key)) - self.add(SSHFP(self.get_name_vi(nom, 6), hash, algo, key)) + algo = config.sshfs_ralgo[algo_txt][1] + for r_hash in config.sshfp_hash.keys(): + self.add(SSHFP(nom, r_hash, algo, key)) + if self.ipv4: + self.add(SSHFP(self.get_name_vi(nom, 4), r_hash, algo, key)) + if self.ipv6: + self.add(SSHFP(self.get_name_vi(nom, 6), r_hash, algo, key)) # KeyError is l'algo dans ldap n'est pas connu # TypeError si la clef n'est pas bien en base64 except (KeyError, TypeError): pass def add_tlsa_record(self, cert): + """Ajout d'un certif dans le DNS""" if 'TLSACert' in cert['objectClass']: for host in cert['hostCert']: - nom=self.get_name(host) + nom = self.get_name(host) if nom is None: continue for port in cert['portTCPin']: - self.add(TLSA(nom, port, 'tcp', cert['certificat'][0], cert['certificatUsage'][0], cert['matchingType'][0], cert['selector'][0], format='der')) + self.add(TLSA(nom, port, 'tcp', cert['certificat'][0], cert['certificatUsage'][0], cert['matchingType'][0], cert['selector'][0], r_format='der')) for port in cert['portUDPin']: - self.add(TLSA(nom, port, 'udp', cert['certificat'][0], cert['certificatUsage'][0], cert['matchingType'][0], cert['selector'][0], format='der')) + self.add(TLSA(nom, port, 'udp', cert['certificat'][0], cert['certificatUsage'][0], cert['matchingType'][0], cert['selector'][0], r_format='der')) def add_machine(self, machine): + """Ajout d'une machine, à savoir chaînage d'ajout + d'IP, d'IPv6, de fingerprint et de TLSA, pour chaque + entrée "host" dans la base LDAP.""" for host in machine['host']: - nom=self.get_name(host) - if nom is None: continue + # Le nom peut être None (machine appartenant à une sous-zone, ou à une autre zone) + nom = self.get_name(host) + if nom is None: + continue self.add_a_record(nom, machine) self.add_aaaa_record(nom, machine) @@ -298,14 +412,23 @@ class Zone(ZoneBase): for cert in machine.certificats(): self.add_tlsa_record(cert) - + # Si la machine a bien un nom en "host", on lui ajoute aussi + # les aliases, sous forme de CNAME vers le premier nom. if machine['host']: for alias in machine.get('hostAlias', []): + # Si l'alias pointe dans une autre zone, on passe. (ça sera fait quand on refera le add_machine + # en toutnant dans la sous-zone if str(alias) in self.other_zones and str(alias) != self.zone_name: continue + alias = self.get_name(alias) - if alias is None: continue + if alias is None: + continue + to_nom = self.get_name(machine['host'][0]) + + # Si l'alias est sur le nom de la zone, il faut ajouter + # des entrées standard. if alias in ['@', '%s.' % self.zone_name]: self.add_a_record(alias, machine) self.add_aaaa_record(alias, machine) @@ -315,24 +438,34 @@ class Zone(ZoneBase): if self.ipv4 and self.ipv6: self.add(CNAME(self.get_name_vi(alias, 6), self.get_name_vi(to_nom, 6))) self.add(CNAME(self.get_name_vi(alias, 4), self.get_name_vi(to_nom, 4))) + # Ne devrait pas arriver. else: self.add(CNAME(alias, "%s." % machine['host'][0])) class ZoneReverse(Zone): + """Zone inverse, listant des PTR (toto.crans.org IN PTR 138.231...)""" def __init__(self, net, ttl, soa, ns_list): - if len(ZoneReverse.network_to_arpanets(net))!=1: + """Initialise une zone reverse. + net est un truc de la forme fe80::/64, ou 138.231.136.0/24 + En v4, il faut que net soit un /32, un /24, un /16 ou un /8 + En gros, il faut que network_to_arpanets retourne une liste à un élément.""" + + # Comme dit, liste à un élément. + if len(ZoneReverse.network_to_arpanets(net)) != 1: raise ValueError("%s n'est pas un réseau valide pour une zone de reverse dns" % net) + self.net = net zone_name = ZoneReverse.reverse(net)[0] if '.' in net: - ipv6=False - ipv4=True + ipv6 = False + ipv4 = True elif ':' in net: - ipv6=True - ipv4=False + ipv6 = True + ipv4 = False else: raise ValueError("net should be an ipv4 ou ipv6 network") + super(ZoneReverse, self).__init__(zone_name, ttl, soa, ns_list, ipv6=ipv6, ipv4=ipv4) @staticmethod @@ -341,29 +474,43 @@ class ZoneReverse(Zone): l'adresse donnés, ainsi que le nombre d'éléments de l'ip a mettre dans le fichier de zone si elle est fournie, n'importe quoi sinon.""" - n = netaddr.IPNetwork(net) - a = netaddr.IPAddress(ip if ip else n.ip) - rev_dns_a = a.reverse_dns.split('.')[:-1] - assert a in n - if n.version == 4: - if n.prefixlen == 8: + # Initialise la plage d'IP à partir de net + _network = netaddr.IPNetwork(net) + # Prend la première adresse ip de la plage, sauf si une est fournie + _address = netaddr.IPAddress(ip if ip else _network.ip) + # retourne le reverse splitté. (un reverse ressemble à 0.136.231.138.in-addr.arpa.) + rev_dns_a = _address.reverse_dns.split('.')[:-1] + + # Si la config est foireuse (donc si on a fourni une IP hors de la plage, ça + # va planter ici. + assert _address in _network + + # En v4, le reverse étant de la forme 0.136.231.138.in-addr.arpa., soit + # on a un /8, soit un /16, soit un /24. + if _network.version == 4: + if _network.prefixlen == 8: return ('.'.join(rev_dns_a[3:]), 3) - elif n.prefixlen == 16: + elif _network.prefixlen == 16: return ('.'.join(rev_dns_a[2:]), 2) - elif n.prefixlen == 24: + elif _network.prefixlen == 24: return ('.'.join(rev_dns_a[1:]), 1) else: - raise ValueError("Bad network %s" % n) - elif n.version == 6: - return ('.'.join(rev_dns_a[(128-n.prefixlen)/4:]), (128-n.prefixlen)/4) + raise ValueError("Bad network %s" % _network) + # En v6 c'est plus calme. + # Le reverse a cette tronche : 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.e.f.ip6.arpa. + # Du coup c'est aussi fin qu'on le souhaite. + elif _network.version == 6: + return ('.'.join(rev_dns_a[(128 - _network.prefixlen)/4:]), (128 - _network.prefixlen)/4) @staticmethod def network_to_arpanets(nets): - """ - retourne une liste de reseaux ne contenant que - des préfixes de taille 32, 24, 16 ou 8 en ipv4 - et laisse inchangé les réseaux ipv6. + """Dans reverse(net, ip), on a constaté qu'en v4, on ne pouvait définir + que des plages reverse en /24, /16 ou /8. Cette fonction vise à retourner + une liste des plages en tenant compte de ce critère (donc de taille + 32/24/16/8) + + Ne touche à rien pour l'IPv6. """ if not isinstance(nets, list): nets = [nets] @@ -371,6 +518,8 @@ class ZoneReverse(Zone): for net in nets: if not isinstance(net, netaddr.IPNetwork): net = netaddr.IPNetwork(net) + # Si on est en v4, on fragmente les subnets + # dans les tailles qui vont bien. if net.version == 4: if net.prefixlen > 24: subnets.extend(net.subnet(32)) @@ -380,12 +529,13 @@ class ZoneReverse(Zone): subnets.extend(net.subnet(16)) else: subnets.extend(net.subnet(8)) + # En v6 c'est tout pété. elif net.version == 6: subnets.append(net) return subnets - def add_machine(self, machine): + """Ajout d'un reverse pour une machine.""" if machine['host']: if self.ipv4: attr = 'ipHostNumber' @@ -393,33 +543,42 @@ class ZoneReverse(Zone): attr = 'ip6HostNumber' else: raise ValueError("A reverse zone should be ipv6 or ipv6") + for ip in machine[attr]: try: zone, length = ZoneReverse.reverse(self.net, str(ip)) nom = '.'.join(ip.value.reverse_dns.split('.')[:length]) + + # La zone retournée n'est pas le nom de la zone. A priori + # on aurait dû tomber en AssertionError. if zone != self.zone_name: continue - if attr != 'ip6HostNumber' or machine.get('dnsIpv6', [True])[0]: # Hack pour envoyer le reverse vers l'adresse .v6 dans le cas où dnsIpv6 = False - self.add(PTR(nom, '%s.' % machine['host'][0])) + + if attr != 'ip6HostNumber' or machine.get('dnsIpv6', [True])[0]: + self.add(PTR(nom, '%s.' % machine['host'][0])) + # Gros kludge pour ajouter le reverse vers le .v6 quand on est sur + # une reverse v6 et que dnsIpv6 est faux. else: rev_nom, rev_zone = str(machine['host'][0]).split('.', 1) self.add(PTR(nom, '%s.v6.%s.' % (rev_nom, rev_zone))) except AssertionError: + # L'ip n'est pas dans la zone reverse, donc on continue silencieusement. pass -class dns(gen_config) : +class dns(gen_config): + """Classe de configuration du DNS (les services, generate, toussa)""" ######################################PARTIE DE CONFIGURATION ### Fichiers à écrire # Répertoire d'écriture des fichiers de zone - DNS_DIR = '/etc/bind/generated/' # Avec un / à la fin - DNSSEC_DIR = '/etc/bind/signed/' # Avec un / à la fin + DNS_DIR = config.dns.DNS_DIR + DNSSEC_DIR = config.dns.DNSSEC_DIR # Fichier de définition des zones pour le maître - DNS_CONF = DNS_DIR + 'zones_crans' + DNS_CONF = config.dns.DNS_CONF # Fichier de définition des zones pour les esclaves géré par BCfg2 - DNS_CONF_BCFG2 = "/var/lib/bcfg2/Cfg/etc/bind/generated/zones_crans/zones_crans" + DNS_CONF_BCFG2 = config.dns.DNS_CONF_BCFG2 ### Liste DNS # Le premier doit être le maitre @@ -432,13 +591,10 @@ class dns(gen_config) : ### Serveurs de mail # format : [ priorité serveur , .... ] - MXs = [ - MX('@',10, 'redisdead.crans.org.'), - MX('@',15, 'soyouz.crans.org.'), - MX('@',25, 'freebox.crans.org.'), - ] + MXs = [MX('@', config.dns.MXs[_mx].get('prio', 25), _mx) for _mx in config.dns.MXs] + SRVs = { - 'crans.org': [ + 'crans.org': [ SRV('jabber', 'tcp', 5, 0, 5269, 'xmpp'), SRV('xmpp-server', 'tcp', 5, 0, 5269, 'xmpp'), SRV('xmpp-client', 'tcp', 5, 0, 5222, 'xmpp'), @@ -446,14 +602,14 @@ class dns(gen_config) : SRV('sip', 'tcp', 5, 0, 5060, 'asterisk'), SRV('sips', 'tcp', 5, 0, 5061, 'asterisk'), SRV('stun', 'udp', 5, 0, 3478, 'asterisk'), - ] + ], } NATPRs = { - 'crans.org' : [ - NAPTR('@', 5, 100, "S", "SIPS+D2T", "", '_sips._tcp.crans.org.', ttl=86400), - NAPTR('@', 10, 100, "S", "SIP+D2U", "", '_sip._udp.crans.org.', ttl=86400), - NAPTR('@', 15, 100, "S", "SIP+D2T", "", '_sip._tcp.crans.org.', ttl=86400), - ] + 'crans.org' : [ + NAPTR('@', 5, 100, "S", "SIPS+D2T", "", '_sips._tcp.crans.org.', ttl=86400), + NAPTR('@', 10, 100, "S", "SIP+D2U", "", '_sip._udp.crans.org.', ttl=86400), + NAPTR('@', 15, 100, "S", "SIP+D2T", "", '_sip._tcp.crans.org.', ttl=86400), + ], } # DS à publier dans zone parentes : { parent : [ zone. TTL IN DS key_id algo_id 1 hash ] } @@ -461,10 +617,10 @@ class dns(gen_config) : # /!\ Il faut faire attention au rollback des keys, il faudrait faire quelque chose d'automatique avec opendnssec DSs = { 'crans.org': [ - DS('adm', '64649 8 2 9c45f0fef063672d96c983d5a3813a08a649c72d357f41ddece73ae8872d60cf'), - DS('wifi', '5531 8 2 daf30a647566234edc1617546fd74abbbaf965b17389248f72fc66a33d6f5063'), - DS('tv', '18199 8 2 d3cc2f5f81b830cbb8894ffd32c236e968edd3b0c0305112b6eb970aa763418e'), - ], + DS('adm', '64649 8 2 9c45f0fef063672d96c983d5a3813a08a649c72d357f41ddece73ae8872d60cf'), + DS('wifi', '5531 8 2 daf30a647566234edc1617546fd74abbbaf965b17389248f72fc66a33d6f5063'), + DS('tv', '18199 8 2 d3cc2f5f81b830cbb8894ffd32c236e968edd3b0c0305112b6eb970aa763418e'), + ], } @@ -472,23 +628,28 @@ class dns(gen_config) : serial = int(time.time()) + 1000000000 TTL = 3600 - if hostname == short_name(config.dns.DNSs[0]): + if hostname == short_name(config.dns.DNSs[0]): restart_cmd = '/usr/sbin/ods-signer sign --all && /etc/init.d/bind9 reload' else: restart_cmd = '/etc/init.d/bind9 reload' def __init__(self, *args, **kwargs): + """Surcharge pour affecter EXTRAS""" self.EXTRAS = {} + self.anim = None super(dns, self).__init__(*args, **kwargs) def gen_soa(self, ns_list, serial, ttl): + """Génère l'enregistrement SOA pour le domaine""" return SOA(ns_list[0], 'root.crans.org', serial, 21600, 3600, 1209600, ttl) - def populate_zones(self, zones, machines): - self.anim.iter=len(zones.values()) + """On peuple les fichiers de zones""" + self.anim.iter = len(zones.values()) for zone in zones.values(): + # On met les mêmes MX pour toutes les zones. zone.extend(self.MXs) + # Les RR définis ici sont ajoutés aux zones idoines, de façon à se simplifier la vie. for rr_type in [self.SRVs, self.NATPRs, self.DSs, self.EXTRAS]: if zone.zone_name in rr_type.keys(): zone.extend(rr_type[zone.zone_name]) @@ -498,31 +659,43 @@ class dns(gen_config) : return zones def gen_zones_ldap(self, ttl, ns_list, serial, zones={}, zones_ldap=config.dns.zones_ldap): + """On génère la liste des zones ldap, à partir de config.dns. C'est un peu ici que tout commence. + Le dico zones passé en argument est modifié en place.""" for zone in zones_ldap: - zones[zone]=Zone(zone, ttl, self.gen_soa(ns_list, serial, ttl), ns_list, other_zones=config.dns.zones_direct) + # On crée la zone et on l'ajoute au dico. + zones[zone] = Zone(zone, ttl, self.gen_soa(ns_list, serial, ttl), ns_list, other_zones=config.dns.zones_direct) return zones def gen_zones_reverse(self, ttl, ns_list, serial, zones={}, - zones_reverse_v4=config.dns.zones_reverse, zones_reverse_v6=config.dns.zones_reverse_v6): + zones_reverse_v4=config.dns.zones_reverse, + zones_reverse_v6=config.dns.zones_reverse_v6): + """Deuxième gros morceau, les reverses, pareil, on peuple depuis config.dns, et on crée toutes les zones + idoines. Pareil, ici, le dico zones est modifié en place""" for net in ZoneReverse.network_to_arpanets(zones_reverse_v4 + zones_reverse_v6): - zones[str(net)]=ZoneReverse(str(net), ttl, self.gen_soa(ns_list, serial, ttl), ns_list) + # On crée la zone et on l'ajoute au dico. + zones[str(net)] = ZoneReverse(str(net), ttl, self.gen_soa(ns_list, serial, ttl), ns_list) return zones def gen_zones_clone(self, ttl, ns_list, serial, zones={}): - for zone_clone, zones_alias in config.dns.zone_alias.items(): + """Les clônes, à savoir crans.eu et cie, dico zones modifié en place.""" + for zone_clone, zones_alias in config.dns.zone_alias.iteritems(): for zone in zones_alias: - zones[zone]=ZoneClone(zone, zones[zone_clone], self.gen_soa(ns_list, serial, ttl)) + # On crée la zone et on l'ajoute au dico. + zones[zone] = ZoneClone(zone, zones[zone_clone], self.gen_soa(ns_list, serial, ttl)) + # Et on ajoute les enregistrements concernant la zone clône (pas la clônée, ça + # a déjà été fait à l'init) à la main. for rr_type in [self.SRVs, self.NATPRs, self.DSs]: if zones[zone].zone_name in rr_type.keys(): zones[zone].extend(rr_type[zones[zone].zone_name]) return zones - def gen_zones(self, ttl, serial, ns_list, populate=True): + """On chaîne les différents gen_zones_*""" zones = {} self.gen_zones_ldap(ttl, ns_list, serial, zones) self.gen_zones_reverse(ttl, ns_list, serial, zones) + # Si populate, on remplit les zones avec les enregistrements \o/ if populate: conn = lc_ldap.shortcuts.lc_ldap_admin() machines = conn.search(u"mid=*", sizelimit=10000) @@ -534,30 +707,34 @@ class dns(gen_config) : self.gen_zones_clone(ttl, ns_list, serial, zones) return zones - def gen_tv(self, populate=True): + """Génération de la TV, un peu à part.""" self.anim = affich_tools.anim('\tgénération de la zone tv') zones = {} serial = self.serial self.gen_zones_reverse(self.TTL, config.dns.DNSs, serial, zones, zones_reverse_v4=config.NETs['multicast'], zones_reverse_v6=[]) self.gen_zones_ldap(self.TTL, config.dns.DNSs, serial, zones, zones_ldap=[config.dns.zone_tv]) + # Pareil, si on doit peupler on ajoute ce qu'il faut niveau machines. if populate: conn = lc_ldap.shortcuts.lc_ldap_admin() - machines=conn.machinesMulticast() + machines = conn.machinesMulticast() machines.extend(conn.search(u'(|(host=%s)(host=*.%s)(hostAlias=%s)(hostAlias=*.%s))' % ((config.dns.zone_tv,)*4))) self.populate_zones(zones, machines) for zone in zones.values(): - zone.write(self.DNS_DIR + 'db.' + zone.zone_name) + zone.write(os.path.join(self.DNS_DIR, 'db.%s' % (zone.zone_name,))) self.anim.reinit() print affich_tools.OK return zones def gen_master(self): + """Pour le serveur maître. + + Appelle gen_zones puis écrit les fichiers.""" # Syntaxe utilisée dans le fichier DNS_CONF pour définir une zone sur le maître - zone_template=""" + zone_template = """ zone "%(zone_name)s" { type master; file "%(zone_path)s"; @@ -567,15 +744,17 @@ zone "%(zone_name)s" { with open(self.DNS_CONF, 'w') as f: f.write(disclamer) for zone in zones.values(): - zone.write(self.DNS_DIR + 'db.' + zone.zone_name) + zone.write(os.path.join(self.DNS_DIR, 'db.%s' % (zone.zone_name,))) if zone.zone_name in config.dns.zones_dnssec: - zone_path = self.DNSSEC_DIR + 'db.' + zone.zone_name + zone_path = os.path.join(self.DNSSEC_DIR, 'db.%s' % (zone.zone_name,)) else: - zone_path = self.DNS_DIR + 'db.' + zone.zone_name + zone_path = os.path.join(self.DNS_DIR, 'db.%s' % (zone.zone_name,)) f.write(zone_template % {'zone_name' : zone.zone_name, 'zone_path' : zone_path}) def gen_slave(self): - zone_template=""" + """Pour les slaves, fait l'écriture de la conf dans bcfg2, mais on ne peuple rien ! + On ne fait qu'écrire le fichier zone_crans.""" + zone_template = """ zone "%(zone_name)s" { type slave; file "%(zone_path)s"; @@ -587,9 +766,9 @@ zone "%(zone_name)s" { f.write(disclamer) for zone in zones.values(): if zone.zone_name in config.dns.zones_dnssec: - zone_path = self.DNSSEC_DIR + 'db.' + zone.zone_name + zone_path = os.path.join(self.DNSSEC_DIR, 'db.%s' % (zone.zone_name,)) else: - zone_path = self.DNS_DIR + 'db.' + zone.zone_name + zone_path = os.path.join(self.DNS_DIR, 'db.%s' % (zone.zone_name,)) f.write(zone_template % {'zone_name' : zone.zone_name, 'zone_path' : zone_path, 'master_ip' : config.dns.master}) def _gen(self): @@ -599,28 +778,28 @@ zone "%(zone_name)s" { return "DNS" -if __name__ == '__main__' : - hostname = short_name(gethostname()) - if hostname == short_name(config.bcfg2_main): +if __name__ == '__main__': + HOSTNAME = short_name(gethostname()) + if HOSTNAME == short_name(config.bcfg2_main): print "Reconfiguration du fichier de BCfg2 pour configurer le bind d'un serveur en esclave (pensez à lancer bcfg2 sur les esclaves)." - c = dns() - c.gen_slave() - elif hostname == short_name(config.dns.DNSs[0]): + CONFIG = dns() + CONFIG.gen_slave() + elif HOSTNAME == short_name(config.dns.DNSs[0]): print "Serveur maître :" - c = dns() - zones = c.gen_tv() + CONFIG = dns() + ZONES = CONFIG.gen_tv() import subprocess - for zone in zones.values(): - if zone.zone_name in config.dns.zones_dnssec: - args=("/usr/sbin/ods-signer sign %s" % zone.zone_name).split() - p=subprocess.Popen(args,stdout=subprocess.PIPE,stderr=subprocess.PIPE) - ret=p.communicate() - print ret[0].strip() - if ret[1].strip(): - print ret[1].strip() + for ZONE in ZONES.values(): + if ZONE.zone_name in config.dns.zones_dnssec: + ARGS = ("/usr/sbin/ods-signer sign %s" % ZONE.zone_name).split() + PROCESS = subprocess.Popen(ARGS, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + RET = PROCESS.communicate() + print RET[0].strip() + if RET[1].strip(): + print RET[1].strip() print "Ce serveur est également serveur maitre pour les autres zones dns, mais leur reconfiguration se fait par generate." - elif hostname in map(lambda fullhostname : short_name(fullhostname),config.dns.DNSs[1:]): - print "Ce serveur est esclave! Lancez ce script sur %s, puis lancez bcfg2 ici" % bcfg2_main + elif HOSTNAME in [short_name(FULLHOSTNAME) for FULLHOSTNAME in config.dns.DNSs[1:]]: + print "Ce serveur est esclave! Lancez ce script sur %s, puis lancez bcfg2 ici" % (config.bcfg2_main,) else: print "Ce serveur ne correspond à rien pour la configuration DNS."