scripts/gestion/gen_confs/bind.py

559 lines
24 KiB
Python
Executable file

#! /usr/bin/env python
# -*- coding: iso-8859-15 -*-
""" 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
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',
}
# 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', '20 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=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())
# 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 += '%(zone)s.\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"
### 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'
### 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('iso-8859-1')
except :
warnings += u'Machine ignorée (mid=%s) : format nom incorrect (%s)\n' % ( machine.id().encode('iso-8859-1'), machine.nom().encode('iso-8859-1') )
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 zone in self.zone_alias:
for alias in self.zone_alias[zone]:
direct[alias] = direct.get(alias, "") + 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('iso-8859-1'), zone.encode('iso-8859-1') )
# 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
if zone in self.zone_alias:
for alias in self.zone_alias[zone]:
if alias in self.zones_v4_to_v6:
alias_v6=self.zones_v4_to_v6[alias]
direct[alias_v6] = direct.get(alias_v6, "") + ligne
if machine.dnsIpv6():
direct[alias] = direct.get(alias, "") + 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('iso-8859-1')
# 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('iso-8859-1')
direct[alias] = direct.get(alias, "") + ligne
if alias in self.zone_alias:
for alias2 in self.zone_alias[alias]: direct[alias2] = direct.get(alias2, "") + ligne
if machine.dnsIpv6():
ligne = "@\tIN\tAAAA\t%s\n" % machine.ipv6()
ligne = ligne.encode('iso-8859-1')
direct[alias]= direct.get(alias, "") + ligne
if alias in self.zone_alias:
for alias2 in self.zone_alias[alias]: direct[alias2] = direct.get(alias2, "") + ligne
if alias in self.zones_v4_to_v6:
ligne = "@\tIN\tAAAA\t%s\n" % machine.ipv6()
ligne = ligne.encode('iso-8859-1')
zone6 = self.zones_v4_to_v6[alias]
direct[zone6] = direct.get(zone6, '') + ligne
if alias in self.zone_alias:
for alias2 in self.zone_alias[alias]:
if alias2 in self.zones_v4_to_v6:
alias26=self.zones_v4_to_v6[alias2]
direct[alias26] = direct.get(alias26, "") + 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('iso-8859-1'), alias.encode('iso-8859-1') )
continue
zone = zone.encode('iso-8859-1')
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
if zone in self.zone_alias:
for alias in self.zone_alias[zone]:
direct[alias] = direct.get(alias, '') + ligne
if alias in self.zones_v4_to_v6:
alias6 = self.zones_v4_to_v6[alias]
direct[alias6] = direct.get(alias6, '') + 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('iso-8859-1')
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('iso-8859-1'), machine.ip().encode('iso-8859-1') )
### Ajouts pour les fichiers de résolution directs
for zone in direct.keys() :
# MXs
direct[zone] = MX % { 'zone' : zone } + 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, "")
### 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 )
### 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."