Je suis Charlie

Autres trucs

Accueil

Seulement les RFC

Seulement les fiches de lecture

Mon livre « Cyberstructure »

Ève

dnspython, faire du DNS en Python

Première rédaction de cet article le 17 juillet 2012


Si on est programmeur, et qu'on veut interagir avec le DNS en Python, il existe plusieurs bibliothèques possibles (et, comme souvent en Python, de qualité variable et parfois médiocre, ce qui confusionne le nouveau programmeur qui ne sait pas à quelle bibliothèque se vouer). Mais dnspython est nettement la meilleure. Voici quelques informations de base sur cette bibliothèque.

Je vais faire un article que j'espère concret, donc on va commencer tout de suite par un exemple. Supposons qu'on veuille récupérer le MX de google.com :

import dns.resolver

answers = dns.resolver.query('google.com', 'MX')
for rdata in answers:
    print 'Host', rdata.exchange, 'has preference', rdata.preference

Et, si on l'utilise, on a bien le résultat attendu :

% python mx.py
Host alt4.aspmx.l.google.com. has preference 50
Host alt3.aspmx.l.google.com. has preference 40
Host alt2.aspmx.l.google.com. has preference 30
Host aspmx-v4v6.l.google.com. has preference 10
Host alt1.aspmx.l.google.com. has preference 20

Ce programme utilise l'interface de haut niveau de dnspython (dns.resolver). Elle est très simple à utiliser mais ne permet pas de choisir toutes les options comme on veut. Il existe aussi une interface de bas niveau, présentée plus loin.

En créant explicitement un objet Resolver (dans le code ci-dessus, il était créé implicitement lors de l'appel à query()), on peut quand même configurer quelques options. Ici, on va faire de l'EDNS pour récupérer les DNSKEY de .fr (la réponse fait près de 1 500 octets, plus que la vieille limite de 512 du DNS d'autrefois) :

 
import dns.resolver

edns_size = 1500

myresolver = dns.resolver.Resolver()
myresolver.use_edns(0, 0, edns_size)
result = myresolver.query ('fr.', 'DNSKEY')
for item in result:
    print item

Cela donne :

% python dnskey.py
257 3 8 AwEAAYz/bZVFyefKTiBBFW/aJcWX3IWH c8iI7Wi01mcZNHGC+w5EszmtHcsK/ggI u+V7lnJlHcamTNstxnSl4DAzN2Mgzux7 sqd7Jlqa+BtRoI9MO3l2yi+qE6WIViUS 0U3atm+l1SQsAgkyXlYBN52Up1jsVZ+F Nt+kKGKzvIMo8/qM3mCYxkLhazj+q4Bg 7bH+yNhHOaZ5KAiFO++8wZJK2rwrh2Yd 5LP+smStJoij42sYALLA9WPdeiSaoxpb jmpYBUQAoDUYdqLnRWOnQds0O1EO5h1P XGSIt9feZpwNbPzTfHL80q2SI1A7qRzf Z6BzA2jZU4BLCv2YhKCcWo3kfbU=
257 3 8 AwEAAc0RXL9OWfbNQj2ptM8KkzMxoHPO qPTy5GvIzQe3uVRfOXAgEQPIs4QzHS1K bQXq4UV8HxaRKjmg/0vfRkLweCXIrk7g Mn5l/P3SpQ9MyaC3IDmCZzTvtfC5F4Kp Zi2g7Kl9Btd+lJuQq+SJRTPDkgeEazOf syk95cm3+j0Sa6E8oNdPXbqyg5noq2WW PAhq3PYKBm062h5F5PCXRotYVoMiId3Z q37iIdnKmGuNbr9gBZD2stVP6B88NeuY N7yaY23RftkjCp5mw4v/GzjoRrYxzk5s YKNNYaN099ALn3Z2rVzuJqpPiOLH71dK pN+f/3YHmh4hhgImPM3ehlK0L8E=
...

La méthode use_edns nous a permis de dire qu'on voulait de l'EDNS, version 0 (la seule actuelle ; et attention, c'est -1 qu'il faudrait passer pour débrayer EDNS). Notez aussi le point à la fin de fr, dnspython a du mal avec les noms ne comportant qu'un seul composant, sauf s'ils sont terminés par un point.

Notez enfin que les résultats sont, par défaut, affichés au format dit « présentation » des fichiers de zone traditionnels (RFC 1035, section 5). Les enregistrements DNS ont deux formats, un sur le câble et un de présentation. dnspython sait lire ce format de présentation, et l'écrire. Dans le programme précédent, on lisait explicitement les champs de la réponse (exchange et preference). Ici, dans le second programme, on a affiché toute la réponse.

Au fait, que renvoie exactement query() ? Il renvoie un dns.resolver.Answer qui est itérable (donc on peut l'utiliser dans une boucle, comme dans les programmes ci-dessus).

Et comment je sais tout cela ? Où l'ai-je appris ? dnspython a une documentation en ligne. On peut y apprendre les noms des champs du MX, les paramètres de query() ou encore la complexité du type Answer.

Si on veut afficher un type un peu complexe, mettons un NAPTR, on doit donc lire la documentation, on trouve les noms des champs et on peut écrire un joli programme :

import dns.resolver
import sys

if len(sys.argv) <= 1:
    raise Exception("Usage: %s domainname ..." % sys.argv[0])

for name in sys.argv[1:]:
    answers = dns.resolver.query(name, 'NAPTR')
    for rdata in answers:
        print """
    %s
    Order: %i
    Flags: %s
    Regexp: %s
    Replacement: %s
    Service: %s
    """ % (name, rdata.order, rdata.flags, rdata.regexp, rdata.replacement,
    rdata.service)

Qui va nous donner :

% python naptr.py http.uri.arpa de.

    http.uri.arpa
    Order: 0
    Flags: 
    Regexp: !^http://([^:/?#]*).*$!\1!i
    Replacement: .
    Service: 
    

    de.
    Order: 100
    Flags: s
    Regexp: 
    Replacement: _iris-lwz._udp.de.
    Service: DCHK1:iris.lwz

dnspython permet également de faire des transferts de zone (RFC 5936) :

import dns.query
import dns.zone

zone = dns.zone.from_xfr(dns.query.xfr('78.32.75.15', 'dnspython.org'))
names = zone.nodes.keys()
names.sort()
for name in names:
        print zone[name].to_text(name)

L'adresse du serveur maître est ici en dur dans le code. La récupérer dynamiquement est laissé comme exercice :-)

Par défaut, l'objet Resolver a récupéré les adresses IP des résolveurs à utiliser auprès du sytème (/etc/resolv.conf sur Unix). Ces adresses sont dans un tableau qu'on peut modifier si on le désire. Ici, on affiche la liste des résolveurs :

import dns.resolver

myresolver = dns.resolver.Resolver()
print myresolver.nameservers

dnspython fournit également un grand nombre de fonctions de manipulation des noms de domaine. Par exemple, pour trouver le nom permettant les résolutions d'adresses IP en noms (requêtes PTR), on a dns.reversename :

import dns.reversename

print dns.reversename.from_address("2001:db8:42::bad:dcaf")
print dns.reversename.from_address("192.0.2.35")

qui donne :

% python manip.py
f.a.c.d.d.a.b.0.0.0.0.0.0.0.0.0.0.0.0.0.2.4.0.0.8.b.d.0.1.0.0.2.ip6.arpa.
35.2.0.192.in-addr.arpa.

Pour analyser des composants d'un nom de domaine, on a dns.name :

import dns.name

parent = 'afnic.fr'
child = 'www.afnic.fr'
other = 'www.gouv.fr'

p = dns.name.from_text(parent)
c = dns.name.from_text(child)
o = dns.name.from_text(other)
print "%s is a subdomain of %s: %s" % (c, p, c.is_subdomain(p))         
print "%s is a parent domain of %s: %s" % (c, p, c.is_superdomain(p))       
print "%s is a subdomain of %s: %s" % (o, p, o.is_subdomain(p))         
print "%s is a parent domain of %s: %s" % (o, p, c.is_superdomain(o))       
print "Relative name of %s in %s: %s" % (c, p, c.relativize(p))
# Method labels() returns also the root
print "%s has %i labels" % (p, len(p.labels)-1)
print "%s has %i labels" % (c, len(c.labels)-1)

Il nous donne, comme on pouvait s'y attendre :

www.afnic.fr. is a subdomain of afnic.fr.: True
www.afnic.fr. is a parent domain of afnic.fr.: False
www.gouv.fr. is a subdomain of afnic.fr.: False
www.gouv.fr. is a parent domain of afnic.fr.: False
Relative name of www.afnic.fr. in afnic.fr.: www
afnic.fr. has 2 labels
www.afnic.fr. has 3 labels

Et les mises à jour dynamiques du RFC 2136 ? dnspython les permet :

import dns.query
import dns.tsigkeyring
import dns.update

keyring = dns.tsigkeyring.from_text({
    'foobar-example-dyn-update.' : 'WS7T0ISoaV+Myge+G/wemHTn9mQwMh3DMwmTlJ3xcXRCIOv1EVkNLlIvv2h+2erWjz1v0mBW2NPKArcWHENtuA=='
})

update = dns.update.Update('foobar.example', keyring=keyring,
                           keyname="foobar-example-dyn-update.")
update.replace('www', 300, 'AAAA', '2001:db8:1337::fada')

response = dns.query.tcp(update, '127.0.0.1', port=53)

print response

Ici, on s'authentifier auprès du serveur faisant autorité en utilisant TSIG (RFC 8945). On remplace ensuite l'adresse IPv6 (AAAA) de www.foobar.example par 2001:db8:1337::fada.

J'ai parlé plus haut de l'interface de bas niveau de dnspython. Elle offre davantage de possibilités mais elle est plus complexe. Des choses comme la retransmission (en cas de perte d'un paquet) doivent être gérées par le programmeur et non plus par la bibliothèque. Voici un exemple complet pour obtenir l'adresse IPv6 d'une machine :

import dns.rdatatype
import dns.message
import dns.query
import dns.resolver

import sys

MAXIMUM = 4
TIMEOUT = 0.4

if len(sys.argv) > 3 or len(sys.argv) < 2:
    print >>sys.stderr, ("Usage: %s fqdn [resolver]" % sys.argv[0])
    sys.exit(1)

name = sys.argv[1]
if len(sys.argv) == 3:
    resolver = sys.argv[2]
else:
    resolver = dns.resolver.get_default_resolver().nameservers[0]

try:
    message = dns.message.make_query(name, dns.rdatatype.AAAA, use_edns=0, payload=4096,
                                 want_dnssec=True)
except TypeError: # Old DNS Python... Code here just as long as it lingers in some places
    message = dns.message.make_query(name, dns.rdatatype.AAAA, use_edns=0, 
                                 want_dnssec=True)
    message.payload = 4096
        
done = False
tests = 0
while not done and tests < MAXIMUM:
    try:
        response = dns.query.udp(message, resolver, timeout=TIMEOUT)
        done = True
    except dns.exception.Timeout:
        tests += 1

if done:
    print "Return code: %i" % response.rcode()
    print "Response length: %i" % len(response.to_wire())
    for answer in response.answer:
        print answer
else:
    print "Sad timeout"

On a quand même utilisé dns.resolver pour récupérer la liste des résolveurs. On fabrique ensuite un message DNS avec make_query(). On fait ensuite une boucle jusqu'à ce qu'on ait une réponse... ou que le nombre maximum d'itérations soit atteint. Notez qu'il reste encore des tas de détails non gérés : on n'utilise que le premier résolveur de la liste (il faudrait passer aux suivants en cas de délai expiré), on ne bascule pas en TCP si la réponse est tronquée, etc.

Le code ci-dessus donne, par exemple :

% python resolver-with-low-level.py www.ripe.net
Return code: 0
Response length: 933
www.ripe.net. 21374 IN AAAA 2001:67c:2e8:22::c100:68b
www.ripe.net. 21374 IN RRSIG AAAA 5 3 21600 20120816100223 20120717090223 16848 ripe.net. Q0WRfwauHmvCdTI5mqcqRCeNaI1jOFN8 Z0B+qwsac3VRqnk9xVGtmSjhGWPnud/0 pS858B4vUZFcq8x47hnEeA9Ori5mO9PQ NLtcXIdEsh3QVuJkXSM7snCx7yHBSVeV 0aBgje/AJkOrk8gS62TZHJwAHGeDhUsT +CvAdVcURrY=

Voilà, c'était juste une toute petite partie des possibilités de dnspython, j'espère que cela vous aura donné envie de voir le reste. Quelques autres articles :

Bonne programmation.

Version PDF de cette page (mais vous pouvez aussi imprimer depuis votre navigateur, il y a une feuille de style prévue pour cela)

Source XML de cette page (cette page est distribuée sous les termes de la licence GFDL)