El Camino Del Backend Developer: DNS, segunda parte
En muchos computadores modernos que usan algún sistema operativo basado en Unix existe un archivo llamado hosts
. Normalmente se encuentra ubicado en el directorio /etc/
. Si yo ejecuto el comando cat /etc/hosts
en mi computador obtengo esto:
##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting. Do not change this entry.
##
127.0.0.1 localhost
255.255.255.255 broadcasthost
::1 localhost
# Added by Docker Desktop
# To allow the same kube context to work on the host and the container:
127.0.0.1 kubernetes.docker.internal
# End of section
Este archivo es un resabio de un archivo que se llamaba HOSTS.TXT que era mantenido por el Stanford Research Institute para ARPANET. En este registro se mantiene un mapeo simple de direcciones con nombres de servidores.
Este archivo se repartía entre los pocos miembros de internet y era la forma que se usaba para resolver nombres de servidores con direcciones IP. Con el tiempo, al crecer la cantidad de servidores conectados a la red mantener estos archivos era imposible. Así fue como en 1983 Paul Mockapetris diseñó e implementó DNS. Lo notable es que este servicio escaló de unos pocos servidores a soportar la inmensa cantidad de servidores que existen en internet.
En esta segunda parte sobre DNS exploraremos el protocolo en si mismo implementando nada menos que un servidor DNS. Esto nos permitirá aplicar todo lo que hemos revisado hasta ahora sobre protocolos de redes y los conceptos definidos en el anterior artículo de esta serie.
Otra razón de implementar este servidor es mostrar al futuro desarrollador backend los rudimentos de cómo implementar un protocolo binario, algo que puede ser muy útil en su trabajo cuando se requiere cierto nivel de desempeño o prestaciones que no se dan con protocolos de texto, normalmente basados en HTTP.
Construyendo un servidor DNS
Usaremos Python para construir nuestro servidor. Para esto nos apoyaremos en el excelente trabajo de Emil Hernvall, quien escribió una guía sobre DNS construyendo un servidor DNS en el lenguaje Rust1.
Los servidores DNS caen en dos tipos de categorías:
- Authoritative Server - un servidor NS que hospeda una o más “zonas”. Por ejemplo, el servidor para la zona google.com son ns1.google.com, ns2.google.com, ns3.google.com and ns4.google.com.
- Caching Server - un servidor DNS que realiza busquedas primero verificando en su caché para ver si ya conoce el registro siendo consultado, y si no es el caso realiza una búsqueda recursiva para resolver el nombre. Este es el tipo de servidores que se encuentran en el router de tu casa y los que te asigna tu proveedor de internet.
El servidor que implementaremos en este artículo es un servidor que resuelve recursivamente, sin usar un caché.
Un servidor DNS en Python
DNS usa UDP2 y se ha establecido que por defecto todo servidor DNS “escucha” sus requerimientos a través del puerto 53.
Lo primero que haremos es crear nuestra función principal que tendrá el clásico bucle de atención de un servidor, tal como vimos en los primeros capítulos de esta serie:
def main(host, port):
server_address = (host, port)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # DNS es UDP
sock.bind(server_address)
while True:
handle_query(sock)
Pondremos todo esto en un script python que llamaremos dns_server.py
y agregaremos al final de nuestro script esta instrucción:
if __name__ == '__main__':
main('localhost', 2053)
Noten que he elegido usar el puerto 2053, porque el puerto 53 está reservado y no será sencillo ejecutar nuestro servidor para que lo ocupe3.
Este código es simple y ya lo hemos visto, así que no requiere mucha explicación, ahora viene implementar la función handle_query()
.
La estructura de una consulta
El objetivo de la función handle_query
es gestionar las consultas que los clientes DNS (como el utilitario dig
) realizan a nuestro servidor. Para implementarlo debemos primero ser capaces de analizar una consulta, y para esto debemos entender la estructura de estas.
Tanto las consultas como las respuestas de neustro servicio tienen un formato establecido que corresponde a una trama de bytes con la siguiente estructura:
Sección | Tamaño | Tipo | Propósito |
---|---|---|---|
Header | 12 Bytes | Encabezado | Información sobre la consulta o respuesta. |
Question Section | Variable | Lista de Preguntas | En la práctica contiene sólo una pregunta indicando el nombre del dominio y el tipo de registro de interés. |
Answer Section | Variable | Lista de Registros | Los registros relevantes del tipo solicitado. |
Authority Section | Variable | Lista de Registros | Una lista de servidores de nombre (registros NS), usados para resolver consultas recursivamente. |
Additional Section | Variable | Lista de Registros | Registros adicionales, que podrían ser útiles. Por ejemplo, los correspondientes registros tipo A para los registros NS. |
La primera especificación del protocolo indica que un paquete con una solicitud a DNS tiene un largo máximo de 512 bytes y se realiza a través de UDP. Esto se modificó posteriormente y se puede usar TCP en la actualidad y extender el tamaño del paquete mediante eDNS, pero vamos a adherirnos a la especificación original en este ejercicio.
Dado lo anterior definiremos una constante en nuestro código para controlar el largo máximo del paquete a procesar:
buffer_size = 512
El encabezado (Header) es una trama de 12 bytes que codifica información sobre la estructura general del paquete. Su forma es la siguiente:
Nombre RFC | Nombre Descriptivo | Largo | Descripción |
---|---|---|---|
ID | Packet Identifier | 16 bits | Un identificador aleatorio que se asigna a los paquetes de consulta. Los paquetes de respuesta deben responder con el mismo identificador. Este se requiere para diferenciar las respuestas debido a la naturaleza sin estado (stateless) de UDP. |
QR | Query Response | 1 bit | 0 para consulta, 1 para respuestas. |
OPCODE | Operation Code | 4 bits | Tipicamente tiene el valor 0, consultar RFC1035 para más detalles. |
AA | Authoritative Answer | 1 bit | Contiene un 1 si el servidor que responde es autoridad, es decir, es dueño del dominio consultado. |
TC | Truncated Message | 1 bit | Contiene un 1 si el mensaje excede los 512 bytes. Es una indicación de que la consulta puede ser realizada nuevamente usando TCP, para lo cual no hay limitación en el largo. |
RD | Recursion Desired | 1 bit | Definida por el que envía la pregunta si el servidor debe intentar resolver la consulta recursivamente si no hay una respuesta disponible. |
RA | Recursion Available | 1 bit | Definida por el servidor para indicar si están o no permitidas las consultads recursivas. |
Z | Reserved | 3 bits | Originalmente reservado para uso posterior, hoy se usa para consultas DNSSEC. |
RCODE | Response Code | 4 bits | Definida por el servidor para idnicar el estado de la respuesta, es decir, si ha sido existosa o ha fallod, y en el último caso proveer detalles de la causa de la falla. |
QDCOUNT | Question Count | 16 bits | El tamaño de la lista en la Question Section |
ANCOUNT | Answer Count | 16 bits | El tamaño de la lista en la Answer Section |
NSCOUNT | Authority Count | 16 bits | El tamaño de la lista en la Authority Section |
ARCOUNT | Additional Count | 16 bits | El tamaño de la lista en la Additional Section |
Este encabezado lo modelaremos en python con la siguiente clase:
from recordclass import RecordClass
class DnsHeader(RecordClass):
id: int
response: int
opcode: int
authoritative_answer: int
truncated_message: int
recursion_desired: int
recursion_available: int
z: int
rescode: int
questions: int
answers: int
authoritative_entries: int
resource_entries: int
Noten que estamos usando el paquete recordclass, por conveniencia pues nos permite hacer nuestro código más compacto y aplicar “type hints”.
Después del encabezado, viene la sección de preguntas (Query Section), la estructura de una pregunta es la siguiente:
Campo | Tipo | Descripción |
---|---|---|
Name | Secuencia de Etiquetas | El nombre de dominio, codificado en secuencias que describiremos más adelante. |
Type | Entero de 2-bytes | El tipo de registro |
Class | Entero de 2-bytes | La clase, en la prácita siempre tiene el valor 1. |
Esto lo modelaremos en python así:
class DnsQuestion(RecordClass):
name: str
qtype: int
Noten que no usaremos el campo class, porque siempre tiene 1.
Luego vienen distintos tipos de registros. El protocolo define varios tipos, pero nosotros nos concentraremos en algunos de los más típicos.
Todos los registros tienen el siguiente preámbulo:
Campo | Tipo | Descripción |
---|---|---|
Name | Secuencia de Etiquetas | El nombre de dominio, codificado en secuencias que describiremos más adelante. |
Type | Entero de 2-bytes | El tipo de registro |
Class | Entero de 2-bytes | La clase, en la prácita siempre tiene el valor 1. |
TTL | Entero de 4-bytes | Time-To-Live, i.e. cuanto tiempo un registro debe estar en caché antes de se requerido. |
Len | Entero de 2-bytes | Largo de la data específica para este tipo de registros. |
El primer tipo de registros es el tipo A. Este registro mapea un nombre a una dirección IP. La estructura es la siguiente:
Campo | Tipo | Descripción |
---|---|---|
Preamble | Preámbulo del registro | El preámbulo según el formato descrito recién, con el campo Len con el valor 4. |
IP | Entero de 4-bytes | Una dirección IP-address codificada como un entero de 4bytes. |
El campo tipo A lo modelaremos así:
class DnsRecord_A(RecordClass):
domain: str
addr: str
ttl: int
Noten que sólo almacenamos la información esencial.
Los tipos de registos que implementaremos son los siguientes:
ID | Nombre | Descripción | Codificación |
---|---|---|---|
1 | A | Alias - Mapea nombre a direcciones IP | Preámbulo + Cuatro bytes para una dirección IPv4 |
2 | NS | Name Server - La dirección del Servidor DNS para un dominio | Preámbulo + Secuencia de Etiquetas |
5 | CNAME | Canonical Name - Mapea nombres a nombres | Preámbulo + Secuencia de Etiquetas |
15 | MX | Mail eXchange - El host del servidor de mail de un dominio | Preámbulo + 2-bytes para prioridad + Secuencia de etiquetas |
28 | AAAA | IPv6 alias | Preámbulo + dieciseis bytes para direcciones IPv6 |
Para poder convertir una trama de bytes en esta estructura usaremos el paquete bitstruct que nos permite “empaquetar” y “desempaquetar” estructuras de python a un formato binario.
Todos estos tipos de registros los modelaremos así:
class DnsRecord_AAAA(RecordClass):
domain: str
addr: str
ttl: int
class DnsRecord_CNAME(RecordClass):
domain: str
host: str
ttl: int
class DnsRecord_NS(RecordClass):
domain: str
host: str
ttl: int
class DnsRecord_MX(RecordClass):
domain: str
addr: str
ttl: int
priority: int
class DnsRecord_UNKNOWN(RecordClass):
domain: str
qtype: int
data_len: int
ttl: int
Noten que hemos agregado una clase llamada DnsRecord_UNKNOWN
con la que manejaremos el resto de los tipos que no hemos enumerado acá.
Como cada tipo se identifica por un número definiremos estas constantes para poder usarlas en nuestro analizador de paquetes:
UNKNOWN = 0
A = 1
NS = 2
CNAME = 5
MX = 15
AAAA = 28
Todos estos elementos se agrupan en la clase DnsPacket
:
class DnsPacket(RecordClass):
header: DnsHeader
questions: list
answers: list
authorities: list
resources: list
Analizando un paquete DNS
Ahora podemos construir una función que nos permita analizar los paquetes a partir de un arreglo de bytes:
HEADER_BINARY_STRUCT = ">u16>u1>u4>u1>u1>u1>u1>u3>u4>u16>u16>u16>u16"
def parse_packet(buf):
unpacked = unpack(HEADER_BINARY_STRUCT, buf)
header = DnsHeader(*unpacked)
pos = 12
pos, questions = parse_questions(header, pos, buf)
pos, answers = parse_answers(header, pos, buf)
pos, authorities = parse_authorities(header, pos, buf)
pos, resources = parse_resources(header, pos, buf)
return DnsPacket(header, questions, answers, authorities, resources)
Para poder analizar los paquetes usaremos la biblioteca bitstruct.
Por ejemplo, si la variable buf
contiene 12 bytes, estos pueden ser convertidos a la estructura DnsHeader mediante este código:
HEADER_BINARY_STRUCT = ">u16>u1>u4>u1>u1>u1>u1>u3>u4>u16>u16>u16>u16"
unpacked = unpack(HEADER_BINARY_STRUCT, buf)
header = DnsHeader(*unpacked)
La función unpack realiza toda la magia de interpretar los bytes en buffer como tramas de bits que son agrupados.
La notación “>u16”, indica que lo que viene es un entero sin signo de tamaño 16 bits y sus bits más significativos están a la izquierda (big endian)4.
Noten que cada función parse_XXXX
recibe el encabezado, una variable pos
y el buffer de entrada (un arreglo de bytes).
Es responsabilidad de cada función incrementar pos y retornarlo, esto permite posicionar la lectura de nuestro analizador.
Este es el código de las funciones para analizar cada una de las partes de un paquete DNS:
def parse_elements(limit, pos, buf, parser):
result = []
for i in range(0, limit):
pos, e = parser(pos, buf)
result.append(e)
return pos, result
def parse_questions(header, pos, buf):
return parse_elements(header.questions, pos, buf, parse_question)
def parse_answers(header, pos, buf):
return parse_elements(header.answers, pos, buf, parse_dns_record)
def parse_authorities(header, pos, buf):
return parse_elements(header.authoritative_entries, pos, buf, parse_dns_record)
def parse_resources(header, pos, buf):
return parse_elements(header.resource_entries, pos, buf, parse_dns_record)
Como se aprecia usamos una función auxiliar parse_elements
que agrupa el código común, lo que hacen estas funciones es retornar listas de elementos de distintos tipos.
Hay dos tipos de funciones para analizar elementos específicos: parse_question
y parse_dns_record
.
Esta es la función para analizar un elemento tipo question:
def parse_question(pos, buf):
pos, name = parse_qname(pos, buf)
pos, qtype = parse_u16(pos, buf)
pos, _ = parse_u16(pos, buf)
return pos, DnsQuestion(name, qtype)
def parse_u16(pos, buf):
return pos + 2, unpack("u16", buf[pos:pos + 2])[0]
La función parse_dns_record es más compleja:
def parse_dns_record(pos, buf):
pos, domain = parse_qname(pos, buf)
pos, qtype = parse_u16(pos, buf)
pos, _ = parse_u16(pos, buf)
pos, ttl = parse_u32(pos, buf)
pos, data_len = parse_u16(pos, buf)
if qtype == A:
pos, raw_addr = parse_u32(pos, buf)
addr = ipaddress.ip_address(raw_addr)
return pos, DnsRecord_A(domain, addr, ttl)
elif qtype == AAAA:
pos, raw_addr1 = parse_u32(pos, buf)
pos, raw_addr2 = parse_u32(pos, buf)
pos, raw_addr3 = parse_u32(pos, buf)
pos, raw_addr4 = parse_u32(pos, buf)
x = "{:x}:{:x}:{:x}:{:x}:{:x}:{:x}:{:x}:{:x}".format((raw_addr1 >> 16) & 0xFFFF, raw_addr1 & 0xFFFF,
(raw_addr2 >> 16) & 0xFFFF, raw_addr2 & 0xFFFF,
(raw_addr3 >> 16) & 0xFFFF, raw_addr3 & 0xFFFF,
(raw_addr4 >> 16) & 0xFFFF, raw_addr4 & 0xFFFF,
)
addr = ipaddress.ip_address(x)
return pos, DnsRecord_AAAA(domain, addr, ttl)
elif qtype == NS:
pos, host = parse_qname(pos, buf)
return pos, DnsRecord_NS(domain, host, ttl)
elif qtype == CNAME:
pos, host = parse_qname(pos, buf)
return pos, DnsRecord_CNAME(domain, host, ttl)
elif qtype == MX:
pos, priority = parse_u16(pos, buf)
pos, host = parse_qname(pos, buf)
return pos, DnsRecord_MX(domain, host, ttl, priority)
else:
pos += data_len
return pos, DnsRecord_UNKNOWN(domain, qtype, data_len, ttl)
def parse_u32(pos, buf):
return pos + 4, unpack("u32", buf[pos:pos + 4])[0]
Lo más complejo es analizar las etiquetas de nombres. Las etiquetas de nombres se codifican como caracteres ascii con un máximo de 63 bytes. Pero un dominio puede tener varias partes separadas por un punto. Por ejemplo “www.programado.org” tiene tres partes, estas se codifican en forma binaria colocando el largo de cada parte seguido de los caracteres que la componen. Por ejemplo www.programando.org se codifica así:
\x03www\x0Bprogramando\x03org\x00
Esto parece sencillo, pero recordemos que el protocolo original sólo nos permite 512 bytes, esto obliga a usar cierto tipo de compresión.Por ejemplo, consideren esta consulta usando la herramienta dig
que vimos en el capíulo anterior:
$ dig @a.root-servers.net com
-- snip --
;; AUTHORITY SECTION:
com. 172800 IN NS a.gtld-servers.net.
com. 172800 IN NS b.gtld-servers.net.
com. 172800 IN NS c.gtld-servers.net.
com. 172800 IN NS d.gtld-servers.net.
com. 172800 IN NS e.gtld-servers.net.
com. 172800 IN NS f.gtld-servers.net.
com. 172800 IN NS g.gtld-servers.net.
com. 172800 IN NS h.gtld-servers.net.
com. 172800 IN NS i.gtld-servers.net.
com. 172800 IN NS j.gtld-servers.net.
com. 172800 IN NS k.gtld-servers.net.
com. 172800 IN NS l.gtld-servers.net.
com. 172800 IN NS m.gtld-servers.net.
;; ADDITIONAL SECTION:
a.gtld-servers.net. 172800 IN A 192.5.6.30
b.gtld-servers.net. 172800 IN A 192.33.14.30
c.gtld-servers.net. 172800 IN A 192.26.92.30
d.gtld-servers.net. 172800 IN A 192.31.80.30
e.gtld-servers.net. 172800 IN A 192.12.94.30
f.gtld-servers.net. 172800 IN A 192.35.51.30
g.gtld-servers.net. 172800 IN A 192.42.93.30
h.gtld-servers.net. 172800 IN A 192.54.112.30
i.gtld-servers.net. 172800 IN A 192.43.172.30
j.gtld-servers.net. 172800 IN A 192.48.79.30
k.gtld-servers.net. 172800 IN A 192.52.178.30
l.gtld-servers.net. 172800 IN A 192.41.162.30
m.gtld-servers.net. 172800 IN A 192.55.83.30
-- snip --
Como pueden ver la secuencia gtld-servers.net.
se repite muchas veces. La forma de comprimir la información es incluir esta etiqueta sólo una vez y generar una manera para poder “saltar” a un sector del paquete donde se encuentre esta etiqueta.
Recordemos que cada etiqueta viene precedida por el largo de la misma. Cuando este byte tiene los dos primeros bits en 1 significa que se debe realizar un salto, la posición a la cual saltar se indica en el siguiente campo. Por ejemplo, si la etiqueta empieza en binario con la siguiente secuencia:
BINARIO: 11000000 00001100
HEXADECIMAL: 0xC0 0x0C
Esto quiere decir que nuestra etiqueta se encuentra en la posición 12 de nuestro buffer. Considerando esto el análisis de un nombre se implementa con esta función:
def parse_qname(pos, buf):
parts = []
max_jumps = 5
jumps_performed = 0
p = pos
l = int(buf[p])
while l > 0 and jumps_performed <= max_jumps:
if (l & 0xC0) == 0XC0:
if jumps_performed == 0:
pos = p + 2
b2 = int(buf[p + 1])
offset = ((l ^ 0xC0) << 8) | b2
p = offset
jumps_performed += 1
else:
p += 1
str_buf = str(buf[p:p + l].decode("utf-8"))
parts.append(str_buf.lower())
p += l
l = int(buf[p])
if jumps_performed > max_jumps:
return pos, None
if jumps_performed == 0:
pos = p + 1
return pos, str(str(".").join(parts))
Notar que hemos puesto un límite a la cantidad de saltos, esta es una medida de seguridad para evitar paquetes mal formados que pueden provocar un loop infinito en nuestro servidor.
Esta función retorna el nombre en un formato legible por nosotros colocando “.” entre cada etiqueta.
Implementando handle_query
Ahora tenemos todos los elementos básicos para implementar
La función handle_query
inicia con estas líneas de código:
NOERROR = 0
FORMERR = 1
SERVFAIL = 2
NXDOMAIN = 3
NOTIMP = 4
REFUSED = 5
buffer_size = 512
def handle_query(sock):
buf, address = sock.recvfrom(buffer_size)
request = parse_packet(buf)
packet = DnsPacket(DnsHeader(request.header.id, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0), [], [], [], [])
question = request.questions.pop()
if question:
result = recursive_lookup(question.name, question.qtype)
if result:
packet.questions.append(question)
packet.header.rescode = result.header.rescode
packet.answers = result.answers
packet.authorities = result.authorities
packet.resources = result.resources
else:
packet.header.rescode = SERVFAIL
else:
packet.header.rescode = FORMERR
out_buf = write_packet(packet)
sock.sendto(out_buf, address)
Notar que hemos definido una serie de constantes para indicar los distintos tipos de errores que se pueden producir en el protocolo.
Además esta función delega a la función recursive_lookup
la resolución de la consulta.
recursive_lookup
se define así:
def recursive_lookup(qname, qtype):
# partiremos con a.root-servers.net
ns = "198.41.0.4"
while True:
print("intentamos búsqueda de {} {} con el ns {}".format(qtype, qname, ns))
ns_copy = ns
server = (str(ns_copy), 53)
response = lookup(qname, qtype, server)
if response.answers and response.header.rescode == NOERROR:
return response
if response.header.rescode == NXDOMAIN:
return response
new_ns = get_resolved_ns(response, qname)
if new_ns:
ns = new_ns
continue
new_ns_name = get_unresolved_ns(response, qname)
if not new_ns_name:
return response
recursive_response = recursive_lookup(new_ns_name, A)
new_ns = get_random_a(recursive_response)
if new_ns:
ns = new_ns
else:
return response
La búsqueda recusriva parte consultando a un servidor raiz. Si tenemos una respuesta (en la lista answers
) la retornamos.
De lo contrario derivamos a los servidores DNS que vienen en el paquete que se resuelven usando las funciones get_resolved_ns
y get_unresolved_ns
. El detalle de estas funciones está en el código en el repositorio de este ejemplo y su análisis queda como ejercicio al lector.
Finalmente la función lookup es la que termina consultando a un servidor DNS:
def lookup(qname, qtype, server):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # DNS es UDP
sock.bind(("0.0.0.0", 43210))
packet = DnsPacket(DnsHeader(6666, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0), [DnsQuestion(qname, qtype)], [], [],
[])
req_buffer = write_packet(packet)
sock.sendto(req_buffer, server)
buf, _ = sock.recvfrom(512)
return parse_packet(buf)
Esta función es bastante simple. La función write_packet
es el inverso de parse_packet
y lo que hace es convertir una estructura DnsPacket
en un buffer binario.
def write_packet(packet):
packet.header.questions = len(packet.questions)
packet.header.answers = len(packet.answers)
packet.header.authoritative_entries = len(packet.authorities)
packet.header.resource_entries = len(packet.resources)
buf = bytearray()
header = packet.header
buf.extend(pack('>u16', header.id))
buf.extend(pack('>u1u4u1u1u1u1u3u4', header.response, header.opcode, header.authoritative_answer,
header.truncated_message, header.recursion_desired, header.recursion_available,
header.z, header.rescode))
buf.extend(pack('>u16', header.questions))
buf.extend(pack('>u16', header.answers))
buf.extend(pack('>u16', header.authoritative_entries))
buf.extend(pack('>u16', header.resource_entries))
for q in packet.questions:
buf.extend(write_question(q))
for a in packet.answers:
buf.extend(write_dns_record(a))
for a in packet.authorities:
buf.extend(write_dns_record(a))
for r in packet.resources:
buf.extend(write_dns_record(r))
return buf
El detalle de las otras funciones lo pueden revisar en el repositorio.
Todo el código de nuestro servidor consiste en unas 400 líneas de código python y pueden verlo por completo en el siguiente repo en GitHub: https://github.com/lnds/desafios-programando.org/tree/master/2020-09-02/py-dns-server
Cuando lo ejecutamos:
$ python dns_server.py
Podemos probarlo usando dig
:
$ dig @127.0.0.1 -p 2053 www.programando.org
; <<>> DiG 9.10.6 <<>> @127.0.0.1 -p 2053 www.programando.org
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 37493
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;www.programando.org. IN A
;; ANSWER SECTION:
www.programando.org. 20 IN A 18.230.52.212
;; Query time: 724 msec
;; SERVER: 127.0.0.1#2053(127.0.0.1)
;; WHEN: Sun Sep 06 23:33:43 -03 2020
;; MSG SIZE rcvd: 72
Como pueden ver nuestro servidor funciona bastante bien. Recomiendo analizar el código y probarlo, esta es una manera de entender los detalles de bajo nivel de DNS, pero también les permite entender cómo se implementa un protocolo binario usando UDP.
Con esto hemos logrado terminar la primera fase de nuestro roadmap:
¡Eso es un gran avance! ¿Están listos para revisar sus conocimientos básicos de desarrollo front? Eso es lo que empezaremos en la siguiente fase de nuestra jornada.
Ejercicios
- Modifica el servidor para que inicie su búsqueda recursiva a partir de un servidor definido en un archivo de configuración.
- Modifica el servidor para que tenga un “caché” donde guarde los resultados de las últimas 100 consultas realizadas. Recuerda considerar el campo TTL para invalidar valores en el caché.
-
La guia original de Emil Hernvall se encuentra en Github: https://github.com/EmilHernvall/dnsguide, yo modifiqué un poco su código y reescribí el servidor en Rust en este repo: https://github.com/lnds/desafios-programando.org/tree/master/2020-09-02/dns-server ↩︎
-
Te sugiero repasar la primera y segunda parte de esta serie donde se habla sobre los tipos de protocolos en internet. ↩︎
-
El problema tiene solución, y tiene que ver con los permisos con los que corras tu servidor y tener cuidado de que en tu sistema no esté corriendo un servidor DNS en el puerto 53. ↩︎
-
Sobre la forma de almacenar bytes en un computador, y el concepto de bits significativos y endianness puedes leer este artículo introductorio en Wikipedia: https://es.wikipedia.org/wiki/Endianness ↩︎