
Certain vieux utilisent des JID en @jabber.crans.org et sans les SRV on retrouve : "timed out on request for "jabber.crans.org" IN SRV. You should check your DNS configuration." dans les logs.
829 lines
35 KiB
Python
Executable file
829 lines
35 KiB
Python
Executable file
#! /usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
""" Génération de la configuration pour bind9
|
||
|
||
Copyright (C) Valentin Samir
|
||
Licence : GPLv3
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import ssl
|
||
import time
|
||
import base64
|
||
import hashlib
|
||
import binascii
|
||
import netaddr
|
||
if '/usr/scripts' not in sys.path:
|
||
sys.path.append('/usr/scripts')
|
||
|
||
import lc_ldap.shortcuts
|
||
import affich_tools
|
||
from gestion.gen_confs import gen_config
|
||
from socket import gethostname
|
||
from gestion import config
|
||
import config.dns
|
||
|
||
disclamer = """//**************************************************************//
|
||
// Ce fichier est genere par les scripts de gen_confs //
|
||
// Les donnees proviennent de la base LDAP et de la conf //
|
||
// presente au debut du script. Il peut être propagé via bcfg2. //
|
||
// //
|
||
// NE PAS EDITER //
|
||
// //
|
||
//**************************************************************//
|
||
"""
|
||
|
||
def short_name(fullhostname):
|
||
return fullhostname.split(".")[0]
|
||
|
||
class ResourceRecord(object):
|
||
"""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.r_type, self.value)
|
||
else:
|
||
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):
|
||
"""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)
|
||
certtype: type d'enregistrement 0 = CA pinning, 1 = cert pinning, 2 = self trusted CA, 3 = self trusted cert
|
||
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 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')
|
||
except Exception as e:
|
||
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 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__(
|
||
'TYPE52',
|
||
'_%s._%s%s' % (port, proto, '.' + name if name else ''),
|
||
"\# %s 0%s0%s0%s%s" % (len(certhex)/2 +3, certtype, selector, reftype, certhex),
|
||
ttl
|
||
)
|
||
else:
|
||
super(TLSA, self).__init__(
|
||
'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:
|
||
return binascii.b2a_hex(certblob).upper()
|
||
elif reftype == 1:
|
||
hashobj = hashlib.sha256()
|
||
hashobj.update(certblob)
|
||
elif reftype == 2:
|
||
hashobj = hashlib.sha512()
|
||
hashobj.update(certblob)
|
||
else:
|
||
raise ValueError("reftype sould be 0 1 or 2, not %s" % reftype)
|
||
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)
|
||
|
||
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.r_type, self.value)
|
||
else:
|
||
return '%s\tIN\t%s\t"%s"' % (self.name, self.r_type, self.value)
|
||
|
||
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 SRV(ResourceRecord):
|
||
"""Entrée DNS pour les champs SRV"""
|
||
def __init__(self, service, proto, priority, weight, port, target, ttl=None, subdomain=None):
|
||
super(SRV, self).__init__('SRV', '_%s._%s' % (service, proto) + ('.%s' % subdomain if subdomain else ''), '%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):
|
||
"""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[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):
|
||
"""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):
|
||
"""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.append(rr)
|
||
else:
|
||
raise ValueError("You can only add ResourceRecords to a Zone")
|
||
|
||
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))
|
||
|
||
# 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)
|
||
|
||
# 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):
|
||
"""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
|
||
self.ipv6 = ipv6
|
||
self.other_zones = other_zones
|
||
self.subzones = [z for z in self.other_zones if z != self.zone_name and z.endswith(self.zone_name)]
|
||
|
||
self.add(soa)
|
||
for ns in ns_list:
|
||
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):
|
||
"""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]
|
||
if ret == "":
|
||
return "@"
|
||
else:
|
||
return ret
|
||
else:
|
||
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(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', []):
|
||
# 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 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)
|
||
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], 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], 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']:
|
||
# 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)
|
||
self.add_sshfp_record(nom, machine)
|
||
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
|
||
|
||
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)
|
||
self.add_sshfp_record(alias, machine)
|
||
elif to_nom:
|
||
self.add(CNAME(alias, "%s" % to_nom))
|
||
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):
|
||
"""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
|
||
elif ':' in net:
|
||
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
|
||
def reverse(net, ip=None):
|
||
"""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 si elle est fournie, n'importe
|
||
quoi sinon."""
|
||
# 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 _network.prefixlen == 16:
|
||
return ('.'.join(rev_dns_a[2:]), 2)
|
||
elif _network.prefixlen == 24:
|
||
return ('.'.join(rev_dns_a[1:]), 1)
|
||
else:
|
||
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):
|
||
"""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]
|
||
subnets = []
|
||
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))
|
||
elif net.prefixlen > 16:
|
||
subnets.extend(net.subnet(24))
|
||
elif net.prefixlen > 8:
|
||
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'
|
||
elif self.ipv6:
|
||
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]:
|
||
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):
|
||
"""Classe de configuration du DNS (les services, generate, toussa)"""
|
||
######################################PARTIE DE CONFIGURATION
|
||
|
||
### Fichiers à écrire
|
||
# Répertoire d'écriture des fichiers de zone
|
||
DNS_DIR = config.dns.DNS_DIR
|
||
DNSSEC_DIR = config.dns.DNSSEC_DIR
|
||
# Fichier de définition des zones pour le maître
|
||
DNS_CONF = config.dns.DNS_CONF
|
||
|
||
# Fichier de définition des zones pour les esclaves géré par BCfg2
|
||
DNS_CONF_BCFG2 = config.dns.DNS_CONF_BCFG2
|
||
|
||
### Liste DNS
|
||
# Le premier doit être le maitre
|
||
|
||
### 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 le nom sans point à la fin
|
||
# { nom_de_zone : [ ns1, ns2, ...]
|
||
DELEG = {}
|
||
|
||
### Serveurs de mail
|
||
# format : [ priorité serveur , .... ]
|
||
MXs = [MX('@', config.dns.MXs[_mx].get('prio', 25), "%s." %_mx) for _mx in config.dns.MXs]
|
||
|
||
SRVs = {
|
||
'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'),
|
||
SRV('sip', 'udp', 5, 0, 5060, 'asterisk'),
|
||
SRV('sip', 'tcp', 5, 0, 5060, 'asterisk'),
|
||
SRV('sips', 'tcp', 5, 0, 5061, 'asterisk'),
|
||
SRV('stun', 'udp', 5, 0, 3478, 'asterisk'),
|
||
|
||
# Quelques ancien utilisent le server XMPP avec des addresses de la forme
|
||
# login@jabber.crans.org, aussi les clients XMPP et autres serveurs de la
|
||
# fédération veulent ces ResourceRecord
|
||
SRV('jabber', 'tcp', 5, 0, 5269, 'xmpp', subdomain="jabber"),
|
||
SRV('xmpp-server', 'tcp', 5, 0, 5269, 'xmpp', subdomain="jabber"),
|
||
SRV('xmpp-client', 'tcp', 5, 0, 5222, 'xmpp', subdomain="jabber"),
|
||
],
|
||
}
|
||
|
||
SPFs = {
|
||
'crans.org': [
|
||
TXT('@', 'v=spf1 mx ~all'),
|
||
],
|
||
}
|
||
|
||
NON_CLONABLE_SPFs = {
|
||
'crans.org': [
|
||
TXT(short_name(_mx), 'v=spf1 mx:crans.org ~all') for _mx in config.dns.MXs
|
||
],
|
||
}
|
||
|
||
|
||
NAPTRs = {
|
||
'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 ] }
|
||
# 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
|
||
DSs = {
|
||
'crans.org': [
|
||
DS('adm', '64649 8 2 9c45f0fef063672d96c983d5a3813a08a649c72d357f41ddece73ae8872d60cf'),
|
||
DS('wifi', '5531 8 2 daf30a647566234edc1617546fd74abbbaf965b17389248f72fc66a33d6f5063'),
|
||
DS('tv', '18199 8 2 d3cc2f5f81b830cbb8894ffd32c236e968edd3b0c0305112b6eb970aa763418e'),
|
||
],
|
||
}
|
||
|
||
|
||
hostname = short_name(gethostname())
|
||
serial = int(time.time()) + 1000000000
|
||
TTL = 3600
|
||
|
||
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):
|
||
"""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.NAPTRs, self.DSs, self.EXTRAS, self.SPFs, self.NON_CLONABLE_SPFs]:
|
||
if zone.zone_name in rr_type.keys():
|
||
zone.extend(rr_type[zone.zone_name])
|
||
for m in machines:
|
||
zone.add_machine(m)
|
||
self.anim.cycle()
|
||
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:
|
||
# 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):
|
||
"""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):
|
||
# 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={}):
|
||
"""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:
|
||
# 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.NAPTRs, self.DSs, self.SPFs]:
|
||
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)
|
||
machines.extend(conn.machinesMulticast())
|
||
self.populate_zones(zones, machines)
|
||
|
||
# Doit être fait après populate_zones lorsque l'on a l'intention d'écrire les fichiers de zone
|
||
# En effet, la génération de la zone clone dépend du contenue de la zone originale
|
||
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.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(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 "%(zone_name)s" {
|
||
type master;
|
||
file "%(zone_path)s";
|
||
};
|
||
"""
|
||
zones = self.gen_zones(self.TTL, self.serial, config.dns.DNSs)
|
||
with open(self.DNS_CONF, 'w') as f:
|
||
f.write(disclamer)
|
||
for zone in zones.values():
|
||
zone.write(os.path.join(self.DNS_DIR, 'db.%s' % (zone.zone_name,)))
|
||
if zone.zone_name in config.dns.zones_dnssec:
|
||
zone_path = os.path.join(self.DNSSEC_DIR, 'db.%s' % (zone.zone_name,))
|
||
else:
|
||
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):
|
||
"""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";
|
||
masters { %(master_ip)s; };
|
||
};
|
||
"""
|
||
zones = self.gen_zones(self.TTL, self.serial, config.dns.DNSs, populate=False)
|
||
with open(self.DNS_CONF_BCFG2, 'w') as f:
|
||
f.write(disclamer)
|
||
for zone in zones.values():
|
||
if zone.zone_name in config.dns.zones_dnssec:
|
||
zone_path = os.path.join(self.DNSSEC_DIR, 'db.%s' % (zone.zone_name,))
|
||
else:
|
||
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):
|
||
self.gen_master()
|
||
|
||
def __str__(self):
|
||
return "DNS"
|
||
|
||
|
||
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)."
|
||
CONFIG = dns()
|
||
CONFIG.gen_slave()
|
||
elif HOSTNAME == short_name(config.dns.DNSs[0]):
|
||
print "Serveur maître :"
|
||
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()
|
||
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 [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."
|
||
|
||
|