Un paquet sur les bosses dans une forêt lointaine pour DNS ...
L. Kaganov "Hamlet au fond"
Lors du développement d'une application réseau, il devient parfois nécessaire de l'exécuter localement, mais d'y accéder en utilisant un vrai nom de domaine. La solution standard éprouvée consiste à enregistrer le domaine dans le fichier hosts. L'inconvénient de l'approche est que les hôtes nécessitent une correspondance claire des noms de domaine, c'est-à-dire ne prend pas en charge les étoiles. C'est-à-dire s'il existe des domaines du formulaire:
dom1.example.com, dom2.example.com, dom3.example.com, ................ domN.example.com,
puis dans les hôtes, vous devez tous les enregistrer. Dans certains cas, le domaine de troisième niveau n'est pas connu à l'avance. Il y a un désir (j'écris pour moi, quelqu'un pourrait dire que c'est normal) de s'en sortir avec une ligne comme celle-ci:
*.example.com
La solution au problème peut être d'utiliser votre propre serveur DNS, qui traitera les demandes conformément à la logique spécifiée. Il existe de tels serveurs, à la fois entièrement gratuits et avec une interface graphique pratique, comme CoreDNS . Vous pouvez également modifier les enregistrements DNS sur le routeur. Enfin, utilisez un service comme xip.io , ce n'est pas tout à fait un serveur DNS à part entière, mais il est parfait pour certaines tâches. Bref, des solutions toutes faites existent, vous pouvez les utiliser et ne pas vous embêter.
Mais cet article décrit une autre façon - écrire votre propre vélo, le point de départ pour créer un outil comme ceux énumérés ci-dessus. Nous écrirons notre proxy DNS, qui écoutera les requêtes DNS entrantes, et si le nom de domaine demandé est dans la liste, il renverra l'IP spécifiée, et sinon, il demandera un serveur DNS supérieur et transmettra la réponse reçue sans modifications au programme demandeur.
Dans le même temps, vous pouvez enregistrer les demandes et les réponses reçues. Étant donné que le DNS est nécessaire à tout le monde - navigateurs, messagers et antivirus, et services de système d'exploitation, etc., il peut être très informatif.
Le principe est simple. Dans les paramètres de connexion réseau pour IPv4, nous changeons l'adresse du serveur DNS en l'adresse de la machine avec notre proxy DNS auto-écrit en cours d'exécution (127.0.0.1, si nous ne travaillons pas sur le réseau), et dans ses paramètres, nous spécifions l'adresse du serveur DNS supérieur. Et, semble-t-il, c'est tout!
Nous n'utiliserons pas les fonctions standard pour résoudre les noms de domaine nslookup et nsresolve , donc les paramètres du système DNS et le contenu du fichier hosts n'affecteront pas le fonctionnement du programme. Selon la situation, cela peut être utile ou non, il suffit de s'en souvenir. Par souci de simplicité, nous nous limitons à l'implémentation de la fonctionnalité de base elle-même:
- Usurpation d'adresse IP uniquement pour les enregistrements de type A (adresse d'hôte) et de classe IN (Internet)
- adresses IP usurpées uniquement version 4
- connexion pour les demandes entrantes locales sur UDP uniquement
- connexion au serveur DNS en amont via UDP ou TLS
- s'il y a plusieurs interfaces réseau, les demandes locales entrantes seront acceptées sur l'une d'entre elles
- pas de support EDNS
En parlant de testsIl y a peu de tests unitaires dans le projet. Certes, ils fonctionnent selon le principe: je l'ai lancé, et si quelque chose de sain est affiché dans la console, alors tout va bien, mais si une exception vole, alors il y a un problème. Mais même une approche aussi maladroite vous permet de localiser avec succès le problème, alors Unit.
Démarrer - serveur sur le port 53
Commençons. Tout d'abord, vous devez apprendre à l'application à accepter les requêtes DNS entrantes. Nous écrivons un simple serveur TCP qui écoute simplement le port 53 et enregistre les connexions entrantes. Dans les propriétés de la connexion réseau, nous écrivons l'adresse du serveur DNS 127.0.0.1, lançons l'application, allons sur le navigateur pour plusieurs pages - et ... silence dans la console, le navigateur affiche la page normalement. Eh bien, nous changeons TCP en UDP, nous commençons, nous passons par le navigateur - dans le navigateur il y a une erreur de connexion, des données binaires versées dans la console. Ainsi, le système envoie des requêtes via UDP, et nous écouterons les connexions entrantes via UDP sur le port 53. Une demi-heure de travail, dont 15 minutes pour savoir comment élever un serveur TCP et UDP sur NodeJS - et nous avons résolu la tâche fondamentale du projet, qui détermine la structure de la future application. Le code est le suivant:
const dgram = require('dgram'); const server = dgram.createSocket('udp4'); (function() { server.on('error', (err) => { console.log(`server error:\n${err.stack}`); server.close(); }); server.on('message', async (localReq, linfo) => { console.log(localReq);
Listing 1. Le code minimum nécessaire pour recevoir des requêtes DNS locales
Le point suivant est de lire le message afin de comprendre s'il est nécessaire de renvoyer notre IP en réponse, ou simplement de le transmettre.
Message DNS
La structure du message DNS est décrite dans la RFC-1035. Les demandes et les réponses suivent cette structure et, en principe, diffèrent par un indicateur de bit (champ QR) dans l'en-tête du message. Le message comprend cinq sections:
+---------------------+ | Header | +---------------------+ | Question | the question for the name server +---------------------+ | Answer | RRs answering the question +---------------------+ | Authority | RRs pointing toward an authority +---------------------+ | Additional | RRs holding additional information +---------------------+
Structure (s) générale (s) des messages DNS https://tools.ietf.org/html/rfc1035#section-4.1
Un message DNS commence par un en-tête de longueur fixe (il s'agit de la section dite en- tête ), qui contient des champs de 1 bit à deux octets (ainsi, un octet dans l'en-tête peut contenir plusieurs champs). L'en-tête commence par le champ ID - il s'agit de l'identifiant de requête 16 bits, la réponse doit avoir le même ID. Les champs suivants décrivent le type de demande, le résultat de son exécution et le nombre d'enregistrements dans chacune des sections suivantes du message. Décrivez-les tous pendant longtemps, alors peu importe - bien dans la RFC: https://tools.ietf.org/html/rfc1035#section-4.1.1 . La section En - tête est toujours présente dans le message DNS.
1 1 1 1 1 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ID | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |QR| Opcode |AA|TC|RD|RA| Z | RCODE | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | QDCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ANCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | NSCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ARCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
Structure (s) d'en-tête de message DNS https://tools.ietf.org/html/rfc1035#section-4.1.1
Section des questions
La section Question contient une entrée indiquant au serveur exactement quelles informations sont nécessaires. Théoriquement, dans la section de ces enregistrements, il peut y en avoir un ou plusieurs, leur nombre est indiqué dans le champ QDCOUNT de l'en-tête du message et peut être 0, 1 ou plus. Mais en pratique, la section Question ne peut contenir qu'une seule entrée. Si la section Question contenait plusieurs enregistrements et que l'un d'entre eux entraînait une erreur lors du traitement de la demande sur le serveur, une situation indéfinie se produirait. Bien que le serveur renvoie un code d'erreur dans le champ RCODE du message de réponse, il ne pourra pas indiquer lors du traitement de l'enregistrement de l'incident, la spécification ne le décrit pas. Les enregistrements n'ont également aucun champ contenant une indication de l'erreur et de son type. Par conséquent, il existe un accord (non documenté), selon lequel la section Question ne peut contenir qu'un seul enregistrement et le champ QDCOUNT a une valeur de 1. Il n'est pas non plus entièrement clair comment traiter la demande côté serveur, si elle contient toujours plusieurs enregistrements dans Question . Quelqu'un conseille de renvoyer un message avec une erreur de demande. Et, par exemple, Google DNS ne traite que le premier enregistrement de la section Question , il ignore simplement le reste. Apparemment, cela reste à la discrétion des développeurs de services DNS.
Dans le message DNS de réponse du serveur, la section Question est également présente et doit copier complètement la question de la demande (afin d'éviter les conflits, au cas où un champ ID ne suffirait pas).
La seule entrée dans la section Question contient les champs: QNAME (nom de domaine), QTYPE (type), QCLASS (classe). QTYPE et QCLASS sont des nombres à deux octets indiquant le type et la classe de la demande. Les types et classes possibles sont décrits dans la RFC-1035 https://tools.ietf.org/html/rfc1035#section-3.2 , tout y est clair. Mais sur la méthode d'enregistrement d'un nom de domaine, nous nous attarderons plus en détail dans la section "Format d'enregistrement des noms de domaine".
Dans le cas d'une requête, le message DNS se termine le plus souvent par la section Question , parfois la section supplémentaire peut le suivre.
Si une erreur s'est produite lors du traitement de la demande sur le serveur (par exemple, une demande entrante a été mal formée), le message de réponse se terminera également par la section Question ou supplémentaire , et le champ RCODE de l'en-tête du message de réponse contiendra un code d'erreur.
Réponse , autorisation et sections supplémentaires
Les sections suivantes sont Réponse , Autorité et Supplémentaire (La réponse et l' Autorité sont contenues uniquement dans le message DNS de réponse, des informations supplémentaires peuvent apparaître dans la demande et dans la réponse). Ils sont facultatifs, c'est-à-dire l'un d'eux peut être présent ou non, selon la demande. Ces sections ont la même structure et contiennent des informations au format des «enregistrements de ressources» (enregistrement de ressource, ou RR). Au sens figuré, chacune de ces sections est un tableau d'enregistrements de ressources, et un enregistrement est un objet avec des champs. Chaque section peut contenir un ou plusieurs enregistrements, leur numéro est indiqué dans le champ correspondant de l'en-tête du message (ANCOUNT, NSCOUNT, ARCOUNT, respectivement). Par exemple, une demande IP pour le domaine "google.com" renverra plusieurs adresses IP, il y aura donc également plusieurs entrées dans la section Réponse , une pour chaque adresse. Si la section est absente, le champ d'en-tête correspondant contient 0.
Chaque enregistrement de ressource (RR) commence par un champ NAME contenant un nom de domaine. Le format de ce champ est le même que le champ QNAME de la section Question .
À côté de NAME se trouvent les champs TYPE (type d'enregistrement) et CLASS (sa classe), les deux champs sont numériques sur 16 bits, indiquent le type et la classe de l'enregistrement. Cela ressemble également à la section Question , à la différence que ses QTYPE et QCLASS peuvent avoir toutes les mêmes valeurs que TYPE et CLASS, et certaines autres qui leur sont propres. Autrement dit, dans un langage scientifique sec, l'ensemble des valeurs QTYPE et QCLASS est un sur-ensemble des valeurs TYPE et CLASS. En savoir plus sur les différences sur https://tools.ietf.org/html/rfc1035#section-3.2.2 .
Les champs restants sont:
- TTL est un nombre de 32 bits indiquant la dernière fois que l'enregistrement a été (en secondes).
- RDLENGTH est un nombre de 16 bits qui indique la longueur du champ RDATA suivant en octets.
- RDATA est en fait une charge utile, le format dépend du type d'enregistrement. Par exemple, pour un enregistrement de type A (adresse d'hôte) et de classe IN (Internet), ce sont 4 octets représentant une adresse IPv4.
Le format d'enregistrement des noms de domaine est le même pour les champs QNAME et NAME, ainsi que pour le champ RDATA, s'il s'agit d'un enregistrement CNAME, MX, NS ou autre enregistrement de classe qui suppose un nom de domaine comme résultat.
Un nom de domaine est une séquence d'étiquettes (sections d'un nom, sous-domaines - c'est une étiquette dans l'original, je n'ai pas trouvé de meilleure traduction). Une étiquette est un seul octet de longueur contenant un nombre - la longueur du contenu de l'étiquette en octets, suivie d'une séquence d'octets de la longueur spécifiée. Les étiquettes se succèdent jusqu'à ce qu'un octet de longueur contenant 0 soit rencontré. La toute première étiquette peut être immédiatement de longueur nulle, cela indique le domaine racine (domaine racine) avec un nom de domaine vide (parfois écrit "").
Dans les versions antérieures de DNS, les octets de l'étiquette pouvaient avoir n'importe quelle valeur de (0 à 255). Il y avait des règles qui étaient de la nature d'une recommandation urgente: que le libellé commence par une lettre, se termine par une lettre ou un chiffre et ne contienne que des lettres, des chiffres ou des tirets en codage ASCII 7 bits, avec le bit zéro le plus significatif. La spécification EDNS actuelle exige déjà le respect de ces règles clairement, sans déviation.
Les deux bits les plus significatifs de l'octet de longueur sont utilisés comme attribut de type d'étiquette. S'ils sont nuls ( 0b00xxxxxx ), il s'agit alors d'une étiquette normale et les bits restants de l'octet de longueur indiquent le nombre d'octets de données inclus dans sa composition. La longueur maximale de l'étiquette est de 63 caractères. 63 en codage binaire est juste 0b00111111 .
Si les deux bits de poids fort sont respectivement 0 et 1 ( 0b01xxxxxx ), il s'agit d'une étiquette de type étendue de la norme EDNS ( https://tools.ietf.org/html/rfc2671#section-3.1 ), qui nous est parvenue le 1er février 2019. Les six bits inférieurs contiendront la valeur de l'étiquette. Nous ne discutons pas d'EDNS dans cet article, mais il est utile de savoir que cela se produit également.
La combinaison des deux bits les plus significatifs, égaux à 1 et 0 ( 0b10xxxxxx ), est réservée pour une utilisation future.
Si les deux bits hauts sont égaux à 1 ( 0b11xxxxxx ), cela signifie que les noms de domaine sont compressés ( compression ), et nous nous attarderons sur cela plus en détail.
Compression des noms de domaine
Donc, si un octet de longueur a deux bits hauts égaux à 1 ( 0b11xxxxxx ), c'est un signe de compression de nom de domaine. La compression est utilisée pour rendre les messages plus courts et plus concis. Cela est particulièrement vrai lorsque vous travaillez sur UDP, lorsque la longueur totale du message DNS est limitée à 512 octets (bien qu'il s'agisse de l'ancienne norme, voir https://tools.ietf.org/html/rfc1035#section-2.3.4 Limites de taille , le nouveau EDNS permet d'envoyer des messages UPD et plus). L'essence du processus est que si un message DNS contient des noms de domaine avec les mêmes sous-domaines de premier niveau (par exemple, mail.yandex.ru et yandex.ru ), au lieu de re-spécifier le nom de domaine entier, le numéro d'octet dans le message DNS à partir duquel Continuez à lire le nom de domaine. Il peut s'agir de n'importe quel octet du message DNS, non seulement dans l'enregistrement ou la section en cours, mais à condition qu'il s'agisse d'un octet de la longueur de l'étiquette de domaine. Vous ne pouvez pas vous référer au milieu de la marque. Supposons qu'il y ait un domaine mail.yandex.ru dans le message, puis à l'aide de la compression, il est possible de désigner également les domaines yandex.ru , ru et root "" (bien sûr, la racine est plus facile à écrire sans compression, mais il est techniquement possible de le faire avec la compression), et ici pour faire ndex.ru ne fonctionnera pas. De plus, tous les noms de domaine dérivés se termineront dans le domaine racine, c'est-à-dire que l'écriture, disons, mail.yandex échouera également.
Un nom de domaine peut:
- être entièrement enregistré sans compression,
- partir d'un endroit qui utilise la compression
- commencez par une ou plusieurs étiquettes sans compression, puis passez en compression,
- être vide (pour le domaine racine).
Par exemple, nous compilons un message DNS, et nous y avions déjà rencontré le nom "dom3.example.com", nous devons maintenant spécifier "dom4.dom3.example.com". Dans ce cas, vous pouvez enregistrer la section "dom4" sans compression, puis basculer vers la compression, c'est-à-dire ajouter un lien vers "dom3.example.com". Ou vice versa, si le nom "dom4.dom3.example.com" a déjà été rencontré, pour indiquer "dom3.example.com", vous pouvez immédiatement utiliser la compression en vous référant à l'étiquette "dom3". Ce que nous ne pouvons pas faire est, comme cela a déjà été dit, d'indiquer la partie de "dom4.dom3" par compression, car le nom doit se terminer par une section de niveau supérieur. Si vous devez soudainement spécifier des segments à partir du milieu, ils sont simplement indiqués sans compression.
Par souci de simplicité, notre programme ne sait pas écrire les noms de domaine avec compression, il ne peut que lire. La norme le permet, la lecture doit être implémentée nécessairement, l'écriture est facultative. Techniquement, la lecture est implémentée comme ceci: si les deux bits les plus significatifs d'un octet de longueur contiennent 1, alors nous lisons l'octet qui le suit et traitons ces deux octets comme un entier non signé de 16 bits, avec l'ordre des bits Big Endian. Nous supprimons les deux bits les plus significatifs (contenant 1), lisons le nombre de 14 bits résultant et continuons à lire le nom de domaine de l'octet dans le message DNS sous le numéro correspondant à ce numéro.
Le code de la fonction de lecture de nom de domaine est le suivant:
function readDomainName (buf, startOffset, objReturnValue = {}) { let currentByteIndex = startOffset;
Listing 2. Lecture de noms de domaine à partir d'une requête DNS
Code complet de la fonction pour lire l'enregistrement DNS à partir du tampon binaire:
Listing 3. Lecture d'un enregistrement DNS à partir d'un tampon binaire function parseDnsMessageBytes (buf) { const msgFields = {};
Listing 3. Lecture d'un enregistrement DNS à partir d'un tampon binaire
, . , , , . , DNS-, , . , .
, - server.on("message", () => {})
1. :
4. DNS- server.on('message', async (localReq, linfo) => { const dnsRequest = functions.parseDnsMessageBytes(localReq); const question = dnsRequest.questions[0];
Listing 4. Traitement d'une requête DNS locale entrante
TLS
DNS-. , DNS- TLS (HTTPS ). DNS- TLS TCP, , TLS . TCP, RFC-7766 DNS Transport over TCP ( https://tools.ietf.org/html/rfc7766 ). , : TLS, TCP ( , DNS TCP, TLS- TCP-, ).
TLS-
TLS- , , . , TLS-, . RFC-7858 - :
In order to amortize TCP and TLS connection setup costs, clients and servers SHOULD NOT immediately close a connection after each response. Instead, clients and servers SHOULD reuse existing connections for subsequent queries as long as they have sufficient resources. In some cases, this means that clients and servers may need to keep idle connections open for some amount of time. () https://tools.ietf.org/html/rfc7858#section-3.4
, TLS-, , , , , . , 30 , , , DNS-. 30 ~ ~ , 15 60 , . , . - .
TLS- NodeJS. , TLS- :
const tls = require('tls'); const TLS_SOCKET_IDLE_TIMEOUT = 30000;
5. , TLS-
DNS-over-TLS , Google DNS. , socket = tls.connect(connectionOptions, () => {})
. NodeJS: https://nodejs.org/api/tls.html#tls_tls_connect_options_callback , .
TLS- :
const options = { port: config.upstreamDnsTlsPort,
6. TLS-
, TCP-. TCP/TLS- DNS-, , , , . TCP ( TLS), DNS- 512 , UDP (, EDNS UDP ). , DNS- UDP, . onData() 6.
const onData = (data) => {
7. TLS- DNS- 6
DNS-
, , . , ID QNAME, QTYPE QCLASS Question :
Since pipelined responses can arrive out of order, clients MUST match responses to outstanding queries on the same TLS connection using the Message ID. If the response contains a Question Section, the client MUST match the QNAME, QCLASS, and QTYPE fields. () https://tools.ietf.org/html/rfc7858#section-3.3
, , , ID Question ( , ).
UDP (. 4), , -, , UDP- . , DNS-, . , -. , , UDP- -. , , .
TLS, . (IP ), , .
IP "-". , , , DNS-. , , IP , . 7:
8. 7
TLS-:
9. DNS- TLS- ( . 4)
, , . JSON, , NodeJS JSON- . JSON — , . , JSON- "comment" ( ) . , , , , . , , . , - , , NodeJS. , , . , , ; , . , - .
10. const path = require('path'); const fs = require('fs'); const CONFIG_FILE_PATH = path.resolve('./config.json'); function Module () {
Listing 10. Lecteur et module de mise à jour de la configuration
Total
DNS- NodeJS, npm . , , , , .
GitHub
: