#! /usr/bin/env python # -*- coding: utf-8 -*- """ Génération de la configuration pour bind9 Copyright (C) Frédéric Pauget Licence : GPLv2 """ import time, sys, re, hashlib, base64, os sys.path.append('/usr/scripts/gestion') from socket import gethostname from gen_confs import gen_config import config import config.dns from iptools import AddrInNet, AddrInNets import ip6tools import netaddr import ldap_crans def short_name(fullhostname): return fullhostname.split(".")[0] def netv4_to_arpa(net): addr, prefixlen = net.split('/') if prefixlen == '8': return ["%s.in-addr.arpa" % addr.split('.')[0]] if prefixlen == '16': return ["%s.in-addr.arpa" % '.'.join(reversed(addr.split('.')[0:2]))] zones=[] n = map(int,net.split('/')[0].split('.')[:3]) while 1 : try: innet = AddrInNet("%d.%d.%d.1" % tuple(n),net) except ValueError: break else: if not innet: break else : n.reverse() zones.append("%d.%d.%d.in-addr.arpa" % tuple(n)) n.reverse() n[2] += 1 return zones def netv6_to_arpa(net): n = netaddr.IPNetwork(net) network_reverse = netaddr.IPAddress(n.first).reverse_dns zone = network_reverse.split('.')[(128-n.prefixlen)/4:-1] return '.'.join(zone) class dns(gen_config) : """ Génération des fichiers de configuration de bind9 : * fichier DNS_CONF qui contient les définitions de zone conformément à zone_template. Ce fichier doit être inclus à partir de la config statique de bind * les fichiers de zones, ce sont eux qui contiennent les données du dns, ils ont appellés par le fichier DNS_CONF et sont générés dans DNS_DIR Leur entète est générée à partir de zone_entete. Les fichiers générés placent bind comme autoritaire sur les noms de zones_direct et les adresses de zones_reverse. Les données proviennent de la base LDAP """ ######################################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 # Fichier de définition des zones pour le maître DNS_CONF = 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" ### Sur quelles zones on a autorité ? ## En cas de modification de ces zones penser à regéner le fichier de ## zone des esclaves (sur le serveur principal de bcfg2 : python /usr/scripts/gestion/gen_confs/bind.py puis lancer bcfg2 sur les miroirs) # Résolution directe zones_direct = config.dns.zones_direct # Zones signée par opendnssec sur le serveur maitre zones_dnssec = config.dns.zones_dnssec # Zones alias pour les enregistrement A AAAA CNAME TXT et SSHFP zone_alias = config.dns.zone_alias zones_v4_to_v6 = { 'wifi.crans.eu': 'wifi.v6.crans.eu', 'crans.eu': 'v6.crans.eu', 'crans.org': 'v6.crans.org', 'wifi.crans.org': 'wifi.v6.crans.org', 'adm.crans.org': 'adm.v6.crans.org', 'ferme.crans.org': 'ferme.v6.crans.org', } zone_alias.update({ 'wifi.v6.crans.eu': ['v6.wifi.crans.eu'], 'wifi.v6.crans.org': ['v6.wifi.crans.org'], 'adm.v6.crans.org': ['v6.adm.crans.org'], 'ferme.v6.crans.org': ['v6.ferme.crans.org'], }) # Résolution inverse zones_reverse = config.dns.zones_reverse zones_v6_to_net = { 'crans.eu': config.prefix["fil"][0], 'crans.org': config.prefix["fil"][0], 'wifi.crans.org': config.prefix["wifi"][0], 'adm.crans.org': config.prefix["adm"][0], 'ferme.crans.org': config.prefix["fil"][0], # Hack pour générer un fichier de zone vide '##HACK##': config.prefix["subnet"][0], } ### Liste DNS # Le premier doit être le maitre DNSs = ['sable.crans.org', 'freebox.crans.org', 'ovh.crans.org'] DNSs_private = [] ip_master_DNS = config.dns.master zone_multicast = 'tv.crans.org' ### Liste des délégations de zone # Pour les demandes de ces zones, le DNS dira d'aller voir les serveurs listés ici # Pour les noms des serveurs on met l'IP sans point ou le nom avec un point DELEG = { zone_multicast : [ 'sable.crans.org.' , 'mdr.crans.org.', 'freebox.crans.org.', 'ovh.crans.org.'] , } ### Serveurs de mail # format : [ priorité serveur , .... ] MXs = ['10 redisdead.crans.org', '20 ovh.crans.org', '25 freebox.crans.org'] SRVs = [ '_jabber._tcp.crans.org. 86400 IN SRV 5 0 5269 xmpp.crans.org.', '_xmpp-server._tcp.crans.org. 86400 IN SRV 5 0 5269 xmpp.crans.org.', '_xmpp-client._tcp.crans.org. 86400 IN SRV 5 0 5222 xmpp.crans.org.', '_sip._udp.crans.org. 86400 IN SRV 5 0 5060 asterisk.crans.org.', '_sip._tcp.crans.org. 86400 IN SRV 5 0 5060 asterisk.crans.org.', '_sips._tcp.crans.org. 86400 IN SRV 5 0 5061 asterisk.crans.org.', ] # DS à publier dans zone parentes : { parent : [ zone. TTL IN DS key_id algo_id 1 hash ] } # ex : { 'crans.eu' : ['wifi.crans.eu. 86400 IN DS 33131 8 1 3B573B0E2712D8A8B1B0C3'] } # /!\ Il faut faire attention au rollback des keys, il faudrait faire quelque chose d'automatique avec opendnssec DS = { 'crans.eu': [ 'wifi.crans.eu. 3600 IN DS 49739 8 2 de49579462a8f439c9b1853c2a06d337ae4e5ffbd41c21449d4c7112794254ed', 'v6.crans.eu. 3600 IN DS 81 8 2 36fa8d4782ecdbc25f0455e2c14aea899b86cf497ce78c7d90d1cf85647f5190', ], 'v6.crans.eu' : [ 'wifi.v6.crans.eu. 3600 IN DS 1799 8 2 52a40a7dfb3e9c88aee032c21c59be756c8d3de29149c408ed8b699d83e30032', ], 'crans.org': [ 'v6.crans.org. 3600 IN DS 23641 8 2 3fff97a2581f0f2f49257b4914d5badf8ccb0a49c5a6f4cbf2f520b97de332d0', 'adm.crans.org. 3600 IN DS 565 8 2 498f6cd5bcf291aae4129700a7569fa6e9a86821185bd655f0b9efc6a3bf547e', 'ferme.crans.org. 3600 IN DS 35156 8 2 b63a1443b3d7434429e879e046bc8ba89056cdcb4b9c3566853e64fd521895b8', 'wifi.crans.org. 3600 IN DS 41320 8 2 024799c1d53f1e827f03d17bc96709b85ee1c05d77eb0ebeadcfbe207ee776a4', 'tv.crans.org. 3600 IN DS 30910 8 2 3317f684081867ab94402804fbb3cd187e29655cc7f34cb92c938183fe0b71f5', ], 'v6.crans.org' : [ 'adm.v6.crans.org. 3600 IN DS 1711 8 2 f154eeb8eb346d2ca5cffb3f9cc464a17c0c4d69ee425b4fe44eaed7f5dd253b', 'ferme.v6.crans.org. 3600 IN DS 44434 8 2 fb87cb4216599cb6574add543078a9e48d0e50438483386585a9960557434ab0', 'wifi.v6.crans.org. 3600 IN DS 59539 8 2 dbe86f2f2e92d6a27bd1436f03ec1588f2948a2aa02124de0383be801cced85e', ] } ### Entète des fichiers de zone zone_entete=""" $ORIGIN %(zone)s. $TTL 3600 @\tIN\tSOA %(serveur_autoritaire)s. root.crans.org. ( %(serial)i ; numero de serie 21600 ; refresh (s) 3600 ; retry (s) 1209600 ; expire (s) 3600 ; TTL (s) ) """ # Syntaxe utilisée dans le fichier DNS_CONF pour définir une zone sur le maître zone_template=""" zone "%(NOM_zone)s" { type master; file "%(FICHIER_zone)s"; }; """ # Syntaxe utilisée dans le fichier DNS_CONF_BCFG2 pour définir une zone sur un esclave zone_template_slave=""" zone "%(NOM_zone)s" { type slave; file "%(FICHIER_zone)s"; masters { %(ip_master_DNS)s; }; }; """ ### Verbosité # Si =2, ralera (chaine warnings) si machines hors zone trouvée # Si =1, comme ci-dessus, mais ne ralera pas pour freebox # Si =0, ralera seulement contre les machines ne pouvant être classées verbose = 1 hostname = short_name(gethostname()) if hostname == short_name(DNSs[0]): restart_cmd = '/usr/sbin/ods-signer sign --all && /etc/init.d/bind9 reload' else: restart_cmd = '/etc/init.d/bind9 reload' ######################################FIN PARTIE DE CONFIGURATION def __str__(self) : return "DNS" def reverse(self, net, ip): """Renvoie la zone DNS inverse correspondant au réseau et à l'adresse donnés, ainsi que le nombre d'éléments de l'ip a mettre dans le fichier de zone.""" n = netaddr.IPNetwork(net) a = netaddr.IPAddress(ip) rev_dns_a = a.reverse_dns.split('.')[:-1] assert a in n if n.version == 4: if n.prefixlen == 16: return ('.'.join(rev_dns_a[2:]), 2) else: return ('.'.join(rev_dns_a[1:]), 1) elif n.version == 6: return ('.'.join(rev_dns_a[(128-n.prefixlen)/4:]), (128-n.prefixlen)/4) def gen_tv(self): serial = time.time() + 1000000000 zone_reverse=netv4_to_arpa(config.NETs['multicast'][0])[0] sap=open('/tmp/chaines_recup_sap.txt').readlines() DNS='; DNS de la zone par ordre de priorité\n' for d in self.DELEG[self.zone_multicast] : DNS += '@\tIN\tNS %s\n' % d DNS += '\n' lignes_d = '\n' lignes_r = '\n' lignes_d +='@\tIN\tA\t%s\n' % '138.231.136.243' for line in sap: [nom,ip]=line.split(':') nom=re.sub('TNT([0-9]*) ','',nom) # on enlève les TNT## des noms nom=nom.replace('TNT%2lcn ','') # on enlève les TNT## des noms nom=re.sub(' +([^ ])','-\g<1>',nom) # on remplaces les espaces intérieur par un tiret nom=re.sub('[ .():,"\'+<>]','',nom) # on enlève tous les caractères illégaux nom=nom.lower() try: [ip1,ip2,ip3,ip4]=ip.strip().split('.') lignes_r += '%s.%s.%s\tIN\tPTR\t%s.%s.\n' % (ip4,ip3,ip2,nom,self.zone_multicast) lignes_d +='%s\tIN\tA\t%s' % (nom,ip) except: pass # Écriture de la zone directe file = self.DNS_DIR + 'db.' + self.zone_multicast fd = self._open_conf(file,';') fd.write(self.zone_entete % \ { 'zone' : self.zone_multicast, 'serveur_autoritaire' : self.DELEG[self.zone_multicast][0][0:-1] , 'serial' : serial } ) fd.write('\n') fd.write(DNS) fd.write(lignes_d) fd.close() # Écriture du reverse file = self.DNS_DIR + 'db.' + zone_reverse fd = self._open_conf(file,';') fd.write(self.zone_entete % \ { 'zone' : zone_reverse, 'serveur_autoritaire' : self.DELEG[self.zone_multicast][0][0:-1] , 'serial' : serial } ) fd.write('\n') fd.write(DNS) fd.write(lignes_r) fd.close() def gen_slave(self) : """ Génération du fichier de config de zone pour les esclaves """ zones = self.zones_direct zones.extend(self.zones_v4_to_v6.values()) zones.extend([z for l in self.zone_alias.values() for z in l]) zones = list(set(zones)) zones.sort() # Ajout des zones reverse for net in self.zones_reverse: # IPv4 reverse zones.extend(netv4_to_arpa(net)) for net in set(self.zones_v6_to_net.values()): # IPv6 reverse zones.append(netv6_to_arpa(net)) # Ecriture fd = self._open_conf(self.DNS_CONF_BCFG2,'//') for zone in zones : if zone in self.zones_dnssec: path=self.DNSSEC_DIR + 'db.' + zone else: path=self.DNS_DIR + 'db.' + zone fd.write(self.zone_template_slave % { 'NOM_zone' : zone, 'FICHIER_zone' : path, 'ip_master_DNS': self.ip_master_DNS}) fd.close() def _gen(self) : ### Génération du numéro de série # Le + 1000.... s'explique pas l'idée précédente et peu pratique d'avoir # le numéro de série du type AAAAMMJJNN (année, mois, jour, incrément par jour) serial = time.time() + 1000000000 ### DNS DNS='; DNS de la zone par ordre de priorité\n' for d in self.DNSs : DNS += '@\tIN\tNS %s.\n' % d DNS += '\n' ### Serveurs de mail MX='; Serveurs de mails\n' for m in self.MXs : MX += '@\t' # Sera remplacé par le nom de zone plus tard MX += 'IN\tMX\t%s.\n' % m MX += '\n' direct = {} # format : { zone : [ lignes correspondantes] } reverse = {} warnings = '' direct['crans.org'] = "" # P'tit lien vers irc.rezosup.org #direct["crans.org"] = "\n; irc.crans.org -> irc.rezosup.org\n" #direct["crans.org"] += "irc\tIN\tCNAME\tirc.rezosup.org.\n\n" ### Tri des machines self.anim.iter=len(self.machines) for machine in self.machines : self.anim.cycle() # Calculs préliminaires try : nom , zone = machine.nom().split('.',1) zone = zone.encode('utf-8') except : warnings += u'Machine ignorée (mid=%s) : format nom incorrect (%s)\n' % ( machine.id().encode('utf-8'), machine.nom().encode('utf-8') ) continue # Le direct if zone in self.zones_direct : ligne = "%s\tIN\tA\t%s\n" % ( nom, machine.ip() ) # Si la machine est une borne wifi, on ajoute la position if isinstance(machine,ldap_crans.BorneWifi) and machine.position(): ligne +="%s\tIN\tTXT\t\"LOC %s,%s \"\n" % (nom,machine.position()[0],machine.position()[1]) # Si la machine à des clefs ssh, on ajoute les champs SSFP correspondant for sshkey in machine.sshFingerprint(): try: [algo_txt,key]=sshkey.split()[:2] algo=None for value in config.sshfp_algo.values(): if algo_txt == value[1]: algo=value[0] break if not algo: raise ValueError("Invalid Algorithms %s" % algo_txt) key=hashlib.sha1(base64.b64decode(key)).hexdigest() ligne +="%s\tIN\tSSHFP\t%s\t1\t%s\n" % (nom,algo,key) except(ValueError,TypeError): pass direct[zone] = direct.get(zone, "") + ligne if isinstance(machine,ldap_crans.BorneWifi): direct['ap.crans.org'] = direct.get('ap.crans.org', "") + ligne elif self.verbose and machine.nom() != "ftp.federez.net": warnings += u'Résolution directe ignorée (mid=%s) : zone non autoritaire (%s)\n' % ( machine.id().encode('utf-8'), zone.encode('utf-8') ) # IPv6 if zone in self.zones_v4_to_v6: # Direct zone_v6 = self.zones_v4_to_v6[zone] ipv6 = machine.ipv6() net_v6 = machine.netv6() ligne = "%s\tIN\tAAAA\t%s\n" % (nom, ipv6) direct[zone_v6] = direct.get(zone_v6, "") + ligne if machine.dnsIpv6(): direct[zone] = direct.get(zone, "") + ligne # Reverse zone_rev, length = self.reverse(net_v6, ipv6) rev = '.'.join(ipv6.reverse_dns.split('.')[:length]) ligne = "%s\tIN\tPTR\t%s.\n" % (rev, machine.nom6()) reverse[zone_rev] = reverse.get(zone_rev, "") + ligne # Le direct avec alias for alias in machine.alias() : alias = alias.encode('utf-8') # Cas particulier : nom de l'alias = nom de la zone if alias in self.zones_direct : ligne = "@\tIN\tA\t%s\n" % machine.ip() ligne = ligne.encode('utf-8') direct[alias] = direct.get(alias, "") + ligne if machine.dnsIpv6(): ligne = "@\tIN\tAAAA\t%s\n" % machine.ipv6() ligne = ligne.encode('utf-8') direct[alias]= direct.get(alias, "") + ligne if alias in self.zones_v4_to_v6: ligne = "@\tIN\tAAAA\t%s\n" % machine.ipv6() ligne = ligne.encode('utf-8') zone6 = self.zones_v4_to_v6[alias] direct[zone6] = direct.get(zone6, '') + ligne continue # Bon format ? alias_l = alias.split('.') ok = 0 for i in range(len(alias_l)) : zone_essai = '.'.join(alias_l[i:]) if zone_essai in self.zones_direct : # On est autoritaire sur cette zone # On place donc l'alias dans le fichier de cette zone zone = zone_essai nom = '.'.join(alias_l[:i]) ok = 1 break if not ok: warnings += u'Alias ignoré (mid=%s) : %s\n' % ( machine.id().encode('utf-8'), alias.encode('utf-8') ) continue zone = zone.encode('utf-8') ligne = "%s\tIN\tCNAME\t%s.\n" % ( nom, machine.nom() ) direct[zone] = direct.get(zone, '') + ligne if zone in self.zones_v4_to_v6: zone6 = self.zones_v4_to_v6[zone] ligne = "%s\tIN\tCNAME\t%s.\n" % ( nom, machine.nom6() ) direct[zone6] = direct.get(zone6, '') + ligne # Le reverse ip = machine.ip() net = AddrInNets(ip, self.zones_reverse) if net: base_ip = ip.split('.') base_ip.reverse() zone, length = self.reverse(net, ip) zone = zone.encode('utf-8') ligne = '%s\tIN\tPTR\t%s.\n' % ('.'.join(base_ip[:length]), machine.nom()) try : reverse[zone] += ligne except : reverse[zone] = ligne elif self.verbose >= 2 or machine.nom() not in ('freebox.crans.org', 'ovh.crans.org', 'kokarde.crans.org'): warnings += u'Résolution inverse ignorée (mid=%s) : ip sur zone non autoritaire (%s)\n' % ( machine.id().encode('utf-8'), machine.ip().encode('utf-8') ) ### Ajouts pour les fichiers de résolution directs for zone in direct.keys() : # MXs direct[zone] = MX + direct[zone] ### XXX: création de la zone inverse pour le /48 IPv6 complet du Cr@ns full_net_v6 = self.zones_v6_to_net["##HACK##"] zone_rev, length = self.reverse(full_net_v6, netaddr.IPNetwork(full_net_v6).first) reverse[zone_rev] = reverse.get(zone_rev, "") ### Alias de zone zone_todo = [zone for zone in self.zone_alias] while zone_todo: for zone in zone_todo: for alias in self.zone_alias[zone]: try: direct[alias] = direct[zone] zone_todo.remove(zone) except KeyError: pass if alias in self.zones_v4_to_v6: alias_v6=self.zones_v4_to_v6[alias] zone_v6 = self.zones_v4_to_v6[zone] direct[alias_v6] = direct[zone_v6] ### Ajout des parametres SPF direct['crans.org'] +='; Parametres SPF\n' direct['crans.org'] +='crans.org.\tIN\tTXT\t"v=spf1 a mx ?all"\n' for m in self.MXs: direct['crans.org'] +='%s.\tIN\tTXT\t"v=spf1 a ?all"\n' % m.split()[-1] direct['crans.org'] += '\n' direct['crans.ens-cachan.fr'] ='; Parametres SPF\n' direct['crans.ens-cachan.fr'] +='crans.ens-cachan.fr.\tIN\tTXT\t"v=spf1 a:crans.org mx ?all"\n\n' ### Ajout d'eventuels champs SRV direct['crans.org'] +='; Champs SRV\n' for s in self.SRVs: direct['crans.org'] += s + '\n' direct['crans.org'] += '\n' ### Ajout des délégations de zones for deleg in self.DELEG.keys(): nom, zone = deleg.split('.',1) if not zone in direct.keys(): warnings += u'Délégation ignorée %s : on ne génère pas la zone parent\n' % deleg continue for serv in self.DELEG[deleg]: direct[zone] = direct[zone] + "%s\tIN\tNS\t%s\n" % ( nom, serv ) for zone in direct.keys(): child, parent = zone.split('.',1) if not zone in self.DELEG.keys() and parent in self.zones_direct + [z for l in self.zone_alias.values() for z in l] + self.zones_v4_to_v6.values(): for d in self.DNSs: direct[parent] = direct.get(parent, "") + '%s\tIN\tNS %s.\n' % (child, d) ### Ajout d'eventuel champs DS pour les délégation dnssec for zone,ds in self.DS.items(): for s in ds: direct[zone] += s + '\n' direct[zone] += '\n' ### Ecriture des fichiers de zone et préparation du fichier de définition f = '' for zone, lignes in direct.items() + reverse.items() : if zone in self.zones_dnssec: path = self.DNSSEC_DIR + 'db.' + zone else: path = self.DNS_DIR + 'db.' + zone file = self.DNS_DIR + 'db.' + zone fd = self._open_conf(file,';') fd.write(self.zone_entete % \ { 'zone' : zone, 'serveur_autoritaire' : self.DNSs[0] , 'serial' : serial } ) fd.write('\n') fd.write(DNS) fd.write(lignes) fd.close() os.chmod(file,0664) if short_name(gethostname()) in map(short_name, dns.DNSs[1:]): f += self.zone_template_slave % {'NOM_zone': zone, 'FICHIER_zone': path, 'ip_master_DNS': self.ip_master_DNS} else: f += self.zone_template % { 'NOM_zone' : zone, 'FICHIER_zone' : path } ### Ecriture fichier de définition fd = self._open_conf(self.DNS_CONF,'//') fd.write(f) fd.close() return warnings if __name__ == '__main__' : from config import bcfg2_main hostname = short_name(gethostname()) if hostname == short_name(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() if hostname == short_name(dns.DNSs[0]): print "Ce serveur est également serveur maitre, mais la reconfiguration du DNS maitre se fait par generate." elif hostname == short_name(dns.DELEG['tv.crans.org'][0][0:-1]): print "Serveur maᅵtre pour tv.crans.org, génération de la zone" c = dns() c.gen_tv() import subprocess args="/usr/sbin/ods-signer sign tv.crans.org".split() p=subprocess.Popen(args,stdout=subprocess.PIPE,stderr=subprocess.PIPE) ret=p.communicate() print ret[0].strip() print ret[1].strip() if hostname == short_name(dns.DNSs[0]): print "Ce serveur est également serveur maitre, mais la reconfiguration du DNS maitre se fait par generate." elif hostname == short_name(dns.DNSs[0]): print "Ce serveur est maître ! Utilisez generate." elif hostname in map(lambda fullhostname : short_name(fullhostname),dns.DNSs[1:]+dns.DNSs_private): print "Ce serveur est esclave! Lancez ce script sur %s, puis lancez bcfg2 ici" % bcfg2_main else: print "Ce serveur ne correspond à rien pour la configuration DNS."