scripts/gestion/gen_confs/bind.py

844 lines
36 KiB
Python
Executable file
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#! /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 selector != 0:
raise NotImplementedError("selector different form 0 not implemented")
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))
self.certhex = certhex
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)
if len(self.value) > 200:
self.value = '( "' + '"\n\t\t\t\t"'.join([self.value[x:x+200] for x in xrange(0, len(self.value), 200)]) + '" )'
else:
self.value = '"%s"' % (self.value,)
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']:
if not cert.get('revocked', [False])[0]:
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'),
],
}
DKIM = {
'crans.org': [
TXT('mail._domainkey', 'v=DKIM1; k=rsa; p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtwkNVd9Mmz8S4WcfuPk0X2drG39gS8+uxAv8igRILgzWeN8j2hjeZesl8pm/1UTVU87bYcdfUgXiGfQy9nR5p/Vmt2kS7sXk9nsJ/VYENgb3IJQ6paWupSTFMyeKycJ4ZHCEZB/bVvifoG6vLKqW5jpsfCiOcfdcgXATn0UPuVx9t93yRrhoEMntMv9TSodjqd3FKCtJUoh5cNQHo0T6dWKtxoIgNi/mvZ92D/IACwu/XOU+Rq9fnoEI8GukBQUR5AkP0B/JrvwWXWX/3EjY8X37ljEX0XUdq/ShzTl5iK+CM83stgkFUQh/rpww5mnxYEW3X4uirJ7VJHmY4KPoIU+2DPjLQj9Hz63CMWY3Ks2pXWzxD3V+GI1aJTMFOv2LeHnI3ScqFaKj9FR4ZKMb0OW2BEFBIY3J3aeo/paRwdbVCMM7twDtZY9uInR/NhVa1v9hlOxwp4/2pGSKQYoN2CkAZ1Alzwf8M3EONLKeiC43JLYwKH1uBB1oikSVhMnLjG0219XvfG/tphyoOqJR/bCc2rdv5pLwKUl4wVuygfpvOw12bcvnTfYuk/BXzVHg9t4H8k/DJR6GAoeNAapXIS8AfAScF8QdKfplhKLJyQGJ6lQ75YD9IwRAN0oV+8NTjl46lI/C+b7mpfXCew+p6YPwfNvV2shiR0Ez8ZGUQIcCAwEAAQ==')
],
}
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, self.DKIM]:
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."