Je suis Charlie

Autres trucs

Accueil

Seulement les RFC

Seulement les fiches de lecture

Mon livre « Cyberstructure »

Ève

Documentation technique de mon résolveur DoH

Première rédaction de cet article le 30 septembre 2019
Dernière mise à jour le 28 septembre 2021


Cet article documente le fonctionnement interne du résolveur DoH https://doh.bortzmeyer.fr/ et du résolveur DoT dot.bortzmeyer.fr. Il concerne donc essentiellement les technicien·ne·s. Si vous vous intéressez plutôt aux conditions d'utilisation de ce service, voyez l'article décrivant la politique.

Pour comprendre les protocoles DoH et DoT, il faut relire les normes qui les décrivent, respectivement les RFC 8484 et RFC 7858.

C'est le même résolveur qui gère les deux protocoles. Si une version initiale de ce résolveur DoH utilisait une mise en œuvre en Python que j'avais écrite lors d'un hackathon de l'IETF, la version « de production » se sert de l'excellent logiciel dnsdist. dnsdist est un frontal pour serveurs DNS, assurant des fonctions telles que la répartition de charge et, ce qui nous intéresse surtout ici, le relais entre différents protocoles. Ici, dnsdist a été configuré pour accepter des connexions DoH et DoT (mais pas le traditionnel DNS sur UDP, ni sur TCP en clair, d'ailleurs) et pour relayer les requêtes DNS vers le « vrai » résolveur.

La machine est une Arch Linux tournant chez OVH, sur l'offre nommée « VPS ».

La configuration de dnsdist se trouve dans un fichier de configuration, dnsdist.conf. N'hésitez pas à consulter la documentation très complète de dnsdist. Voyons les points essentiels.

On fait du DoH donc on lance un service DoH, en IPv4 et en IPv6 :

    
addDOHLocal("0.0.0.0:443", "/etc/dnsdist/server-doh.pem", "/etc/dnsdist/server-doh.key", {"/", "/rfc", "/about", "/policy", "/help"}, {minTLSVersion="tls1.2", customResponseHeaders={["link"]="<https://www.bortzmeyer.org/doh-bortzmeyer-fr-policy.html> rel=\"service-meta\"; type=\"text/html\""}}) 
addDOHLocal("[::]:443", "/etc/dnsdist/server-doh.pem", "/etc/dnsdist/server-doh.key", {"/", "/rfc", "/about", "/policy", "/help"}, {minTLSVersion="tls1.2", customResponseHeaders={["link"]="<https://www.bortzmeyer.org/doh-bortzmeyer-fr-policy.html> rel=\"service-meta\"; type=\"text/html\""}})

  

Pour améliorer un peu la sécurité, on exige du TLS 1.2 au minimum. Notez qu'il y aurait plein d'autres paramètres à configurer (refuser des algorithmes trop faibles), pour avoir une meilleure note sur SSLlabs. Et les trucs bizarres qui commencent par customResponseHeaders ? Il s'agit d'utiliser la technique du RFC 8631 pour indiquer des méta-informations sur le service, ici, la politique suivie. Cela donne :


% curl -v https://doh.bortzmeyer.fr/help
...
< HTTP/2 200 
< server: h2o/dnsdist
< link: <https://www.bortzmeyer.org/doh-bortzmeyer-fr-policy.html> rel="service-meta"; type="text/html"
< content-length: 86
< 

  

Pour avoir des URL « spéciaux » ne faisant pas du DoH, on ajoute aussi :


supportpagemap = { newDOHResponseMapEntry("^/rfc$", 307, "https://www.rfc-editor.org/info/rfc8484"),
                   newDOHResponseMapEntry("^/about$", 307, "https://www.bortzmeyer.org/doh-bortzmeyer-fr-policy.html"),
                   newDOHResponseMapEntry("^/policy$", 307, "https://www.bortzmeyer.org/doh-bortzmeyer-fr-policy.html"),
		   newDOHResponseMapEntry("^/help$", 200, "For the server policy, see <https://www.bortzmeyer.org/doh-bortzmeyer-fr-policy.html>.") }
dohFE = getDOHFrontend(0)
dohFE:setResponsesMap(supportpagemap)
dohFE6 = getDOHFrontend(1)
dohFE6:setResponsesMap(supportpagemap)

Ces instructions disent à dnsdist de rediriger /policy vers l'article décrivant la politique, et d'afficher un court message pour /help.

Et on veut faire du DoT, pas seulement du DoH, donc on met aussi :

addTLSLocal("0.0.0.0:853", "/etc/dnsdist/server-dot.pem", "/etc/dnsdist/server-dot.key", {minTLSVersion="tls1.2"})
addTLSLocal("[::]:853", "/etc/dnsdist/server-dot.pem", "/etc/dnsdist/server-dot.key", {minTLSVersion="tls1.2"})

Le résolveur est public, donc les ACL autorisent tout le monde :

addACL('0.0.0.0/0')
addACL('[::]/0')
  

Mais on se méfie quand même des clients trop enthousiastes donc on les limite à cent requêtes par seconde :

addAction(MaxQPSIPRule(100), DropAction()) 
  

Au passage, il faut dire un mot de addAction car il sert souvent. dnsdist permet de définir des politiques à appliquer lors du traitement d'une requête DNS (la documentation dit « paquet », mais, apparemment, il s'agit bien d'une requête). La syntaxe générale (cf. la documentation) est addAction(critère, action) avec de nombreux critères possibles (ici, « a fait plus de 100 r/s ») et plein d'actions disponibles (ici, ignorer la requête).

dnsdist est un répartiteur de charge, il n'est pas un résolveur DNS. Il faut donc un ou plusieurs « vrais » résolveurs derrière. Le principal résolveur, pour le service https://doh.bortzmeyer.fr, est un Unbound qui tourne sur la même machine. Au cas où quelque chose irait mal, un second résolveur, complètement indépendant, est fourni par OVH. dnsdist permet de choisir quel résolveur utiliser et j'ai mis :

  
setServerPolicy(firstAvailable)
newServer({address="127.0.0.1:53", name="Local-Unbound"})	
newServer({address="213.186.33.99:53", name="OVH"})

Pourquoi firstAvailable ? Parce que je voulais éviter autant que possible d'envoyer des requêtes à ce second résolveur que je ne contrôle pas et dont je ne connais pas la politique en matière de vie privée. Comme voulu, la quasi-totalité des requêtes arrivent donc au premier résolveur :

> showServers()
#   Name                 Address                       State     Qps    Qlim Ord Wt    Queries   Drops Drate   Lat Outstanding Pools
0   Local-Unbound        127.0.0.1:53                     up     0.0       0   1  1       2509       0   0.0  70.4           0 
1   OVH                  213.186.33.99:53                 up     0.0       0   1  1          0       0   0.0   0.0           0 
All                                                              0.0                      2509       0                         
  

Pour pouvoir utiliser la console de dnsdist, comme ci-dessus, il faut l'activer dans le fichier de configuration :

controlSocket('[::1]:5199')
setKey("kg...=")
  

(La documentation explique quelle valeur indiquer à setKey().) L'accès à la console se fait ensuite avec dnsdist -c :

%  dnsdist -c
> dumpStats()
acl-drops              	          0    latency0-1             	       5058
cache-hits             	       4429    latency1-10            	        351
cache-misses           	       2571    latency10-50           	        785
cpu-sys-msec           	      12929    latency100-1000        	        388
cpu-user-msec          	      47305    latency50-100          	        280
...
 

dnsdist dispose également d'un serveur Web minimal, permettant de regarder l'état du service (ce n'est pas un site Web d'administration, c'est juste pour jeter un coup d'œil). Voici une configuration typique :

webserver("[::1]:8082", "2e...", "a5..")
 

Les deux derniers paramètres indiquent le mot de passe à utiliser pour les accès au site Web et la clé pour l'API. Ici, le serveur n'écoute que sur une adresse locale. Si vous voulez le rendre accessible de l'extérieur, comme il ne gère pas HTTPS, il vaut mieux le mettre derrière un relais. J'ai utilisé stunnel, avec cette configuration :

[dnsdist]
accept  = 8083
connect = localhost-ipv6:8082
cert = /etc/stunnel/stunnel.pem
key = /etc/stunnel/stunnel.key
 

Cela me permet de regarder de l'extérieur : doh-web.png

Et pour l'API de ce serveur interne, qui renvoie des résultats en JSON :

% curl -s --header "X-API-Key: XXXX" https://doh.bortzmeyer.fr:8083/api/v1/servers/localhost/statistics | jq .
[
  {
    "name": "responses",
    "type": "StatisticItem",
    "value": 2643
  },
  {
    "name": "queries",
    "type": "StatisticItem",
    "value": 3698
  },
...
   

Il y a aussi quelques paramètres liés aux performances du serveur. Celui-ci active la mémoire de dnsdist, qui gardera au maximum 100 000 enregistrements DNS :

  
pc = newPacketCache(100000)
getPool(""):setCache(pc)			

Pour l'instant, cette valeur est énorme pour ce modeste serveur. Dans la console :

> getPool(""):getCache():printStats()
Entries: 84/100000
Hits: 1127
Misses: 2943
...

Cette mémoire explique pourquoi, plus haut, le serveur indiquait davantage de requêtes que de réponses (il ne compte comme réponse que ce qui a été traité par les vrais résolveurs). Autres réglages, portant sur le réseau, qu'il me reste à ajuster dès que le serveur recevra du trafic abondant :

setMaxUDPOutstanding(65535)     -- Nombre maximum de requêtes en attente pour un résolveur
setMaxTCPClientThreads(30) 	-- Nombre maximum de fils d'exécution TCP (chacun pouvant traiter plusieurs clients)
setMaxTCPConnectionDuration(1800) -- Après trente minutes, on raccroche
setMaxTCPQueriesPerConnection(300) -- Après trois cents requêtes, on raccroche 
setMaxTCPConnectionsPerClient(10) -- Dix connexions pour un seul client, c'est déjà beaucoup, mais il faut penser à des choses comme le CG-NAT
  

dnsdist permet d'enregistrer chaque requête DNS, par exemple dans un fichier. Cette fonction n'est pas activée sur https://doh.bortzmeyer.fr car elle serait contraire aux promesses faites quant au respect de la vie privée. Mais c'est un bon exemple d'utilisation de addAction pour toutes les requêtes (AllRule) :

-- addAction(AllRule(), LogAction("/tmp/dnsdist.log", false, true, false))

(Attention si vous utilisez systemd, avec l'option PrivateTmp=true, le fichier journal sera mis quelque part sous /tmp/systemd-private-XXXXXXX-dnsdist.service-Ijy8yw, pour être plus difficile à trouver.) Les lignes journalisées sont du genre :

Packet from [2001:db8::a36b::64]:44364 for www.ietf.org. AAAA with id 8283

Pour la même raison de vie privée, je n'utilise pas la TeeAction (qui permet de copier les requêtes ailleurs) ou dnstap.

De même qu'on ne journalise pas, on ne ment pas. dnsdist peut le faire via SpoofAction() ou bien avec un script Lua spécifique, pour faire des choses horribles comme bloquer un type de données particulier :

luarule(dq) if (dq.qtype==dnsdist.NAPTR) then return DNSAction.Nxdomain, "" else return DNSAction.Allow, "" end end
addLuaAction(AllRule(), luarule)
  

DoH et DoT fonctionnent tous les deux sur TLS (RFC 8446) et utilisent donc son mécanisme d'authentification via des certificats PKIX (RFC 5280). Il faut donc obtenir des certificats raisonnablement reconnus par les clients donc, comme tout le monde, j'ai utilisé Let's Encrypt. dnsdist n'inclut pas de client ACME (RFC 8555), j'ai donc utilisé certbot que je fais tourner en mode autonome (standalone). Cela marche car dnsdist n'écoute que sur le port 443 alors qu'ACME n'utilise que le 80. Pour créer le certificat initial :

certbot certonly -n --standalone --domain doh.bortzmeyer.fr
  

Et pour le renouveler :

certbot renew --standalone --reuse-key --deploy-hook /usr/local/sbin/restart-dnsdist
  

(restart-dnsdist contient dnsdist -e 'reloadAllCertificates()'.) Et voici les liens symboliques configurés pour pointer vers les répertoires utilisés par Let's Encrypt :

% ls -lt                                                                        
total 28
lrwxrwxrwx  1 root root   51 Sep 24 15:50 server-dot.key -> /etc/letsencrypt/live/dot.bortzmeyer.fr/privkey.pem
lrwxrwxrwx  1 root root   53 Sep 24 15:50 server-dot.pem -> /etc/letsencrypt/live/dot.bortzmeyer.fr/fullchain.pem
lrwxrwxrwx  1 root root   51 Sep 15 16:31 server-doh.key -> /etc/letsencrypt/live/doh.bortzmeyer.fr/privkey.pem
lrwxrwxrwx  1 root root   53 Sep 15 16:31 server-doh.pem -> /etc/letsencrypt/live/doh.bortzmeyer.fr/fullchain.pem
...
  

Avec gnutls-cli, on peut voir le certificat utilisé :

% gnutls-cli dot.bortzmeyer.fr:853
...
- Certificate type: X.509
- Got a certificate list of 2 certificates.
- Certificate[0] info:
 - subject `CN=dot.bortzmeyer.fr', issuer `CN=Let's Encrypt Authority X3,O=Let's Encrypt,C=US', serial 0x047aa99cbff8ac180c4c3b14935d9599b1f5, RSA key 2048 bits, signed using RSA-SHA256, activated `2019-09-24 12:46:32 UTC', expires `2019-12-23 12:46:32 UTC', key-ID `sha256:787005b3173d1c95bc425241ea40e5474b644f00fded7fd35d87350331644c56'
	Public Key ID:
		sha1:696c94f0f093a3b1037ffcb2fcd9d23859d539bc
		sha256:787005b3173d1c95bc425241ea40e5474b644f00fded7fd35d87350331644c56
	Public key's random art:
		+--[ RSA 2048]----+
		|      o          |
		|       = o       |
		|    . . O     ...|
		|     o B +    .+.|
		|      = S    .  o|
		|       = .  .  E |
		|        . .=     |
		|       . o=o.    |
		|        o.oo.    |
		+-----------------+

- Certificate[1] info:
 - subject `CN=Let's Encrypt Authority X3,O=Let's Encrypt,C=US', issuer `CN=DST Root CA X3,O=Digital Signature Trust Co.', serial 0x0a0141420000015385736a0b85eca708, RSA key 2048 bits, signed using RSA-SHA256, activated `2016-03-17 16:40:46 UTC', expires `2021-03-17 16:40:46 UTC', key-ID `sha256:60b87575447dcba2a36b7d11ac09fb24a9db406fee12d2cc90180517616e8a18'
- Status: The certificate is trusted. 
- Description: (TLS1.2)-(ECDHE-RSA-SECP256R1)-(AES-256-GCM)
...
  

Pour pouvoir authentifier le serveur par d'autres moyens que la chaîne de confiance PKIX (par exemple par épinglage de la clé, ou par DANE), je demande à certbot de ne pas changer la clé lors des renouvellements de certificats, avec l'option --reuse-key (cf. mon précédent article sur la question).

À propos de DANE (RFC 6698), j'ai créé les enregistrements nécessaires avec hash-slinger :

% tlsa --create --selector 1  --port 853 dot.bortzmeyer.fr
Got a certificate with Subject: /CN=dot.bortzmeyer.fr
_853._tcp.dot.bortzmeyer.fr. IN TLSA 3 1 1 787005b3173d1c95bc425241ea40e5474b644f00fded7fd35d87350331644c56
  

Puis je les mets dans le fichier de zone DNS, et on peut voir les enregistrements avec dig :

    
% dig TLSA _853._tcp.dot.bortzmeyer.fr
...
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 15141
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 4, ADDITIONAL: 11
...
;; ANSWER SECTION:
_853._tcp.dot.bortzmeyer.fr. 86400 IN TLSA 3 1 1 (
				787005B3173D1C95BC425241EA40E5474B644F00FDED

  

Et on peut les vérifier :

  
% tlsa --verify --resolvconf="" doh.bortzmeyer.fr
SUCCESS (Usage 3 [DANE-EE]): Certificate offered by the server matches the TLSA record (193.70.85.11)
SUCCESS (Usage 3 [DANE-EE]): Certificate offered by the server matches the TLSA record (2001:41d0:302:2200::180)

Et superviser le tout depuis Icinga.

On a vu qu'il y avait deux résolveurs DNS derrière dnsdist, le principal, géré par moi, et un résolveur de secours. Le résolveur principal utilise Unbound. Pour mieux protéger la vie privée, il utilise la QNAME minimisation du RFC 9156. Dans le unbound.conf, on a  :

server:
  qname-minimisation: yes
  

Vous pouvez vérifier que c'est bien activé avec le domaine de test qnamemintest.internet.nl et l'outil test-doh présenté plus loin :

% test-doh https://doh.bortzmeyer.fr qnamemintest.internet.nl TXT
...
a.b.qnamemin-test.internet.nl. 10 IN TXT "HOORAY - QNAME minimisation is enabled on your resolver :)!"
  

Alors qu'avec un autre résolveur DoH, moins soucieux de vie privée :

% test-doh https://dns.google/dns-query qnamemintest.internet.nl TXT   
...
a.b.qnamemin-test.internet.nl. 9 IN TXT "NO - QNAME minimisation is NOT enabled on your resolver :("
  

J'ai aussi mis quelques autres paramètres Unbound, typiquement pour durcir la résolution face à diverses menaces :

  harden-glue: yes
  harden-below-nxdomain: yes
  harden-dnssec-stripped: yes
  harden-referral-path: yes
  aggressive-nsec: yes
  

Avec tout ça, je crois ne pas avoir dit comment j'avais installé dnsdist. Comme il n'existe pas de paquetage dnsdist dans Arch Linux (mais il existe dans AUR), j'ai téléchargé le source et compilé moi-même, selon les instructions. D'abord, j'ai téléchargé et installé la bibliothèque libh2o, qui permet à dnsdist de parler HTTP/2 (RFC 9113) :

cmake .
make
make install
  

Puis on peut installer dnsdist :

PKG_CONFIG_PATH=/usr/local/lib/pkgconfig  ./configure --enable-dns-over-tls --enable-dns-over-https
make
make install
  

Testons maintenant, d'abord avec kdig (qui fait partie de Knot, et notez la première ligne de la réponse) :

    
% kdig  +tls @dot.bortzmeyer.fr nextinpact.com
;; TLS session (TLS1.3)-(ECDHE-SECP256R1)-(RSA-PSS-RSAE-SHA256)-(AES-256-GCM)
;; ->>HEADER<<- opcode: QUERY; status: NOERROR; id: 56403
;; Flags: qr rd ra; QUERY: 1; ANSWER: 2; AUTHORITY: 0; ADDITIONAL: 1

;; EDNS PSEUDOSECTION:
;; Version: 0; flags: ; UDP size: 4096 B; ext-rcode: NOERROR

;; QUESTION SECTION:
;; nextinpact.com.     		IN	A

;; ANSWER SECTION:
nextinpact.com.     	118	IN	A	45.60.122.203
nextinpact.com.     	118	IN	A	45.60.132.203

;; Received 75 B
;; Time 2019-10-03 17:34:29 CEST
;; From 2001:41d0:302:2200::180@853(TCP) in 14.1 ms
  

Puis grâce aux sondes RIPE Atlas, le service. Les Atlas savent faire du DoT. Voyons d'abord en IPv6 (le choix par défaut) :

%  blaeu-resolve --dnssec --displayvalidation --displayrtt --tls --nameserver=dot.bortzmeyer.fr --nsid --sort --requested=1000   cyberstructure.fr  
Nameserver dot.bortzmeyer.fr
[ (Authentic Data flag)  2001:4b98:dc0:41:216:3eff:fe27:3d3f] : 956 occurrences Average RTT 1572 ms
[TIMEOUT] : 17 occurrences Average RTT 0 ms
[TUCONNECT (may be a TLS negotiation error)] : 14 occurrences Average RTT 0 ms
Test #22928943 done at 2019-09-30T09:37:15Z
  

Il y a quand même trop d'échecs. Ceci dit, il s'agit parfois de problèmes réseau et pas de problèmes DoT. Essayons ICMP Echo avec les mêmes sondes :

  
% blaeu-reach --old_measurement=22928943 --by_probe 2001:41d0:302:2200::180

493 probes reported
Test #22929082 done at 2019-09-30T09:48:25Z
Tests: 487 successful probes (98.8 %), 6 failed (1.2 %), average RTT: 72 ms

Donc, au moins une partie des problèmes n'est pas spécifique à DoT. Pour les autres cas d'échec, on peut aussi imaginer que certains réseaux ne laissent pas sortir les communications vers le port 853, qu'utilise DoT (c'est d'ailleurs une des principales raisons qui a motivé le développement de DoH qui, utilisant HTTPS, est plus difficile à bloquer). En attendant, essayons en IPv4 :

%  blaeu-resolve --dnssec --displayvalidation --displayrtt --tls --nameserver=dot.bortzmeyer.fr --nsid --sort --requested=1000  -4 cyberstructure.fr  
Nameserver dot.bortzmeyer.fr
[ (Authentic Data flag)  2001:4b98:dc0:41:216:3eff:fe27:3d3f] : 971 occurrences Average RTT 975 ms
[TIMEOUT] : 9 occurrences Average RTT 0 ms
[TUCONNECT (may be a TLS negotiation error)] : 6 occurrences Average RTT 0 ms
Test #22929087 done at 2019-09-30T09:51:59Z

% blaeu-reach --old_measurement=22929087  --by_probe  193.70.85.11
494 probes reported
Test #22929254 done at 2019-09-30T11:35:16Z
Tests: 494 successful probes (100.0 %), 0 failed (0.0 %), average RTT: 78 ms
  

C'est clair, on a moins d'erreurs. L'Internet est hélas moins fiable en IPv6 (surtout vu les difficultés de configuration d'IPv6 chez OVH).

J'ai utilisé plus haut le script test-doh pour tester le serveur. Ce script est écrit en Python et disponible en test-doh.py. Exemple d'utilisation :

% test-doh https://doh.bortzmeyer.fr netflix.com AAAA
id 0
opcode QUERY
rcode NOERROR
flags QR RD RA
;QUESTION
netflix.com. IN AAAA
;ANSWER
netflix.com. 60 IN AAAA 2a01:578:3::3431:7806
netflix.com. 60 IN AAAA 2a01:578:3::22fd:6807
netflix.com. 60 IN AAAA 2a01:578:3::36e5:444d
...
;AUTHORITY
;ADDITIONAL
  

On peut aussi utiliser doh-client.sh, qui appelle curl, et utilise deux programmes Python, dns-t2b.py et dns-b2t.py pour créer les messages DNS en entrée et les lire à la sortie (DoH n'utilise pas JSON ou XML, mais le format binaire du DNS.)

Le service est supervisé par Icinga. Pour DoT, je me sers d'un programme développé lors d'un hackathon IETF. (Il y a depuis une version plus propre dans getdns, le programme dans le paquetage Debian getdns-utils se nomme getdns_server_mon.) Lancé à la main, il donne :

 % /usr/local/lib/nagios/plugins/check_dns_with_getdns -H 2001:41d0:302:2200::180 -n nextinpact.com
GETDNS OK - 16 ms, expiration date 2019-12-23, auth. None:  Address 45.60.132.203 Address 45.60.122.203
  

Pour DoH, certains des tests sont les tests génériques des monitoring plugins, par exemple le test de l'expiration du certificat (pour ne pas être surpris si les renouvellements Let's Encrypt se sont mal passés). Cela se configure avec :

vars.http_vhosts["doh-http"] = {
    http_uri = "/"
    http_vhost = "doh.bortzmeyer.fr"
    http_ssl = true
    http_sni = true
    http_timeout = 15
    http_certificate = "4,2"
    }
  

Ainsi, je suis prévenu s'il reste moins de quatre jours au certificat (et une alarme est levée s'il ne reste que deux jours). Autre test générique, que le serveur renvoie bien un échec si on ne lui parle pas en DoH correctement :

vars.http_vhosts["doh-raw-http"] = {
    http_uri = "/"
    http_vhost = "doh.bortzmeyer.fr"
    http_ssl = true
    http_sni = true
    http_timeout = 15
    http_expect = 400
    http_string = "Unable to parse the request"
    }
  

Mais il faut évidemment tester que le serveur répond bien aux requêtes DoH. On utilise pour cela un script dérivé du test-doh cité plus haut, check_doh.py. Utilisé à la main, ça donne :

% /usr/local/lib/nagios/plugins/check_doh -H doh.bortzmeyer.fr -n nextinpact.com
https://doh.bortzmeyer.fr/ OK - No error for nextinpact.com/AAAA, 107 bytes received
   

Et pour l'intégrer dans Icinga, on déclare une commande dans commands.conf :

object CheckCommand "doh_monitor" {                                               
  command = [ PluginContribDir + "/check_doh" ]                                          
                                                                 
  arguments = {                                                                
          "-H" = "$address6$",
          "-n" = "$doh_lookup$",                                    
          "-p" = "$doh_path$",                                   
          "-V" = "$doh_vhost$",                                         
          "-t" = "$doh_type$",                                        
          "-p" = "$doh_post$",                                                   
          "-i" = "$doh_insecure$",                                       
          "-h" = "$doh_head$"                                                  
          }                                                        
}                                                                                     
  

Puis un service dans services.conf :

Apply Service "doh" {                                                                   
  import "generic-service"                                                     
                                                                                 
  check_command = "doh_monitor"                                                                   
    assign where (host.address || host.address6) && host.vars.doh                                        
}                                                                                          
  

On peut alors configurer la machine à superviser :

     vars.doh = true
     vars.doh_vhost = "doh.bortzmeyer.fr"
     vars.doh_lookup = "fr.wikipedia.org"
     vars.doh_post = true
   

On supervise également le serveur Web internet :

vars.http_vhosts["doh-admin"] = {
    http_uri = "/api/v1/servers/localhost"
    http_port =	8083
    http_vhost = "doh.bortzmeyer.fr"
    http_ssl = true
    http_sni = true
    http_timeout = 15
    http_header = "X-API-Key: 23b..."
    http_string = "dohFrontends"
    }
}
   

Ce service est également accessible via un .onion, le lani4a4fr33kqqjeiy3qubhfx2jewfd3aeaepuwzxrx6zywp2mo4cjad.onion. Testons avec kdig et le wrapper torsocks :

   
% torsocks kdig +tls @lani4a4fr33kqqjeiy3qubhfx2jewfd3aeaepuwzxrx6zywp2mo4cjad.onion A toto.fr

;; TLS session (TLS1.3)-(ECDHE-SECP256R1)-(RSA-PSS-RSAE-SHA256)-(AES-256-GCM)
;; ->>HEADER<<- opcode: QUERY; status: NOERROR; id: 54471
;; Flags: qr rd ra; QUERY: 1; ANSWER: 1; AUTHORITY: 0; ADDITIONAL: 1

;; EDNS PSEUDOSECTION:
;; Version: 0; flags: ; UDP size: 1232 B; ext-rcode: NOERROR
;; PADDING: 412 B

;; QUESTION SECTION:
;; toto.fr.            		IN	A

;; ANSWER SECTION:
toto.fr.            	83659	IN	A	164.132.161.100

;; Received 468 B
;; Time 2021-09-28 18:40:47 UTC
;; From 127.42.42.0@853(TCP) in 328.9 ms

 

Et en DoH :

   
%  torsocks  kdig  +https=/  @lani4a4fr33kqqjeiy3qubhfx2jewfd3aeaepuwzxrx6zywp2mo4cjad.onion  A toto.fr

;; TLS session (TLS1.3)-(ECDHE-SECP256R1)-(RSA-PSS-RSAE-SHA256)-(AES-256-GCM)
;; HTTP session (HTTP/2-POST)-(lani4a4fr33kqqjeiy3qubhfx2jewfd3aeaepuwzxrx6zywp2mo4cjad.onion/)-(status: 200)
;; ->>HEADER<<- opcode: QUERY; status: NOERROR; id: 0
;; Flags: qr rd ra; QUERY: 1; ANSWER: 1; AUTHORITY: 0; ADDITIONAL: 1

;; EDNS PSEUDOSECTION:
;; Version: 0; flags: ; UDP size: 1232 B; ext-rcode: NOERROR
;; PADDING: 412 B

;; QUESTION SECTION:
;; toto.fr.            		IN	A

;; ANSWER SECTION:
toto.fr.            	83626	IN	A	164.132.161.100

;; Received 468 B
;; Time 2021-09-28 18:41:21 UTC
;; From 127.42.42.0@443(TCP) in 415.7 ms

 

La configuration a été triviale, il suffit de mettre dans le torrc :

HiddenServiceDir /var/lib/tor/dns/
# DoH
HiddenServicePort 443 [2001:db8::fada]:443
# DoT
HiddenServicePort 853 [2001:db8::fada]:853

Il existe d'autres résolveurs DoH gérés par des associations ou des individus, comme celui de Shaft (avec une documentation détaillée sur la configuration du client et sur celle du serveur), celui du .cz, https://www.nic.cz/odvr/ ou comme celui de l'association 42l. Mais je n'en trouve pas (pour l'instant) qui ait documenté en détail leur configuration technique.

Sinon, si vous aimez lire, il y a les très bons supports de l'exposé de Shaft à Pas Sage En Seine (plein de DoT et un peu de DoH.)

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)