Un paquete sobre los baches en un bosque lejano para DNS ...
L. Kaganov "Aldea en la parte inferior"
Al desarrollar una aplicación de red, a veces se hace necesario ejecutarla localmente, pero acceder a ella utilizando un nombre de dominio real. La solución estándar probada es registrar el dominio en el archivo de hosts. El inconveniente del enfoque es que los hosts requieren una correspondencia clara de los nombres de dominio, es decir No es compatible con las estrellas. Es decir si hay dominios de la forma:
dom1.example.com, dom2.example.com, dom3.example.com, ................ domN.example.com,
luego, en los hosts, debe registrarlos a todos. En algunos casos, el dominio de tercer nivel no se conoce de antemano. Hay un deseo (escribo para mí, alguien podría decir que es normal) sobrevivir con una línea como esta:
*.example.com
La solución al problema puede ser utilizar su propio servidor DNS, que procesará las solicitudes de acuerdo con la lógica especificada. Existen tales servidores, ambos completamente gratuitos y con una interfaz gráfica conveniente, como CoreDNS . También puede cambiar los registros DNS en el enrutador. Finalmente, use un servicio como xip.io , no es un servidor DNS completo, pero es perfecto para algunas tareas. En resumen, existen soluciones listas para usar, que puede usar y no molestar.
Pero este artículo describe otra forma: escribir su propia bicicleta, el punto de partida para crear una herramienta como las enumeradas anteriormente. Escribiremos nuestro proxy DNS, que escuchará las consultas DNS entrantes, y si el nombre de dominio solicitado está en la lista, devolverá la IP especificada y, de lo contrario, solicitará un servidor DNS superior y reenviará la respuesta recibida sin cambios en el programa solicitante.
Al mismo tiempo, puede registrar solicitudes y las respuestas recibidas. Dado que todos necesitan DNS: navegadores, mensajeros y antivirus, y servicios de sistema operativo, etc., puede ser muy informativo.
El principio es simple. En la configuración de conexión de red para IPv4, cambiamos la dirección del servidor DNS a la dirección de la máquina con nuestro proxy DNS autoescrito en ejecución (127.0.0.1, si no estamos trabajando en la red), y en su configuración especificamos la dirección del servidor DNS superior. Y, al parecer, eso es todo!
No utilizaremos las funciones estándar para resolver los nombres de dominio nslookup y nsresolve , por lo que la configuración del sistema DNS y el contenido del archivo hosts no afectarán el funcionamiento del programa. Dependiendo de la situación, puede ser útil o no, solo necesita recordar esto. Para simplificar, nos restringimos a la implementación de la funcionalidad básica en sí:
- IP spoofing solo para registros de tipo A (dirección de host) y clase IN (Internet)
- direcciones IP falsificadas solo versión 4
- conexión para solicitudes entrantes locales solo a través de UDP
- conexión al servidor DNS ascendente a través de UDP o TLS
- Si hay varias interfaces de red, se aceptarán solicitudes locales entrantes en cualquiera de ellas.
- sin soporte EDNS
Hablando de pruebasHay pocas pruebas unitarias en el proyecto. Es cierto que funcionan de acuerdo con el principio: lo lancé, y si se muestra algo sensato en la consola, entonces todo está bien, pero si una excepción vuela, entonces hay un problema. Pero incluso un enfoque tan torpe le permite localizar con éxito el problema, por lo tanto, Unidad.
Inicio - servidor en el puerto 53
Empecemos En primer lugar, debe enseñar a la aplicación a aceptar consultas DNS entrantes. Estamos escribiendo un servidor TCP simple que solo escucha el puerto 53 y registra las conexiones entrantes. En las propiedades de la conexión de red, escribimos la dirección del servidor DNS 127.0.0.1, iniciamos la aplicación, vamos al navegador durante varias páginas y ... en silencio en la consola, el navegador muestra la página normalmente. Bueno, cambiamos TCP a UDP, comenzamos, vamos por el navegador: en el navegador hay un error de conexión, algunos datos binarios se vierten en la consola. Entonces, el sistema envía solicitudes a través de UDP, y escucharemos las conexiones entrantes a través de UDP en el puerto 53. Media hora de trabajo, de los cuales 15 minutos busca en Google cómo generar un servidor TCP y UDP en NodeJS, y hemos resuelto la tarea fundamental del proyecto, que determina la estructura de la aplicación futura. El código es el siguiente:
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);
Listado 1. El código mínimo necesario para recibir consultas DNS locales
El siguiente punto es leer el mensaje para comprender si es necesario devolver nuestra IP en respuesta o simplemente transmitirlo.
Mensaje DNS
La estructura del mensaje DNS se describe en RFC-1035. Tanto las solicitudes como las respuestas siguen esta estructura y, en principio, difieren en un indicador de un bit (campo QR) en el encabezado del mensaje. El mensaje incluye cinco secciones:
+---------------------+ | Header | +---------------------+ | Question | the question for the name server +---------------------+ | Answer | RRs answering the question +---------------------+ | Authority | RRs pointing toward an authority +---------------------+ | Additional | RRs holding additional information +---------------------+
Estructura (s) general (es) de mensajes DNS https://tools.ietf.org/html/rfc1035#section-4.1
Un mensaje DNS comienza con un encabezado de longitud fija (esta es la sección denominada Encabezado ), que contiene campos de 1 bit a dos bytes de largo (por lo tanto, un byte en el encabezado puede contener varios campos). El encabezado comienza con el campo ID: este es el identificador de solicitud de 16 bits, la respuesta debe tener la misma ID. A continuación están los campos que describen el tipo de solicitud, el resultado de su ejecución y el número de registros en cada una de las secciones posteriores del mensaje. Descríbalos a todos durante mucho tiempo, así que a quién le importa, bueno en el RFC: https://tools.ietf.org/html/rfc1035#section-4.1.1 . La sección Encabezado siempre está presente en el mensaje 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 | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
Estructura (s) del encabezado del mensaje DNS https://tools.ietf.org/html/rfc1035#section-4.1.1
Sección de preguntas
La sección Pregunta contiene una entrada que le dice al servidor exactamente qué información se necesita de él. Teóricamente, en la sección de dichos registros puede haber uno o varios, su número se indica en el campo QDCOUNT en el encabezado del mensaje y puede ser 0, 1 o más. Pero en la práctica, la sección Pregunta puede contener solo una entrada. Si la sección Pregunta contenía varios registros, y uno de ellos provocaría un error al procesar la solicitud en el servidor, surgiría una situación indefinida. Aunque el servidor devolverá un código de error en el campo RCODE en el mensaje de respuesta, no podrá indicar al procesar qué registro se produjo el problema, la especificación no describe esto. Los registros tampoco tienen campos que contengan una indicación del error y su tipo. Por lo tanto, existe un acuerdo (no documentado), según el cual la sección Pregunta puede contener solo un registro, y el campo QDCOUNT tiene un valor de 1. Tampoco está completamente claro cómo procesar la solicitud en el lado del servidor, si aún contiene varios registros en la Pregunta . Alguien aconseja devolver un mensaje con un error de solicitud. Y, por ejemplo, Google DNS procesa solo el primer registro en la sección Pregunta , simplemente ignora el resto. Aparentemente, esto queda a discreción de los desarrolladores de servicios de DNS.
En el mensaje DNS de respuesta del servidor, la sección Pregunta también está presente y debe copiar completamente la Pregunta de la solicitud (para evitar conflictos, en caso de que un campo ID no sea suficiente).
La única entrada en la sección Pregunta contiene los campos: QNAME (nombre de dominio), QTYPE (tipo), QCLASS (clase). QTYPE y QCLASS son números de doble byte que indican el tipo y la clase de la solicitud. Los tipos y clases posibles se describen en RFC-1035 https://tools.ietf.org/html/rfc1035#section-3.2 , todo está claro allí. Pero en el método de grabación de un nombre de dominio nos detendremos con más detalle en la sección "Formato para registrar nombres de dominio".
En el caso de una consulta, el mensaje DNS a menudo termina con la sección Pregunta , a veces la sección Adicional puede seguirlo.
Si se produjo un error al procesar la solicitud en el servidor (por ejemplo, una solicitud entrante se formó incorrectamente), el mensaje de respuesta también finalizará con la sección Pregunta o Adicional , y el campo RCODE del encabezado del mensaje de respuesta contendrá un código de error.
Respuesta , autoridad y secciones adicionales
Las siguientes secciones son Respuesta , Autoridad y Adicional ( Respuesta y Autoridad están contenidas solo en el mensaje DNS de respuesta. Puede aparecer Adicional en la solicitud y en la respuesta). Son opcionales, es decir cualquiera de ellos puede estar presente o no, dependiendo de la solicitud. Estas secciones tienen la misma estructura y contienen información en el formato de los llamados "registros de recursos" ( registro de recursos o RR). Hablando en sentido figurado, cada una de estas secciones es una matriz de registros de recursos, y un registro es un objeto con campos. Cada sección puede contener uno o más registros, su número se indica en el campo correspondiente en el encabezado del mensaje (ANCOUNT, NSCOUNT, ARCOUNT, respectivamente). Por ejemplo, una solicitud de IP para el dominio "google.com" devolverá varias direcciones IP, por lo que también habrá varias entradas en la sección Respuesta , una para cada dirección. Si la sección está ausente, el campo de encabezado correspondiente contiene 0.
Cada registro de recursos (RR) comienza con un campo NAME que contiene un nombre de dominio. El formato de este campo es el mismo que el campo QNAME de la sección Pregunta .
Junto a NAME están los campos TYPE (tipo de registro) y CLASS (su clase), ambos campos son numéricos de 16 bits, indican el tipo y la clase del registro. Esto también se asemeja a la sección Pregunta , con la diferencia de que su QTYPE y QCLASS pueden tener los mismos valores que TYPE y CLASS, y algunos más propios que son únicos para ellos. Es decir, en un lenguaje científico seco, el conjunto de valores QTYPE y QCLASS es un superconjunto de los valores TYPE y CLASS. Lea más sobre las diferencias en https://tools.ietf.org/html/rfc1035#section-3.2.2 .
Los campos restantes son:
- TTL es un número de 32 bits que indica el tiempo que duró el registro (en segundos).
- RDLENGTH es un número de 16 bits que indica la longitud del siguiente campo RDATA en bytes.
- RDATA es en realidad una carga útil, el formato depende del tipo de registro. Por ejemplo, para un registro de tipo A (dirección de host) y clase IN (Internet), estos son 4 bytes que representan una dirección IPv4.
El formato para registrar nombres de dominio es el mismo para los campos QNAME y NAME, así como para el campo RDATA, si es un registro CNAME, MX, NS u otra clase que asume un nombre de dominio como resultado.
Un nombre de dominio es una secuencia de etiquetas (secciones de un nombre, subdominios; esta es una etiqueta en el original, no encontré una mejor traducción). Una etiqueta es un byte único de longitud que contiene un número: la longitud del contenido de la etiqueta en bytes, seguida de una secuencia de bytes de la longitud especificada. Las etiquetas siguen una tras otra hasta que se encuentra un byte de longitud que contiene 0. La primera etiqueta puede ser inmediatamente de longitud cero, esto indica el dominio raíz (dominio raíz) con un nombre de dominio vacío (a veces escrito como "").
En versiones anteriores de DNS, los bytes en la etiqueta podrían tener cualquier valor de (0 a 255). Había reglas que estaban en la naturaleza de una recomendación fuerte: que la etiqueta comience con una letra, termine con una letra o número, y contenga solo letras, números o guiones en la codificación ASCII de 7 bits, con un bit alto cero. La especificación EDNS actual ya requiere el cumplimiento de estas reglas claramente, sin desviaciones.
Los dos bits más significativos del byte de longitud se utilizan como un atributo de tipo de etiqueta. Si son cero ( 0b00xxxxxx ), entonces esta es una etiqueta normal, y los bits restantes del byte de longitud indican el número de bytes de datos incluidos en su composición. La longitud máxima de la etiqueta es de 63 caracteres. 63 en codificación binaria es solo 0b00111111 .
Si los dos bits de orden superior son 0 y 1 ( 0b01xxxxxx ), respectivamente , esta es una etiqueta de tipo extendido del estándar EDNS ( https://tools.ietf.org/html/rfc2671#section-3.1 ), que nos llegó desde el 1 de febrero de 2019. Los seis bits inferiores contendrán el valor de la etiqueta. No estamos discutiendo EDNS en este artículo, pero es útil saber que esto también sucede.
La combinación de los dos bits más significativos, igual a 1 y 0 ( 0b10xxxxxx ), está reservada para uso futuro.
Si ambos bits altos son iguales a 1 ( 0b11xxxxxx ), esto significa que los nombres de dominio están comprimidos ( compresión ), y nos detendremos en esto con más detalle.
Compresión de nombres de dominio
Entonces, si un byte de longitud tiene dos bits altos iguales a 1 ( 0b11xxxxxx ), esto es un signo de compresión de nombre de dominio. La compresión se usa para hacer los mensajes más cortos y concisos. Esto es especialmente cierto cuando se trabaja en UDP, cuando la longitud total del mensaje DNS está limitada a 512 bytes (aunque este es el estándar anterior, consulte https://tools.ietf.org/html/rfc1035#section-2.3.4 Límites de tamaño , el nuevo EDNS permite enviar mensajes UPD y más largos). La esencia del proceso es que si un mensaje DNS contiene nombres de dominio con los mismos subdominios de nivel superior (por ejemplo, mail.yandex.ru y yandex.ru ), en lugar de volver a especificar el nombre de dominio completo, el número de bytes en el mensaje DNS desde el cual Continúa leyendo el nombre de dominio. Puede ser cualquier byte del mensaje DNS, no solo en el registro o sección actual, sino con la condición de que sea un byte de la longitud de la etiqueta de dominio. No puede hacer referencia a la mitad de la marca. Supongamos que hay un dominio mail.yandex.ru en el mensaje, luego, con la ayuda de la compresión, también es posible designar los dominios "" yandex.ru , ru y root "(por supuesto, la raíz es más fácil de escribir sin compresión, pero es técnicamente posible hacerlo con compresión), y aquí para hacer que ndex.ru no funcione. Además, todos los nombres de dominio derivados terminarán en el dominio raíz, es decir, escribir, por ejemplo, mail.yandex también fallará.
Un nombre de dominio puede:
- ser completamente grabado sin compresión,
- comenzar desde un lugar que usa compresión
- comience con una o más etiquetas sin compresión, y luego cambie a compresión,
- estar vacío (para el dominio raíz).
Por ejemplo, estamos compilando un mensaje DNS y ya habíamos encontrado el nombre "dom3.example.com" en él, ahora necesitamos especificar "dom4.dom3.example.com". En este caso, puede grabar la sección "dom4" sin compresión y luego cambiar a compresión, es decir, agregar un enlace a "dom3.example.com". O viceversa, si se encontró previamente el nombre "dom4.dom3.example.com", para indicar "dom3.example.com" puede usar inmediatamente la compresión haciendo referencia a la etiqueta "dom3" en él. Lo que no podemos hacer es, como ya se ha dicho, indicar la parte de 'dom4.dom3' a través de la compresión, porque el nombre debe terminar con una sección de nivel superior. Si de repente necesita especificar segmentos desde el medio, entonces simplemente se indican sin compresión.
Para simplificar, nuestro programa no sabe cómo escribir nombres de dominio con compresión, solo puede leer. El estándar lo permite, la lectura debe implementarse necesariamente, la escritura es opcional. Técnicamente, la lectura se implementa de esta manera: si los dos bits más significativos de un byte de longitud contienen 1, entonces leemos el byte siguiente y tratamos estos dos bytes como un entero sin signo de 16 bits, con el orden de los bits Big Endian. Descartamos los dos bits más significativos (que contienen 1), leemos el número de 14 bits resultante y seguimos leyendo el nombre de dominio del byte en el mensaje DNS debajo del número correspondiente a este número.
El código para la función de lectura de nombres de dominio es el siguiente:
function readDomainName (buf, startOffset, objReturnValue = {}) { let currentByteIndex = startOffset;
Listado 2. Lectura de nombres de dominio de una consulta DNS
Código completo para que la función lea el registro DNS del búfer binario:
Listado 3. Lectura de un registro DNS desde un búfer binario 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.
Total
DNS- NodeJS, npm . , , , , .
GitHub
: