Ein Paket über die Unebenheiten in einem fernen Wald für DNS ...
L. Kaganov "Weiler unten"
Bei der Entwicklung einer Netzwerkanwendung muss sie manchmal lokal ausgeführt werden, aber mit einem echten Domänennamen darauf zugegriffen werden. Die bewährte Standardlösung besteht darin, die Domain in der Hosts-Datei zu registrieren. Das Minus des Ansatzes besteht darin, dass Hosts eine eindeutige Entsprechung von Domänennamen erfordern, d. H. unterstützt keine Sterne. Das heißt, Wenn es Domänen des Formulars gibt:
dom1.example.com, dom2.example.com, dom3.example.com, ................ domN.example.com,
Dann müssen Sie in Hosts alle registrieren. In einigen Fällen ist die Domäne der dritten Ebene nicht im Voraus bekannt. Es besteht der Wunsch (ich schreibe für mich selbst, jemand könnte sagen, dass es normal ist), mit einer Zeile wie dieser auszukommen:
*.example.com
Die Lösung des Problems kann darin bestehen, einen eigenen DNS-Server zu verwenden, der Anforderungen gemäß der angegebenen Logik verarbeitet. Es gibt solche Server, sowohl völlig kostenlos als auch mit einer praktischen grafischen Oberfläche wie CoreDNS . Sie können auch die DNS-Einträge auf dem Router ändern. Verwenden Sie einen Dienst wie xip.io. Es handelt sich nicht um einen vollwertigen DNS-Server, aber er eignet sich perfekt für einige Aufgaben. Kurz gesagt, es gibt vorgefertigte Lösungen, die Sie verwenden können und die Sie nicht stören müssen.
Dieser Artikel beschreibt jedoch einen anderen Weg - das Schreiben Ihres eigenen Fahrrads, den Ausgangspunkt für die Erstellung eines Werkzeugs wie das oben aufgeführte. Wir schreiben unseren DNS-Proxy, der eingehende DNS-Abfragen abhört. Wenn der angeforderte Domänenname in der Liste enthalten ist, gibt er die angegebene IP zurück. Wenn nicht, fordert er einen höheren DNS-Server an und leitet die empfangene Antwort ohne Änderungen an das anfordernde Programm weiter.
Gleichzeitig können Sie Anforderungen und die darauf eingegangenen Antworten protokollieren. Da DNS von allen benötigt wird - von Browsern, Messenger und Antivirenprogrammen sowie von Betriebssystemdiensten usw. - kann es sehr informativ sein.
Das Prinzip ist einfach. In den Netzwerkverbindungseinstellungen für IPv4 ändern wir die DNS-Serveradresse in die Adresse des Computers mit unserem selbst geschriebenen DNS-Proxy (127.0.0.1, wenn wir nicht über das Netzwerk arbeiten) und geben in den Einstellungen die Adresse des höheren DNS-Servers an. Und das scheint alles zu sein!
Wir werden die Standardfunktionen zum Auflösen von nslookup- und nsresolve- Domänennamen nicht verwenden, daher wirken sich die DNS-Systemeinstellungen und der Inhalt der Hosts-Datei nicht auf den Betrieb des Programms aus. Je nach Situation kann es nützlich sein oder nicht, Sie müssen sich nur daran erinnern. Der Einfachheit halber beschränken wir uns auf die Implementierung der Grundfunktionalität selbst:
- IP-Spoofing nur für Datensätze vom Typ A (Hostadresse) und Klasse IN (Internet)
- gefälschte IP-Adressen nur Version 4
- Verbindung nur für lokale eingehende Anforderungen über UDP
- Verbindung zum Upstream-DNS-Server über UDP oder TLS
- Wenn mehrere Netzwerkschnittstellen vorhanden sind, werden eingehende lokale Anforderungen für jede dieser Schnittstellen akzeptiert
- Keine EDNS-Unterstützung
Apropos TestsEs gibt nur wenige Unit-Tests im Projekt. Sie funktionieren zwar nach dem Prinzip: Ich habe es gestartet, und wenn in der Konsole etwas Vernünftiges angezeigt wird, ist alles in Ordnung, aber wenn eine Ausnahme auftritt, liegt ein Problem vor. Aber selbst solch ein ungeschickter Ansatz ermöglicht es Ihnen, das Problem erfolgreich zu lokalisieren, so Unit.
Start - Server an Port 53
Fangen wir an. Zunächst müssen Sie der Anwendung beibringen, eingehende DNS-Abfragen zu akzeptieren. Wir schreiben einen einfachen TCP-Server, der nur Port 53 abhört und eingehende Verbindungen protokolliert. In den Eigenschaften der Netzwerkverbindung schreiben wir die Adresse des DNS-Servers 127.0.0.1, starten die Anwendung, gehen für mehrere Seiten zum Browser - und ... Schweigen in der Konsole, der Browser zeigt die Seite normal an. Nun, wir ändern TCP in UDP, wir starten, wir gehen durch den Browser - im Browser gibt es einen Verbindungsfehler, einige Binärdaten werden in die Konsole gegossen. Das System sendet also Anforderungen über UDP, und wir werden eingehende Verbindungen über UDP an Port 53 abhören. Eine halbe Stunde Arbeit, davon 15 Minuten Google, um einen TCP- und UDP-Server auf NodeJS zu erstellen - und wir haben die Eckpfeileraufgabe des Projekts gelöst, die die Struktur der zukünftigen Anwendung bestimmt. Der Code lautet wie folgt:
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. Der Mindestcode für den Empfang lokaler DNS-Abfragen
Der nächste Punkt ist, die Nachricht zu lesen, um zu verstehen, ob es notwendig ist, unsere IP als Antwort darauf zurückzugeben oder sie einfach weiterzuleiten.
DNS-Nachricht
Die Struktur der DNS-Nachricht ist in RFC-1035 beschrieben. Sowohl Anforderungen als auch Antworten folgen dieser Struktur und unterscheiden sich im Prinzip in einem Bit-Flag (QR-Feld) im Nachrichtenkopf. Die Nachricht enthält fünf Abschnitte:
+---------------------+ | Header | +---------------------+ | Question | the question for the name server +---------------------+ | Answer | RRs answering the question +---------------------+ | Authority | RRs pointing toward an authority +---------------------+ | Additional | RRs holding additional information +---------------------+
Allgemeine DNS-Nachrichtenstruktur (en) https://tools.ietf.org/html/rfc1035#section-4.1
Eine DNS-Nachricht beginnt mit einem Header fester Länge (dies ist der sogenannte Header- Abschnitt), der Felder mit einer Länge von 1 Bit bis zwei Bytes enthält (daher kann ein Byte im Header mehrere Felder enthalten). Der Header beginnt mit dem ID-Feld - dies ist die 16-Bit-Anforderungskennung, die Antwort muss dieselbe ID haben. Als nächstes folgen die Felder, die den Anfragetyp, das Ergebnis ihrer Ausführung und die Anzahl der Datensätze in jedem der nachfolgenden Abschnitte der Nachricht beschreiben. Beschreiben Sie sie alle für eine lange Zeit, also wen interessiert das - gut im RFC: https://tools.ietf.org/html/rfc1035#section-4.1.1 . Der Header- Abschnitt ist in der DNS-Nachricht immer vorhanden.
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 | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
DNS-Nachrichtenkopfstruktur (en) https://tools.ietf.org/html/rfc1035#section-4.1.1
Fragenbereich
Der Abschnitt Frage enthält einen Eintrag, der dem Server genau mitteilt, welche Informationen von ihm benötigt werden. Theoretisch kann es im Abschnitt solcher Datensätze einen oder mehrere geben, deren Nummer im Feld QDCOUNT im Nachrichtenkopf angegeben ist und 0, 1 oder mehr sein kann. In der Praxis kann der Abschnitt Fragen jedoch nur einen Eintrag enthalten. Wenn der Abschnitt " Frage " mehrere Datensätze enthält und einer davon bei der Verarbeitung der Anforderung auf dem Server zu einem Fehler führen würde, würde eine undefinierte Situation auftreten. Obwohl der Server einen Fehlercode im RCODE-Feld in der Antwortnachricht zurückgibt, kann er bei der Verarbeitung nicht angeben, welcher Datensatz das Problem aufgetreten ist. Die Spezifikation beschreibt dies jedoch nicht. Datensätze enthalten auch keine Felder, die einen Hinweis auf den Fehler und seinen Typ enthalten. Daher gibt es eine Vereinbarung (ohne Papiere), wonach der Abschnitt " Frage " nur einen Datensatz enthalten kann und das Feld "QDCOUNT" den Wert 1 hat. Es ist auch nicht ganz klar, wie die Anforderung auf der Serverseite verarbeitet werden soll, wenn sie noch mehrere Datensätze in Frage enthält . Jemand rät, eine Nachricht mit einem Anforderungsfehler zurückzugeben. Beispielsweise verarbeitet Google DNS nur den ersten Eintrag im Abschnitt " Frage " und ignoriert den Rest einfach. Dies liegt offenbar im Ermessen der Entwickler von DNS-Diensten.
In der Antwort-DNS-Nachricht vom Server ist auch der Abschnitt Frage vorhanden und sollte die Frage der Anfrage vollständig kopieren (um Konflikte zu vermeiden, falls ein ID-Feld nicht ausreicht).
Der einzige Eintrag im Abschnitt Frage enthält die Felder: QNAME (Domänenname), QTYPE (Typ), QCLASS (Klasse). QTYPE und QCLASS sind Doppelbyte-Nummern, die den Typ und die Klasse der Anforderung angeben. Mögliche Typen und Klassen sind in RFC-1035 https://tools.ietf.org/html/rfc1035#section-3.2 beschrieben , dort ist alles klar. Auf die Methode zum Aufzeichnen eines Domainnamens werden wir jedoch im Abschnitt "Format zum Aufzeichnen von Domainnamen" näher eingehen.
Im Falle einer Abfrage endet die DNS-Nachricht meistens mit dem Abschnitt " Frage ", manchmal folgt der Abschnitt " Zusätzliche" .
Wenn während der Verarbeitung der Anforderung auf dem Server ein Fehler aufgetreten ist (z. B. wurde eine eingehende Anforderung falsch gebildet), endet die Antwortnachricht auch mit dem Abschnitt Frage oder Zusatz , und das RCODE-Feld des Antwortnachrichtenkopfs enthält einen Fehlercode.
Antwort- , Berechtigungs- und zusätzliche Abschnitte
Die folgenden Abschnitte sind Antwort , Berechtigung und Zusätzliche ( Antwort und Berechtigung sind nur in der Antwort-DNS-Nachricht enthalten. Zusätzliche können in der Anforderung und in der Antwort enthalten sein). Sie sind optional, d.h. Je nach Anforderung kann einer von ihnen vorhanden sein oder nicht. Diese Abschnitte haben dieselbe Struktur und enthalten Informationen im Format der sogenannten "Ressourceneinträge" ( Resourse Record oder RR). Im übertragenen Sinne ist jeder dieser Abschnitte ein Array von Ressourceneinträgen, und ein Datensatz ist ein Objekt mit Feldern. Jeder Abschnitt kann einen oder mehrere Datensätze enthalten. Ihre Nummer wird im entsprechenden Feld im Nachrichtenkopf angegeben (ANCOUNT, NSCOUNT bzw. ARCOUNT). Beispielsweise gibt eine IP-Anfrage für die Domain "google.com" mehrere IP-Adressen zurück, sodass im Abschnitt " Antwort" auch mehrere Einträge vorhanden sind, einer für jede Adresse. Fehlt der Abschnitt, enthält das entsprechende Headerfeld 0.
Jeder Ressourceneintrag (RR) beginnt mit einem NAME-Feld, das einen Domänennamen enthält. Das Format dieses Felds entspricht dem QNAME-Feld im Abschnitt " Frage ".
Neben NAME befinden sich die Felder TYPE (Datensatztyp) und CLASS (seine Klasse). Beide Felder sind 16-Bit-numerisch und geben den Typ und die Klasse des Datensatzes an. Dies ähnelt auch dem Abschnitt " Fragen ", mit dem Unterschied, dass QTYPE und QCLASS dieselben Werte wie TYPE und CLASS haben können und einige weitere, die für sie einzigartig sind. Das heißt, in einer trockenen wissenschaftlichen Sprache ist der Satz von QTYPE- und QCLASS-Werten eine Obermenge der TYPE- und CLASS-Werte. Weitere Informationen zu den Unterschieden finden Sie unter https://tools.ietf.org/html/rfc1035#section-3.2.2 .
Die restlichen Felder sind:
- TTL ist eine 32-Bit-Zahl, die die Zeit angibt, zu der der Datensatz zuletzt war (in Sekunden).
- RDLENGTH ist eine 16-Bit-Zahl, die die Länge des nächsten RDATA-Felds in Bytes angibt.
- RDATA ist eigentlich eine Nutzlast, das Format hängt von der Art des Datensatzes ab. Bei einem Datensatz vom Typ A (Hostadresse) und der Klasse IN (Internet) sind dies beispielsweise 4 Byte, die eine IPv4-Adresse darstellen.
Das Format für die Aufzeichnung von Domänennamen ist für die Felder QNAME und NAME sowie für das Feld RDATA identisch, wenn es sich um einen CNAME-, MX-, NS- oder anderen Klassendatensatz handelt, der als Ergebnis einen Domänennamen annimmt.
Ein Domainname ist eine Folge von Labels (Abschnitte eines Namens, Subdomains - dies ist ein Label im Original, ich habe keine bessere Übersetzung gefunden). Ein Label ist ein einzelnes Byte der Länge, das eine Zahl enthält - die Länge des Inhalts des Labels in Bytes, gefolgt von einer Folge von Bytes der angegebenen Länge. Beschriftungen folgen nacheinander, bis ein Byte mit einer Länge von 0 gefunden wird. Die allererste Beschriftung kann sofort die Länge Null haben. Dies gibt die Stammdomäne (Stammdomäne) mit einem leeren Domänennamen an (manchmal als "" geschrieben).
In früheren DNS-Versionen konnten die Bytes in der Bezeichnung einen beliebigen Wert von (0 bis 255) haben. Es gab Regeln, die einer dringenden Empfehlung entsprachen: Das Etikett beginnt mit einem Buchstaben, endet mit einem Buchstaben oder einer Zahl und enthält nur Buchstaben, Zahlen oder Bindestriche in 7-Bit-ASCII-Codierung mit dem höchstwertigen Nullbit. Die aktuelle EDNS-Spezifikation erfordert bereits die eindeutige Einhaltung dieser Regeln ohne Abweichung.
Die zwei höchstwertigen Bits des Längenbytes werden als Tag-Typ-Attribut verwendet. Wenn sie Null sind ( 0b00xxxxxx ), ist dies eine normale Bezeichnung, und die verbleibenden Bits des Bytes der Länge geben die Anzahl der Datenbytes an, die in seiner Zusammensetzung enthalten sind. Die maximale Etikettenlänge beträgt 63 Zeichen. 63 in binärer Codierung ist nur 0b00111111 .
Wenn die beiden höchstwertigen Bits 0 bzw. 1 ( 0b01xxxxxx ) sind, handelt es sich um eine erweiterte Typbezeichnung des EDNS-Standards ( https://tools.ietf.org/html/rfc2671#section-3.1 ), die ab dem 1. Februar 2019 bei uns eingegangen ist. Die unteren sechs Bits enthalten den Beschriftungswert. Wir diskutieren EDNS in diesem Artikel nicht, aber es ist nützlich zu wissen, dass dies auch passiert.
Die Kombination der beiden höchstwertigen Bits, gleich 1 und 0 ( 0b10xxxxxx ), ist für die zukünftige Verwendung reserviert.
Wenn beide High-Bits gleich 1 sind ( 0b11xxxxxx ), bedeutet dies, dass Domänennamen komprimiert werden ( Komprimierung ), und wir werden näher darauf eingehen.
Komprimierung von Domänennamen
Wenn ein Byte mit einer Länge von zwei hohen Bits gleich 1 ( 0b11xxxxxx ) ist, ist dies ein Zeichen für die Komprimierung von Domänennamen. Die Komprimierung wird verwendet, um Nachrichten kürzer und präziser zu gestalten. Dies gilt insbesondere bei der Arbeit mit UDP, wenn die Gesamtlänge der DNS-Nachricht auf 512 Byte begrenzt ist (obwohl dies der alte Standard ist, siehe https://tools.ietf.org/html/rfc1035#section-2.3.4 Größenbeschränkungen , das neue EDNS ermöglicht das Senden von UPD-Nachrichten und länger). Der Kern des Prozesses besteht darin, dass, wenn eine DNS-Nachricht Domänennamen mit denselben Subdomänen der obersten Ebene enthält (z. B. mail.yandex.ru und yandex.ru ), anstelle der erneuten Angabe des gesamten Domänennamens die Bytenummer in der DNS-Nachricht, von der aus Lesen Sie den Domainnamen weiter. Dies kann ein beliebiges Byte der DNS-Nachricht sein, nicht nur im aktuellen Eintrag oder Abschnitt, sondern unter der Bedingung, dass es sich um ein Byte der Länge der Domänenbezeichnung handelt. Sie können sich nicht auf die Mitte der Marke beziehen. Angenommen, die Nachricht enthält eine mail.yandex.ru- Domäne. Mit Hilfe der Komprimierung können dann auch die Domänen yandex.ru , ru und root "" festgelegt werden (natürlich ist das Stammverzeichnis ohne Komprimierung einfacher zu schreiben, dies ist jedoch technisch mit Komprimierung möglich) hier zu machen ndex.ru wird nicht funktionieren. Außerdem enden alle abgeleiteten Domänennamen in der Stammdomäne , dh das Schreiben von beispielsweise mail.yandex schlägt ebenfalls fehl.
Ein Domainname kann:
- vollständig ohne Komprimierung aufgezeichnet werden,
- Beginnen Sie an einem Ort, an dem Komprimierung verwendet wird
- Beginnen Sie mit einem oder mehreren Etiketten ohne Komprimierung und wechseln Sie dann zur Komprimierung.
- leer sein (für die Stammdomäne).
Zum Beispiel kompilieren wir eine DNS-Nachricht und haben bereits den Namen "dom3.example.com" darin gefunden. Jetzt müssen wir "dom4.dom3.example.com" angeben. In diesem Fall können Sie den Abschnitt "dom4" ohne Komprimierung aufzeichnen und dann zur Komprimierung wechseln, dh einen Link zu "dom3.example.com" hinzufügen. Oder umgekehrt, wenn der Name "dom4.dom3.example.com" zuvor gefunden wurde, können Sie zur Angabe von "dom3.example.com" sofort die Komprimierung verwenden, indem Sie auf die Bezeichnung "dom3" verweisen. Was wir nicht tun können, ist, wie bereits gesagt, den Teil von 'dom4.dom3' durch Komprimierung anzugeben, da der Name mit einem Abschnitt der obersten Ebene enden muss. Wenn Sie plötzlich Segmente aus der Mitte angeben müssen, werden diese einfach ohne Komprimierung angezeigt.
Der Einfachheit halber kann unser Programm keine Domänennamen mit Komprimierung schreiben, sondern kann nur lesen. Der Standard erlaubt dies, das Lesen muss unbedingt implementiert werden, das Schreiben ist optional. Technisch wird das Lesen folgendermaßen implementiert: Wenn die beiden höchstwertigen Bits eines Bytes der Länge 1 enthalten, lesen wir das darauf folgende Byte und behandeln diese beiden Bytes als 16-Bit-Ganzzahl ohne Vorzeichen in der Reihenfolge der Big-Endian-Bits. Wir verwerfen die zwei höchstwertigen Bits (mit 1), lesen die resultierende 14-Bit-Nummer und lesen den Domänennamen aus dem Byte in der DNS-Nachricht unter der dieser Nummer entsprechenden Nummer weiter.
Der Code für die Funktion zum Lesen von Domainnamen lautet wie folgt:
function readDomainName (buf, startOffset, objReturnValue = {}) { let currentByteIndex = startOffset;
Listing 2. Lesen von Domainnamen aus einer DNS-Abfrage
Vollständiger Code für die Funktion zum Lesen des DNS-Eintrags aus dem Binärpuffer:
Listing 3. Lesen eines DNS-Eintrags aus einem Binärpuffer function parseDnsMessageBytes (buf) { const msgFields = {};
3. DNS-
, . , , , . , DNS-, , . , .
, - server.on("message", () => {})
1. :
4. DNS- server.on('message', async (localReq, linfo) => { const dnsRequest = functions.parseDnsMessageBytes(localReq); const question = dnsRequest.questions[0];
4. DNS-
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 () {
10.
Insgesamt
DNS- NodeJS, npm . , , , , .
GitHub
Quellen: