Intéressé par des cours d'informatique en ligne ?
Visitez mon nouveau site https://www.yesik.it !

Python dispose en standard de fonctions pour la résolution de noms comme socket.gethostbyname ou socket.getaddrinfo. Pour beaucoup d'applications, c'est suffisant. Mais le DNS permet bien plus de choses que simplement associer une adresse IP à un nom. Si vous voulez exploiter toutes les possibilités offertes par le DNS, il faudra aller au delà. Et c'est exactement cela que permet la bibliothèque Python dnspython.

Celle-ci intéressera autant le développeur qui a besoin d'interroger le DNS dans un de ses programmes, que l'administrateur réseau qui y trouvera, moyennant l'écriture de quelques lignes de Python, une alternative plus souple aux outils classiques dig(1), nslookup(1), nsupdate(1) et autres.

dnspython

Pour citer le site officiel, dnspython est une boîte à outils Python pour le DNS. Toujours d'après ce cite, dnspython était un projet interne développé par la société Nominum pour tester leur solution DNS. Par la suite, cette société en a autorisé la diffusion sous une licence de type BSD.

Sur un système Debian ou dérivé, l'installation de dnspython se résume à l'installation du paquet python-dnspython:

sh# apt-get install python-dnspython

Sur un autre système, s'il n'est pas disponible dans votre gestionnaire de logiciels, il vous faudra en télécharger la dernière version stable puis, après avoir décompressé l'archive, effectuer l'installation à la manière Python:

sh# python setup.py install

Dans tous les cas, la version courante de dnspython au moment où j'écris ces lignes nécessite Python 2.4 ou ultérieur.

Une fois dnspython installé d'une manière ou d'une autre, vous pouvez en vérifier la version:

import dns.version
 
print 'dnspython version',dns.version.version
sh$ python version.py
dnspython version 1.8.0

Utiliser le resolver pour interroger le DNS

Définition: resolver

Le resolver est le logiciel chargé d'interroger le DNS pour obtenir une adresse IP à partir d'un nom d'hôte.

Sur la plupart des systèmes Unix ou Unix-like, le fichier /etc/resolv.conf sert à configurer le fonctionnement du resolver.

Accéder au MX

Le site officiel de dnspython est assez avare en documentation. Le point de départ pour apprendre à utiliser ce paquet reste donc de se référer aux quelques exemples fournis sur le site. Le premier d'entre eux montre comment interroger le DNS pour trouver l'enregistrement MX associé à un nom de domaine. Ceci pourra par exemple être utile pour déterminer le serveur de messagerie vers lequel acheminer les mails destinés aux membres d'un domaine:

import dns.resolver
 
answers = dns.resolver.query('chicoree.fr', 'MX')
for rdata in answers:
    print 'Mail exchange:',rdata.exchange,'preference:',rdata.preference
sh$ python mx.py
Mail exchange: mx0.ovh.net. preference: 1
Mail exchange: mxb.ovh.net. preference: 100

Comme vous le voyez, si vous m'écrivez à sylvain@chicoree.fr votre message transitera par un des deux serveurs mx0.ovh.net ou mxb.ovh.net – la priorité devant être donnée au premier, puisqu'il possède la plus faible priorité d'après la réponse envoyée par le DNS.

Du point de vue du fonctionnement de dnspython maintenant: la méthode dns.resolver.query renvoie un objet de la classe dns.resolver.Answer. Cette classe met en œuvre la plupart des méthodes du protocole séquence, ce qui permet de l'utiliser comme une collection.

Chaque entrée dans l'ensemble des résultats (rdata dans le code) est un objet dont la classe exacte dépend du type de requête effectuée. Ici, il s'agit d'instances de la classe dns.rdtypes.ANY.MX.MX.

Accéder à l'adresse d'un hôte (enregistrements A et AAAA)

Une très brève section si vous avez compris l'exemple précédent: pour accéder à l'adresse d'un hôte, il suffit d'interroger les enregistrements A ou AAAA correspondants:

import dns.resolver
 
host = 'www.freebsd.org'
 
answers_IPv4 = dns.resolver.query(host, 'A')
for rdata in answers_IPv4:
    print 'IPv4:',rdata.address
 
answers_IPv6 = dns.resolver.query(host, 'AAAA')
for rdata in answers_IPv6:
    print 'IPv6:',rdata.address
sh$ python a.py
IPv4: 69.147.83.34
IPv6: 2001:4f8:fff6::22

Comme précédemment, les résultats extraits du DNS sont retournés sous la forme d'objets des classes adaptées. Ici, dns.rdtypes.IN.A.A et dns.rdtypes.IN.AAAA.AAAA.

Reverse DNS (enregistrement PTR)

Cette utilisation est exactement l'inverse de la précédente: moyennant une adresse IP, on cherche à déterminer l'hôte correspondant. Ceci est rendu possible par l’existence d'enregistrements PTR dans le DNS. Par contre, la résolution inverse nécessite que les adresses soient passées sous une forme particulière. dnspython propose une fonction spécifique pour cela: dns.reversename.from_address. Cette fonction est compatible IPv4 et IPv6.

import dns.resolver
import dns.reversename
 
no = dns.reversename.from_address('213.186.33.4')
answers = dns.resolver.query(no, 'PTR')
for rdata in answers:
    print rdata.target
sh$ python reverse.py
cluster003.ovh.net.

Accès aux services (enregistrement SVR)

Un enregistrement potentiellement très intéressant est SRV. Celui-ci est d'introduction assez récente puisqu'il n'a été formalisé qu'en 2000 dans la RFC2782A DNS RR for specifying the location of services (DNS SRV).

En deux mots, cet enregistrement permet d'obtenir les informations nécessaires (hôte et port) pour accéder à un service particulier sur un domaine. Un exemple valant mille mots, voici comment interroger le DNS pour savoir comment on peut accéder au service LDAP sous TCP du domaine openldap.org:

import dns.resolver
import dns.reversename
 
domain = 'openldap.org'
service= 'ldap'
protocol='tcp'
 
name = "_{0}._{1}.{2}".format(service, protocol, domain)
answers = dns.resolver.query(name, 'SRV')
for rdata in answers:
    print rdata.target, "port", rdata.port
sh$ python srv.py
www.openldap.org. port 389

Un point sur le point

J'ai passé ce détail sous silence jusqu'à présent, bien que ce soit déjà visible dans les exemples précédents. Remarquez donc que les noms d'hôtes obtenus en interrogeant le DNS se terminent par un point:

www.openldap.org.

et pas

www.openldap.org 
DNS.png

Le DNS a une structure arborescente au somme de laquelle se trouve la racine. Sous celle-ci on trouve les domaines de premier niveau (top-level domains ou TDL).

Pour construire le nom complet d'un hôte, il faut remonter depuis celui-ci jusqu'à la racine, en séparant le nom de chaque nœud par un point.

C'est contraire à l'usage commun, mais cela semblera familier aux habitués du DNS: en effet, l'espace de nommage du DNS à une structure arborescente dont point désigne la racine. Même si ce dernier élément est habituellement omis, tout chemin absolu devrait se terminer ainsi. On parle aussi dans ce cas de chemin complètement qualifié – ou fully qualified domain name (FQDN). En l'absence de ce point terminal, un chemin devrait être considéré comme relatif. La plupart des resolvers (celui de votre système ou celui utilisé par votre navigateur Web) utilisent une heuristique pour dispenser l'utilisateur d'ajouter ce point terminal quand il fait référence à un site. Ainsi, quand je tape l'URL http://chicoree, le DNS va être interrogé pour:

  • chicoree.virtual et chicoree.chicoree.fr (car virtual et chicoree.fr sont les deux domaines de ma machine).
  • Mais aussi pour www.chicoree.com (mon navigateur web ajoute le préfixe www et le suffixe com à toute nom d'hôte dont l'adresse n'a pu être résolue).
  • Et, par combinaison des deux précédents, les noms www.chicoree.com.virtual et www.chicoree.com.chicoree.fr sont aussi testés.

Le tout en IPv6 (requête AAAA) puis en IPv4 (requête A). Au total 10 requêtes ont été faites au DNS! Au delà de l'anecdote, toujours est-il que dnspython étant un outil pour le DNS, les noms renvoyés sont des noms absolus et donc terminés par ce fameux point. A vous de le supprimer si vous voulez utiliser les noms obtenus dans des applications qui ne l'utilisent pas – ou avant de les présenter à des utilisateurs non-DNS aware.

Gestion des exceptions

dnspython utilise les exceptions pour rapporter les situations ... exceptionnelles. Ainsi, jusqu'à présent, mes exemples renvoyaient tous au moins un résultat. Mais dans le cas ou aucun résultat n'est trouvé une exception dns.resolver.NoAnswer est générée:

import dns.resolver
 
host = 'www.chicoree.fr'
 
try:
    answers = dns.resolver.query(host, 'AAAA')
    for rdata in answers:
        print rdata
except dns.resolver.NoAnswer:
    print "*** No AAAA record for", host, "***"

Tant que je n'aurais pas d'adresse IPv6 pour mon site web, ce programme produira le résultat suivant:

sh$ python noanswer.py
*** No AAAA record for www.chicoree.fr ***

Parmi les situations qu'il faut vous attendre à devoir gérer, il y a aussi celle où le nom de l'hôte demandé n'existe tout simplement pas dans le DNS:

import dns.resolver
 
host = 'bad-host.chicoree.fr'
 
try:
    answers = dns.resolver.query(host, 'A')
    for rdata in answers:
        print rdata
except dns.resolver.NoAnswer:
    print "*** No AAAA record for", host, "***"
except dns.resolver.NXDOMAIN:
    print "*** The name", host, "does not exist ***"

Comme vous le comprenez en examinant le code précédent, en cas d'hôte inexistant, une exception de la classe dns.resolver.NXDOMAIN est générée. Ce qui est confirmé à l'exécution du programme:

sh$ python nxdomain.py
*** The name bad-host.chicoree.fr does not exist ***

Mise à jour dynamique du DNS

A l'origine, le DNS était conçu pour être consulté en ligne. Les mises à jour consistant pour l'administrateur à modifier (éventuellement manuellement) les fichiers de zone – puis à indiquer au serveur DNS de prendre en compte ces modifications.

Mais aujourd'hui, Internet est beaucoup plus dynamique dans sa nature qu'à l'époque. Ainsi, beaucoup d'hôtes obtiennent leur adresse IP par DHCP. C'est à dire sans la garantie d'avoir toujours la même adresse. La RFC2136Dynamic Updates in the Domain Name System prend en compte cette réalité en définissant le moyen pour mettre à jour dynamiquement le DNS grâce à une requête UPDATE. dnspython supporte cette possibilité:

import sys
import dns.query
import dns.update
 
# Real program should test args!
(host, zone) = sys.argv[1].split(".",1)
hostip = sys.argv[2]
nsserver = '10.129.37.2'
 
# Assume a nameserver supporting RFC2136
update = dns.update.Update(zone)
update.add(host, 86400, 'A', hostip)
 
response = dns.query.tcp(update, nsserver)
print response
sh$ python update.py magicarpe.johto.pkmn 10.129.37.129
id 48196
opcode UPDATE
rcode NOERROR
flags QR RA
;ZONE
;PREREQ
;UPDATE
;ADDITIONAL

Pour la mise à jour, le code utilise la fonction de bas niveau dns.query.tcp qui permet d'envoyer une requête déjà préparée. Après exécution, cette fonction renvoit la réponse du serveur sous la forme d'un objet de la classe dns.message.Message. Le champ le plus important de cette réponse est rcode, puisque c'est celui qui va nous permettre de savoir si la mise à jour s'est faite. Dans l'exemple précédent, c'est le cas (rcode NOERROR) car j'ai configuré mon serveur de nom pour autoriser les modifications de la zone johto.pkmn à partir du client utilisé.

Voyons maintenant ce qui se passe si je tente d'ajouter une entrée pour une zone sur laquelle le client n'est pas autorisé à faire des modifications (zone kanto.pkmn):

sh$ python update.py magicarpe.kanto.pkmn 10.129.37.129
id 4451
opcode UPDATE
rcode REFUSED
flags QR RA
;ZONE
;PREREQ
;UPDATE
;ADDITIONAL

Contrairement à ce qui se passait avec le resolver (dns.resolver.query), ici aucune exception n'est générée si le serveur a refusé de faire la mise à jour. Seul l'examen du champ rcode de la réponse permettra de le savoir.

Sécuriser les transactions avec TSIG

Je dois avouer que pour les exemples de mises à jour ci dessus, les autorisations sur le serveur étaient basées sur l'adresse IP du client. Ce qui est fort peu sécurisé! En production, vous reposerez sans doute sur quelque-chose de plus sérieux comme TSIG. Cette technique introduite par BIND8.2 et formalisée dans la RFC2845Secret Key Transaction Authentication for DNS (TSIG) repose sur un secret partagé entre le client et le serveur. Ce secret servant à construire et valider un hash envoyé avec le message.

Ainsi, sur mon serveur (BIND 9), j'ai créé une clé, et j'ai configuré celui-ci pour requérir cette clé avant de modifier la zone hoenn.pkmn.:

dns-server# cd /etc/bind9
dns-server# dnssec-keygen -a HMAC-MD5 -b 128 -n HOST -r /dev/urandom pokedex.pkmn.
Kpokedex.pkmn.+157+16121
dns-server# cat >> named.conf.local << EOF
key "pokedex.pkmn." {
       algorithm hmac-md5;
       secret "cwiWHw05MfPB97GA3LKg3A==";
};

zone "hoenn.pkmn" {
        type master;
        file "/etc/bind/db.hoenn";
        allow-update { key pokedex.pkmn.; };
};
EOF
dns-server# rndc reload
server reload successful

Avec cette configuration, les modifications qui ne sont pas signées avec la clé nécessaire sont refusées par le serveur:

sh$ python update.py roselia.hoenn.pkmn 10.129.37.94
id 43760
opcode UPDATE
rcode REFUSED
flags QR RA
;ZONE
;PREREQ
;UPDATE
;ADDITIONAL

Ici, l'algorithme de chiffrement utilisé est hmac-md5. C'est un algorithme symétrique. Autrement dit, le serveur et ses clients utiliseront la même clé (le secret cwiWHw05MfPB97GA3LKg3A==). Dans la réalité, celle-ci devra donc être transférée entre les deux machines avec toutes les précautions de sécurité nécessaires (ssh par exemple). Et l'accès aux fichiers contenant cette clé devra être limité pour éviter qu'elle ne soit compromise. En effet, comme vous le verrez dans les lignes de code suivantes, le programme python utilisera exactement la même clé:

import sys
import dns.tsigkeyring
import dns.query
import dns.update
 
# Real program should test args!
(host, zone) = sys.argv[1].split(".",1)
hostip = sys.argv[2]
nsserver = '10.129.37.2'
 
# Assume a nameserver supporting RFC2136 (UPDATE)
# and RFC2845 (TSIG)
keyring = dns.tsigkeyring.from_text({
	'pokedex.pkmn.': 'cwiWHw05MfPB97GA3LKg3A=='
})
 
update = dns.update.Update(zone, keyring=keyring)
update.add(host, 86400, 'A', hostip)
 
response = dns.query.tcp(update, nsserver)
print response

Les seules modifications par rapport à la version précédente du programme portent sur l'ajout d'un trousseau (keyring) contenant la clé nécessaire. Maintenant, mon serveur DNS accepte les requêtes en modification puisqu'elles sont signées avec la clé convenable:

sh$ python tsig_update.py roselia.hoenn.pkmn 10.129.37.94
id 15197
opcode UPDATE
rcode NOERROR
flags QR RA
;ZONE
;PREREQ
;UPDATE
;ADDITIONAL

Conclusion

Voila: cette présentation de dnspython est terminée. Mon but n'était certainement pas de fournir un inventaire exhaustif des possibilités de cette bibliothèque, mais simplement de donner un aperçu de ses possibilités et de sa simplicité d'utilisation. Son point faible restant clairement son manque de documentation. Au moins aurais-je ajouté ma petite pierre à l'édifice. Et j'espère vous avoir convaincu d'inclure cet outil à votre panoplie lorsqu'il s'agira de travailler avec le DNS.

Ressources