Um pacote sobre os solavancos em uma floresta distante para DNS ...
L. Kaganov "Hamlet no fundo"
Ao desenvolver um aplicativo de rede, às vezes é necessário executá-lo localmente, mas acessá-lo usando um nome de domínio real. A solução padrão comprovada é registrar o domínio no arquivo hosts. O ponto negativo da abordagem é que os hosts exigem uma correspondência clara de nomes de domínio, ou seja, não suporta estrelas. I.e. se houver domínios no formulário:
dom1.example.com, dom2.example.com, dom3.example.com, ................ domN.example.com,
depois, nos hosts, você precisa registrar todos eles. Em alguns casos, o domínio de terceiro nível não é conhecido antecipadamente. Existe um desejo (eu escrevo para mim mesmo, alguém pode dizer que é normal) seguir com uma frase como esta:
*.example.com
A solução para o problema pode ser usar seu próprio servidor DNS, que processará solicitações de acordo com a lógica especificada. Existem servidores, totalmente gratuitos e com uma interface gráfica conveniente, como o CoreDNS . Você também pode alterar os registros DNS no roteador. Por fim, use um serviço como o xip.io , não é um servidor DNS completo, mas é perfeito para algumas tarefas. Em suma, existem soluções prontas, você pode usar e não se incomodar.
Mas este artigo descreve outra maneira - escrever sua própria bicicleta, o ponto de partida para criar uma ferramenta como as listadas acima. Escreveremos nosso proxy DNS, que ouvirá as consultas DNS recebidas e, se o nome de domínio solicitado estiver na lista, ele retornará o IP especificado e, caso contrário, solicitará um servidor DNS mais alto e encaminhará a resposta recebida sem alterações no programa solicitante.
Ao mesmo tempo, você pode registrar solicitações e as respostas recebidas. Como o DNS é necessário para todos - navegadores, mensageiros e antivírus, serviços do sistema operacional etc., pode ser muito informativo.
O princípio é simples. Nas configurações de conexão de rede para IPv4, alteramos o endereço do servidor DNS para o endereço da máquina com o proxy DNS auto-escrito em execução (127.0.0.1, se não estivermos trabalhando na rede) e, nas configurações, especificamos o endereço do servidor DNS mais alto. E, ao que parece, é tudo!
Não usaremos as funções padrão para resolver nomes de domínio nslookup e nsresolve , portanto, as configurações do sistema DNS e o conteúdo do arquivo hosts não afetarão a operação do programa. Dependendo da situação, pode ser útil ou não, você só precisa se lembrar disso. Por uma questão de simplicidade, nos restringimos à implementação da funcionalidade básica em si:
- Falsificação de IP apenas para registros do tipo A (endereço do host) e classe IN (Internet)
- endereços IP falsificados apenas na versão 4
- conexão para solicitações de entrada locais somente por UDP
- conexão com o servidor DNS upstream via UDP ou TLS
- se houver várias interfaces de rede, solicitações locais recebidas serão aceitas em qualquer uma delas
- sem suporte EDNS
Falando de testesExistem poucos testes de unidade no projeto. É verdade que eles funcionam de acordo com o princípio: eu o iniciei, e se algo sensato for exibido no console, tudo estará bem, mas se uma exceção surgir, haverá um problema. Mas mesmo uma abordagem tão desajeitada permite que você localize o problema com sucesso, então Unidade.
Iniciar - servidor na porta 53
Vamos começar. Primeiro de tudo, você precisa ensinar o aplicativo a aceitar as consultas DNS recebidas. Estamos escrevendo um servidor TCP simples que apenas escuta a porta 53 e registra as conexões de entrada. Nas propriedades da conexão de rede, escrevemos o endereço do servidor DNS 127.0.0.1, iniciamos o aplicativo, acessamos o navegador por várias páginas - e ... silêncio no console, o navegador exibe a página normalmente. Bem, mudamos de TCP para UDP, começamos, seguimos pelo navegador - no navegador há um erro de conexão, alguns dados binários foram derramados no console. Portanto, o sistema envia solicitações via UDP e ouviremos as conexões recebidas via UDP na porta 53. Meia hora de trabalho, dos quais 15 minutos pesquisam no Google como criar um servidor TCP e UDP no NodeJS e resolvemos a tarefa principal do projeto, que determina a estrutura do aplicativo futuro. O código é o seguinte:
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);
Lista 1. O código mínimo necessário para receber consultas DNS locais
O próximo ponto é ler a mensagem para entender se é necessário retornar nosso IP em resposta a ela ou simplesmente transmiti-lo.
Mensagem DNS
A estrutura da mensagem DNS é descrita na RFC-1035. Os pedidos e as respostas seguem essa estrutura e, em princípio, diferem em um sinalizador de bit (campo QR) no cabeçalho da mensagem. A mensagem inclui cinco seções:
+---------------------+ | Header | +---------------------+ | Question | the question for the name server +---------------------+ | Answer | RRs answering the question +---------------------+ | Authority | RRs pointing toward an authority +---------------------+ | Additional | RRs holding additional information +---------------------+
Estrutura (s) de mensagem DNS geral https://tools.ietf.org/html/rfc1035#section-4.1
Uma mensagem DNS começa com um cabeçalho de tamanho fixo (essa é a seção Cabeçalho ), que contém campos de 1 bit a dois bytes de comprimento (assim, um byte no cabeçalho pode conter vários campos). O cabeçalho começa com o campo ID - este é o identificador de solicitação de 16 bits, a resposta deve ter o mesmo ID. A seguir, estão os campos que descrevem o tipo de solicitação, o resultado de sua execução e o número de registros em cada uma das seções subsequentes da mensagem. Descreva todos eles por um longo tempo, para quem se importa - bem na RFC: https://tools.ietf.org/html/rfc1035#section-4.1.1 . A seção Cabeçalho está sempre presente na mensagem 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 | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
Estrutura (s) do cabeçalho da mensagem DNS https://tools.ietf.org/html/rfc1035#section-4.1.1
Seção de perguntas
A seção Pergunta contém uma entrada informando ao servidor exatamente quais informações são necessárias. Teoricamente, na seção desses registros, pode haver um ou vários, seu número é indicado no campo QDCOUNT no cabeçalho da mensagem e pode ser 0, 1 ou mais. Mas, na prática, a seção Pergunta pode conter apenas uma entrada. Se a seção Pergunta contivesse vários registros, e um deles levasse a um erro ao processar a solicitação no servidor, uma situação indefinida surgiria. Embora o servidor retorne um código de erro no campo RCODE na mensagem de resposta, não poderá indicar ao processar qual registro o problema ocorreu, a especificação não descreve isso. Os registros também não têm campos contendo uma indicação do erro e seu tipo. Portanto, existe um contrato (não documentado), segundo o qual a seção Pergunta pode conter apenas um registro, e o campo QDCOUNT tem um valor igual a 1. Também não está totalmente claro como processar a solicitação no lado do servidor, se ainda houver vários registros na Pergunta . Alguém aconselha retornar uma mensagem com um erro de solicitação. E, por exemplo, o DNS do Google processa apenas o primeiro registro na seção Pergunta , simplesmente ignora o restante. Aparentemente, isso permanece a critério dos desenvolvedores dos serviços DNS.
Na resposta DNS-message do servidor, a seção Pergunta também está presente e deve copiar completamente a Pergunta da solicitação (para evitar conflitos, caso um campo de ID não seja suficiente).
A única entrada na seção Pergunta contém os campos: QNAME (nome de domínio), QTYPE (tipo), QCLASS (classe). QTYPE e QCLASS são números de byte duplo, indicando o tipo e a classe da solicitação. Os tipos e classes possíveis são descritos na RFC-1035 https://tools.ietf.org/html/rfc1035#section-3.2 , tudo está claro lá. Mas, sobre o método de registrar um nome de domínio, veremos mais detalhadamente na seção "Formato para registrar nomes de domínio".
No caso de uma consulta, a mensagem DNS geralmente termina na seção Pergunta , às vezes a seção Adicional pode segui-la.
Se ocorreu um erro ao processar a solicitação no servidor (por exemplo, uma solicitação de entrada foi formada incorretamente), a mensagem de resposta também terminará com a seção Pergunta ou Adicional , e o campo RCODE do cabeçalho da mensagem de resposta conterá um código de erro.
Seções de resposta , autoridade e adicionais
As seções a seguir são Resposta , Autoridade e Adicional ( Resposta e Autoridade estão contidas apenas na mensagem DNS da resposta, Adicional pode aparecer na solicitação e na resposta). Eles são opcionais, ou seja, qualquer um deles pode estar presente ou não, dependendo da solicitação. Essas seções têm a mesma estrutura e contêm informações no formato dos chamados "registros de recursos" (registro de recurso, ou RR). Figurativamente falando, cada uma dessas seções é uma matriz de registros de recursos e um registro é um objeto com campos. Cada seção pode conter um ou mais registros, seu número é indicado no campo correspondente no cabeçalho da mensagem (ANCOUNT, NSCOUNT, ARCOUNT, respectivamente). Por exemplo, uma solicitação de IP para o domínio "google.com" retornará vários endereços IP; portanto, também haverá várias entradas na seção Resposta , uma para cada endereço. Se a seção estiver ausente, o campo de cabeçalho correspondente conterá 0.
Cada registro de recurso (RR) começa com um campo NAME que contém um nome de domínio. O formato desse campo é igual ao campo QNAME da seção Pergunta .
Ao lado de NAME estão os campos TYPE (tipo de registro) e CLASS (sua classe), ambos os campos são numéricos de 16 bits, indicam o tipo e a classe do registro. Isso também se assemelha à seção Pergunta , com a diferença de que seus QTYPE e QCLASS podem ter todos os mesmos valores que TYPE e CLASS e alguns mais próprios que são exclusivos para eles. Ou seja, em uma linguagem científica seca, o conjunto de valores QTYPE e QCLASS é um superconjunto dos valores TYPE e CLASS. Leia mais sobre as diferenças em https://tools.ietf.org/html/rfc1035#section-3.2.2 .
Os campos restantes são:
- TTL é um número de 32 bits que indica a hora em que o registro foi passado (em segundos).
- RDLENGTH é um número de 16 bits que indica o comprimento do próximo campo RDATA em bytes.
- RDATA é realmente uma carga útil, o formato depende do tipo de registro. Por exemplo, para um registro do tipo A (endereço do host) e classe IN (Internet), são 4 bytes que representam um endereço IPv4.
O formato para registrar nomes de domínio é o mesmo para os campos QNAME e NAME, bem como para o campo RDATA, se for um CNAME, MX, NS ou outro registro de classe que assume um nome de domínio como resultado.
Um nome de domínio é uma sequência de rótulos (seções de um nome, subdomínios - este é um rótulo no original, não encontrei uma tradução melhor). Um rótulo é um único byte de comprimento que contém um número - o comprimento do conteúdo do rótulo em bytes, seguido por uma sequência de bytes do comprimento especificado. Os rótulos seguem um após o outro até que seja encontrado um byte de comprimento 0. O primeiro rótulo pode ter tamanho zero imediatamente, isso indica o domínio raiz (Domínio Raiz) com um nome de domínio vazio (às vezes escrito como "").
Nas versões anteriores do DNS, os bytes no rótulo podiam ter qualquer valor de (0 a 255). Havia regras que eram da natureza de uma recomendação urgente: que o rótulo comece com uma letra, termine com uma letra ou número e contenha apenas letras, números ou hifens na codificação ASCII de 7 bits, com o bit zero mais significativo. A especificação atual do EDNS já exige conformidade com essas regras claramente, sem desvio.
Os dois bits mais significativos do byte de comprimento são usados como um atributo de tipo de tag. Se forem zero ( 0b00xxxxxx ), esse é um rótulo normal e os bits restantes do byte de comprimento indicam o número de bytes de dados incluídos em sua composição. O comprimento máximo da etiqueta é de 63 caracteres. 63 na codificação binária é apenas 0b00111111 .
Se os dois bits de ordem superior são 0 e 1 ( 0b01xxxxxx ), respectivamente , esse é um rótulo de tipo estendido do padrão EDNS ( https://tools.ietf.org/html/rfc2671#section-3.1 ), que chegou até nós em 1 de fevereiro de 2019. Os seis bits inferiores conterão o valor do rótulo. Não estamos discutindo o EDNS neste artigo, mas é útil saber que isso também acontece.
A combinação dos dois bits mais significativos, igual a 1 e 0 ( 0b10xxxxxx ), é reservada para uso futuro.
Se os dois bits altos forem iguais a 1 ( 0b11xxxxxx ), isso significa que os nomes de domínio são compactados ( compactação ), e iremos nos aprofundar nisso com mais detalhes.
Compactação de Nomes de Domínio
Portanto, se um byte de comprimento tiver dois bits altos iguais a 1 ( 0b11xxxxxx ), isso é um sinal de compactação de nome de domínio. A compactação é usada para tornar as mensagens mais curtas e concisas. Isso é especialmente verdadeiro quando se trabalha com UDP, quando o comprimento total da mensagem DNS é limitado a 512 bytes (embora esse seja o padrão antigo, consulte https://tools.ietf.org/html/rfc1035#section-2.3.4 Limites de tamanho , o novo EDNS permite enviar mensagens UPD e mais). A essência do processo é que, se uma mensagem DNS contiver nomes de domínio com os mesmos subdomínios de nível superior (por exemplo, mail.yandex.ru e yandex.ru ), em vez de especificar novamente o nome de domínio inteiro, o número de byte na mensagem DNS a partir do qual Continue lendo o nome do domínio. Pode ser qualquer byte da mensagem DNS, não apenas no registro ou seção atual, mas com a condição de que seja um byte do comprimento do rótulo do domínio. Você não pode se referir ao meio da marca. Suponha que haja um domínio mail.yandex.ru na mensagem e, com a ajuda da compactação, também é possível designar os domínios yandex.ru , ru e root "" (é claro, a raiz é mais fácil de escrever sem compactação, mas é tecnicamente possível fazer isso com a compactação) e aqui para fazer ndex.ru não vai funcionar. Além disso, todos os nomes de domínio derivados terminarão no domínio raiz, ou seja, escreva, digamos, mail.yandex também falhará.
Um nome de domínio pode:
- ser totalmente gravado sem compressão,
- começar de um lugar que usa compressão
- comece com um ou mais rótulos sem compactação e mude para compactação,
- estar vazio (para o domínio raiz).
Por exemplo, estamos compilando uma mensagem DNS e já encontramos o nome "dom3.example.com" nela, agora precisamos especificar "dom4.dom3.exemplo.com". Nesse caso, você pode gravar a seção "dom4" sem compactação e, em seguida, alternar para compactação, ou seja, adicionar um link a "dom3.example.com". Ou vice-versa, se o nome "dom4.dom3.exemplo.com" foi encontrado anteriormente, para indicar "dom3.exemplo.com", você pode usar imediatamente a compactação consultando o rótulo "dom3" nele. O que não podemos fazer é, como já foi dito, indicar a parte de 'dom4.dom3' através da compactação, porque o nome deve terminar com uma seção de nível superior. Se você precisar especificar de repente segmentos do meio, eles são simplesmente indicados sem compactação.
Para simplificar, nosso programa não sabe escrever nomes de domínio com compactação, apenas pode ler. O padrão permite isso, a leitura deve ser implementada necessariamente, a escrita é opcional. Tecnicamente, a leitura é implementada assim: se os dois bits mais significativos de um byte de comprimento contiver 1, lemos o byte a seguir e tratamos esses dois bytes como um número inteiro não assinado de 16 bits, com a ordem dos bits do Big Endian. Descartamos os dois bits mais significativos (contendo 1), lemos o número de 14 bits resultante e continuamos lendo o nome do domínio a partir do byte na mensagem DNS sob o número correspondente a esse número.
O código para a função de leitura de nome de domínio é o seguinte:
function readDomainName (buf, startOffset, objReturnValue = {}) { let currentByteIndex = startOffset;
Listagem 2. Lendo nomes de domínio de uma consulta DNS
Código completo da função para ler o registro DNS do buffer binário:
Listagem 3. Lendo um registro DNS de um buffer binário function parseDnsMessageBytes (buf) { const msgFields = {};
Listagem 3. Lendo um registro DNS de um buffer binário
, . , , , . , DNS-, , . , .
, - server.on("message", () => {})
1. :
4. DNS- server.on('message', async (localReq, linfo) => { const dnsRequest = functions.parseDnsMessageBytes(localReq); const question = dnsRequest.questions[0];
Listagem 4. Processando uma consulta DNS local recebida
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
: