Apuntes · Capítulo 12Transporte y aplicaciones

DNS y HTTP: los nombres y la web por dentro

DNS y HTTP: los nombres y la web por dentro

En las tres semanas anteriores construimos el transporte: UDP, la entrega confiable, TCP y su control de congestión. Con eso, un programa ya le puede mandar bytes a otro programa del otro lado del mundo. Pero nadie abre el navegador para escribir 45.80.81.43. Ustedes escriben un nombre y piden una página. Esta semana subimos a la capa de aplicación para ver los dos protocolos que hacen posible ese gesto tan simple.

El primero es DNS, que traduce nombres a direcciones. Es una base de datos distribuida en todo el planeta, sin un servidor central, que responde en milisegundos y que casi nadie nota hasta que falla. En la semana 7 vimos lo que pasa cuando falla: en octubre de 2021, Facebook desapareció de Internet porque sus servidores DNS retiraron sus propias rutas. Ahora vamos a ver qué hacían esos servidores.

El segundo es HTTP, el protocolo de la web. Es sorprendentemente simple: texto que va y texto que vuelve. Lo interesante está en cómo se volvió rápido: conexiones que se reutilizan, caches que evitan preguntar y, con HTTP/2, muchas respuestas mezcladas en una sola conexión. Cada mejora se entiende contando idas y vueltas, y ya tenemos todo lo necesario para contarlas.

En la Sesión A seguimos una resolución desde la raíz hasta la respuesta, en una red propia y en el Internet real. Leemos los mensajes byte por byte, y vemos por qué DNS usa UDP, cuándo usa TCP y cómo se lo ataca. En la Sesión B escribimos HTTP a mano, medimos lo que cuesta cada conexión, hacemos pasar las peticiones por un proxy con cache y miramos los frames de HTTP/2.

Hay dos clases de resultados en este capítulo. Lo que sale de nuestra red de laboratorio se repite: los RTT son los que le pusimos con tc, y los puertos, los ID y las fechas cambian en cada corrida. Lo que sale del Internet real es una foto de un día: los TTL, las direcciones y los servidores que ustedes vean serán otros.

Ruta de lectura.

Antes de clase (obligatorio, unos 15 a 20 minutos). Sesión A: La red de esta semana, Nombres y direcciones, La jerarquía, la introducción de Recursiva e iterativa, la de Cache y TTL, y Registros. Sesión B: la introducción de HTTP: petición y respuesta, con Métodos y Códigos de estado.

En clase. Sesión A: Glue: un problema de huevo y gallina, En el Internet real, Ver cómo baja el TTL, Elegir el TTL, El mensaje de DNS, byte por byte, Por qué UDP, y cuándo TCP y Ataques a DNS. Sesión B: Para qué sirve Host, HTTP no guarda estado; las cookies sí, Cuánto cuesta una página, Cache de HTTP y HTTP/2.

Después de clase. Repasen con el Resumen y los Ejercicios, y preparen Laboratorio: DNS paso a paso y HTTP por dentro y Archivos y make test. Opcional: Respuestas negativas, DoH y DoT: cifrar, Otros problemas y Lo que HTTP/2 no arregló. Para profundizar, las lecturas del libro y las gratuitas de la semana:

Este capítulo acompaña a Kurose y Ross, Computer Networking: A Top-Down Approach (9.ª ed.). Para la Sesión A, la sección 2.4 DNS: The Internet’s Directory Service, y el capítulo DNS de CS168. Complementarias: RFC 1034 y RFC 1035, DNS; RFC 4033, DNSSEC; y RFC 8484, DNS sobre HTTPS. Para la Sesión B, las secciones 2.2.1 a 2.2.6, de Overview of HTTP hasta HTTP/2, y el capítulo HTTP de CS168. Complementarias: RFC 9110, semántica de HTTP; RFC 9111, cache; RFC 9112, HTTP/1.1; y RFC 9113, HTTP/2. El código de la semana está en code/week-12/.

La red de esta semana

Todo corre en una sola topología, red/red.clab.yml. Es un Internet de bolsillo, con su propia raíz, su propio TLD y un sitio web:

  cliente    resolver     raiz      tld      auth      web     cache     killa
 10.212.1.10 10.212.2.10 .3.10     .4.10    .5.10     .6.10    .7.10     .8.10
     |           |         |         |        |         |        |         |
     +-----------+---------+---------r--------+---------+--------+---------+

 RTT desde r:  raiz, tld, auth, killa   20 ms     (con tc netem)
               web                      50 ms
               cliente, resolver, cache casi nada

Se levanta con make up, que además arranca los servidores:

Nodo Programa Qué hace
raiz dns_auth.py Autoritativo de la zona .
tld dns_auth.py Autoritativo de test.
auth dns_auth.py Autoritativo de wayra.test.
resolver dns_resolver.py Resolver recursivo, con cache
web mini_web.py Un sitio de juguete, guardado en memoria
cache mini_cache.py Un proxy con cache
killa dns_auth.py Vacío al inicio: es el del laboratorio

Usamos el TLD .test porque está reservado para pruebas: nunca va a existir en el DNS real. Los servidores de DNS y HTTP están escritos en Python, en unas pocas páginas, para que se puedan leer. No son para producción. El único programa en C de la semana es dns_ask.c, que arma una pregunta a mano.

Nombres y direcciones

Una dirección IP dice dónde está algo en la red: es lo que un router necesita para hacer forwarding, y por eso tiene estructura de prefijos. Un nombre dice qué es algo, y su estructura le sirve a la gente que lo administra.

Esa diferencia no es solo comodidad. Separar el nombre de la dirección permite cambiar una sin tocar la otra:

  • El sitio se muda a otro proveedor y cambia de dirección, pero sigue llamándose igual.
  • Un mismo nombre puede dar varias direcciones, para repartir la carga entre servidores.
  • Varios nombres pueden vivir en una misma dirección, como veremos con Host.
  • Un nombre puede dar respuestas distintas según quién pregunte. Así funcionan las CDN, que veremos la próxima semana.

Definición. DNS (Domain Name System) es una base de datos distribuida y jerárquica, junto con el protocolo para consultarla. Dado un nombre y un tipo de registro, devuelve los registros que corresponden.

Lo de “traducir nombres” es solo el caso más común. DNS también sirve para:

  • Alias: un nombre que apunta a otro, con un registro CNAME.
  • Correo: a qué servidor se le entrega el correo de un dominio, con un registro MX.
  • Reparto de carga: varias direcciones para un mismo nombre.
  • Datos varios: con registros TXT se publican, por ejemplo, las reglas sobre quién puede mandar correo en nombre de un dominio.

Por qué no un solo servidor

En los primeros años de ARPANET, todos los nombres estaban en un archivo, HOSTS.TXT, y cada máquina bajaba una copia. Funcionó mientras eran pocas. Un diseño centralizado se cae por cuatro lados: es un punto único de falla, concentra todo el tráfico, queda lejos de casi todos, y una sola organización tendría que mantener todos los nombres del mundo.

DNS reparte el problema de dos maneras. Reparte la autoridad: cada organización administra sus propios nombres. Y reparte la carga: las respuestas se guardan en cache cerca de quien pregunta, así que la mayoría de las preguntas nunca llega lejos.

La jerarquía

Los nombres se leen de derecha a izquierda, de lo más general a lo más específico. Cada parte entre puntos es una etiqueta. El nombre completo termina en un punto, que representa a la raíz, aunque casi siempre se omite al escribirlo:

                 www.uni.edu.pe.
                                ^  raíz
                             ^^    TLD (top-level domain)
                         ^^^       edu.pe, administrado por el registro de .pe
                     ^^^           uni.edu.pe, administrado por la UNI
                 ^^^               un nombre dentro de esa zona

Se forma un árbol, y cada organización se encarga de un pedazo:

Definición. Una zona es la parte del árbol de nombres que administra una misma autoridad. Quien administra una zona puede delegar una parte de abajo a otra organización, y esa parte pasa a ser otra zona.

Árbol de nombres, sombreado según quién responde por cada parte. De la raíz salen com, pe y test, cada uno por una delegación. Bajo com está google, con sus propios servidores. Los servidores de pe atienden también com.pe y edu.pe, y de edu sale por delegación uni.edu.pe, con uni y www. De test sale por delegación wayra.test, con wayra, www y correoÁrbol de nombres, sombreado según quién responde por cada parte. De la raíz salen com, pe y test, cada uno por una delegación. Bajo com está google, con sus propios servidores. Los servidores de pe atienden también com.pe y edu.pe, y de edu sale por delegación uni.edu.pe, con uni y www. De test sale por delegación wayra.test, con wayra, www y correo
El árbol de nombres y quién responde por cada parte. Cada arista granate lleva a otros servidores. Los de pe. atienden también edu.pe, y por eso la delegación de uni.edu.pe sale directamente de ellos, como veremos en el Internet real.

Hay tres clases de servidores en la jerarquía:

Servidor De qué responde Qué contesta por www.uni.edu.pe
Raíz La zona . “No sé; los de .pe son estos”
TLD Una zona como pe. o com. “No sé; los de uni.edu.pe son estos”
Autoritativo Los nombres de una organización “Es 45.80.81.43”, y lo dice con autoridad

Existen 13 nombres de servidores raíz, de a.root-servers.net a m.root-servers.net. Eso no significa 13 máquinas. Como vimos en la semana 6, cada letra es una dirección anycast con muchas copias repartidas por el mundo.

Error común. Creer que el servidor raíz conoce todos los nombres. Conoce muy poco: la lista de TLD y quién responde por cada uno. Su trabajo es mandarlos al siguiente nivel. Por eso una sola zona chica aguanta preguntas de todo el planeta.

El resolver

Hay un cuarto actor que no está en el árbol, y es el que hace el trabajo.

Definición. Un resolver recursivo recibe preguntas de los clientes, las responde desde su cache si puede, y si no, recorre la jerarquía preguntándole a cada nivel. No tiene autoridad sobre ningún nombre: solo busca y recuerda.

Su computadora tiene un programa mínimo, el stub resolver, que solo sabe preguntarle al resolver configurado. Ese resolver suele ser el de su proveedor o de su universidad, y su dirección normalmente llega por DHCP. También hay resolvers públicos, como 8.8.8.8 y 1.1.1.1.

El único dato que el resolver necesita de fábrica son las direcciones de los servidores raíz. A partir de ahí, descubre todo lo demás. Nuestro dns_resolver.py recibe la dirección de la raíz como argumento.

Recursiva e iterativa

Hay dos maneras de preguntar:

  • Consulta recursiva: “tráigame la respuesta final”. Es lo que le pide un cliente a su resolver, con el bit RD (recursion desired) en 1.
  • Consulta iterativa: “dígame lo que sepa”. Es lo que el resolver le pide a cada servidor de la jerarquía. Si el servidor no tiene la respuesta, devuelve una referral: una remisión a quienes saben más.
Animación de una resolución con el cliente, el resolver y tres servidores: raíz, TLD y auth. El cliente pide www.wayra.test con una consulta recursiva. El resolver pregunta a la raíz, que manda a ns1.nic.test con glue; al TLD, que manda a ns1.wayra.test con glue; y a auth, que responde 10.212.6.10 con el flag aa. Cada delegación y la respuesta entran en la cache con su TTL: 86400, 3600 y 300 segundos. El resolver contesta al cliente, y en frío tardó 66 ms. La misma pregunta repetida sale de la cache en 0 ms sin tocar ningún servidor. Por último, correo.wayra.test va directo a auth, porque la delegación de wayra.test ya está en cacheAnimación de una resolución con el cliente, el resolver y tres servidores: raíz, TLD y auth. El cliente pide www.wayra.test con una consulta recursiva. El resolver pregunta a la raíz, que manda a ns1.nic.test con glue; al TLD, que manda a ns1.wayra.test con glue; y a auth, que responde 10.212.6.10 con el flag aa. Cada delegación y la respuesta entran en la cache con su TTL: 86400, 3600 y 300 segundos. El resolver contesta al cliente, y en frío tardó 66 ms. La misma pregunta repetida sale de la cache en 0 ms sin tocar ningún servidor. Por último, correo.wayra.test va directo a auth, porque la delegación de wayra.test ya está en cache
El cliente hace una sola pregunta y el resolver hace el recorrido. La cache guarda las respuestas y también las delegaciones, y por eso los servidores de arriba reciben tan pocas preguntas.

Para ver el recorrido real, seguimos el camino en nuestra red. dig +trace hace lo que haría un resolver, y muestra cada paso. Esto se corre en cliente:

$ dig +trace www.wayra.test

; <<>> DiG 9.18.49 <<>> +trace www.wayra.test
;; global options: +cmd
.			86400	IN	NS	a.raiz.test.
;; Received 41 bytes from 10.212.2.10#53(10.212.2.10) in 44 ms

test.			86400	IN	NS	ns1.nic.test.
;; Received 70 bytes from 10.212.3.10#53(a.raiz.test) in 22 ms

wayra.test.		3600	IN	NS	ns1.wayra.test.
;; Received 66 bytes from 10.212.4.10#53(ns1.nic.test) in 22 ms

www.wayra.test.		300	IN	A	10.212.6.10
;; Received 48 bytes from 10.212.5.10#53(ns1.wayra.test) in 21 ms

Se lee de arriba abajo: el resolver le da la lista de raíces (.), la raíz manda a test., test. manda a wayra.test., y ese responde. Cada salto a un servidor cuesta unos 22 ms, los 20 de netem más lo que tarda Python.

Ahora a mano, con +norecurse, pregunta por pregunta. Primero a la raíz:

$ dig @10.212.3.10 +norecurse www.wayra.test
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 48816
;; flags: qr; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

;; QUESTION SECTION:
;www.wayra.test.			IN	A

;; AUTHORITY SECTION:
test.			86400	IN	NS	ns1.nic.test.

;; ADDITIONAL SECTION:
ns1.nic.test.		86400	IN	A	10.212.4.10

Noten tres cosas. ANSWER: 0: no hay respuesta. No es un error: status: NOERROR. Y hay dos secciones más: authority, que dice quién responde por test., y additional, que regala la dirección de ese servidor. Con ella seguimos:

$ dig @10.212.4.10 +norecurse www.wayra.test
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 10521
;; flags: qr; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

;; AUTHORITY SECTION:
wayra.test.		3600	IN	NS	ns1.wayra.test.

;; ADDITIONAL SECTION:
ns1.wayra.test.		3600	IN	A	10.212.5.10
$ dig @10.212.5.10 +norecurse www.wayra.test
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 31625
;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; ANSWER SECTION:
www.wayra.test.		300	IN	A	10.212.6.10

Aparece el flag aa, authoritative answer. Lo da solo quien administra la zona. Las referrals anteriores no lo llevan, y las respuestas de un resolver tampoco.

Glue: un problema de huevo y gallina

Miren la delegación de wayra.test.: su servidor se llama ns1.wayra.test.. Para saber su dirección hay que preguntarle a… el servidor de wayra.test., que es él mismo. Sin ayuda, es un ciclo.

Definición. Un registro de glue es la dirección de un servidor de nombres, que la zona padre incluye junto con la delegación. Es indispensable cuando el nombre del servidor está dentro de la zona que se delega.

Lo probamos quitando el glue de la zona del TLD, en una delegación nueva. El padre dice quién responde, pero no cómo llegar:

$ dig @10.212.4.10 +norecurse www.killa.test +noall +authority +additional
killa.test.		3600	IN	NS	ns1.killa.test.

$ dig www.killa.test
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 53848
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0

Si el servidor tiene un nombre de otra zona, el glue no hace falta: el resolver resuelve ese nombre aparte, como cualquier otro. Lo comprobamos delegando killa.test. en un nombre de wayra.test., sin glue, y la resolución funcionó: el log de auth muestra la pregunta adicional por ese servidor.

Error común. Decir que un SERVFAIL significa que “el nombre no existe”. Son cosas distintas. NXDOMAIN es una respuesta firme: el nombre no existe. SERVFAIL dice que el resolver no pudo averiguarlo. Aquí, el nombre existe y hay un servidor que lo sabe, pero nadie puede llegar a él.

En el Internet real

Hagamos lo mismo con un nombre .pe, el 2 de noviembre de 2026, desde el contenedor:

$ dig +trace +nodnssec www.uni.edu.pe
.			4502	IN	NS	e.root-servers.net.
.			4502	IN	NS	l.root-servers.net.
                         (... las otras once letras ...)
;; Received 420 bytes from 192.168.65.7#53(192.168.65.7) in 103 ms

pe.			172800	IN	NS	a.lactld.org.
pe.			172800	IN	NS	pch.rcp.pe.
pe.			172800	IN	NS	pe1.dnsnode.net.
pe.			172800	IN	NS	quipu.rcp.net.pe.
;; Received 296 bytes from 199.7.83.42#53(l.root-servers.net) in 70 ms

uni.edu.pe.		3600	IN	NS	rngcyt.uni.edu.pe.
uni.edu.pe.		3600	IN	NS	rpcyt.uni.edu.pe.
uni.edu.pe.		3600	IN	NS	dns.uni.edu.pe.
;; Received 178 bytes from 204.61.216.85#53(pch.rcp.pe) in 6 ms

www.uni.edu.pe.		7200	IN	A	45.80.81.43
;; Received 59 bytes from 45.80.81.12#53(rngcyt.uni.edu.pe) in 8 ms

192.168.65.7 es el resolver interno de Docker Desktop; en su laptop verán otro. Noten que el servidor de .pe que nos respondió entregó directamente la delegación de uni.edu.pe., sin un paso aparte para edu.pe.. Eso no quiere decir que edu.pe. no sea una zona. Tiene su propio SOA:

$ dig @quipu.rcp.net.pe +norecurse +noall +answer SOA edu.pe
edu.pe.			3600	IN	SOA	quipu.rcp.net.pe. operador.rcp.pe. 2026110221 1800 300 604800 1800

La atienden los mismos servidores que a pe., así que responden de una vez: un punto más en el nombre no siempre es una consulta más. El árbol de nombres y las consultas que hace un resolver no son lo mismo.

Los servidores de .pe también se pueden preguntar a mano, y traen su glue:

$ dig @199.7.83.42 +norecurse +noall +authority +additional www.uni.edu.pe A
pe.			172800	IN	NS	a.lactld.org.
pe.			172800	IN	NS	pch.rcp.pe.
pe.			172800	IN	NS	pe1.dnsnode.net.
pe.			172800	IN	NS	quipu.rcp.net.pe.
a.lactld.org.		172800	IN	A	200.0.68.10
pch.rcp.pe.		172800	IN	A	204.61.216.85
pe1.dnsnode.net.	172800	IN	A	194.146.106.82
quipu.rcp.net.pe.	172800	IN	A	200.1.176.4

Aquí se ven los dos casos de glue. pch.rcp.pe y quipu.rcp.net.pe están dentro de pe., así que su glue es indispensable. a.lactld.org y pe1.dnsnode.net están en otras zonas: la raíz también entrega sus direcciones, pero un resolver podría averiguarlas por su cuenta.

Cache y TTL

Si cada resolución recorriera la jerarquía, los servidores raíz no darían abasto y cada página tardaría mucho más. Por eso cada registro lleva un TTL (time to live): la cantidad de segundos durante los que se lo puede recordar.

Definición. El TTL de un registro es el tiempo máximo, en segundos, que un resolver puede guardarlo en cache. Lo decide quien administra la zona.

Error común. Confundirlo con el TTL del encabezado de IP. Tienen el mismo nombre, pero nada más. El de IP cuenta saltos y evita que un paquete dé vueltas para siempre. El de DNS cuenta segundos y dice cuánto dura un dato en cache.

Medimos el efecto de la cache con cuatro repeticiones, reiniciando el resolver antes de cada una para vaciarla. La primera columna es la pregunta en frío y la segunda, la repetición inmediata:

 en frío     repetida
  66 ms       0 ms
  66 ms       0 ms
  71 ms       1 ms
  66 ms       0 ms

Tres RTT de 20 ms dan 60 ms de piso, y el resto es Python. La primera consulta justo después de desplegar la red tardó bastante más, 129 y 132 ms en dos corridas, porque también paga el arranque de Python. Los logs de los servidores confirman la explicación: en la pregunta en frío, cada uno recibe una; en la repetida, ninguno. Estas son las últimas líneas de cada log, tras una consulta:

raiz:      udp 10.212.2.10:43857 id=14905 www.wayra.test. A -> NOERROR an=0 ns=1 ar=1
tld:       udp 10.212.2.10:42836 id=16337 www.wayra.test. A -> NOERROR an=0 ns=1 ar=1
auth:      udp 10.212.2.10:60289 id=30730 www.wayra.test. A -> NOERROR an=1 ns=0 ar=0
resolver:  10.212.1.10:46037 id=9193 www.wayra.test. A -> NOERROR an=1

La cache no guarda solo la respuesta final, sino también las delegaciones. Después de esa consulta, al preguntar por correo.wayra.test el resolver va directo a auth, sin molestar a la raíz ni al TLD. Así, los servidores de arriba reciben muchas menos preguntas de lo que parece. La prueba test_after_learning_the_tld_a_new_name_skips_the_root lo comprueba.

Ver cómo baja el TTL

El registro rapido.wayra.test tiene un TTL de 4 segundos a propósito:

$ dig +noall +answer rapido.wayra.test      # primera vez
rapido.wayra.test.	4	IN	A	10.212.6.11
$ sleep 2; dig +noall +answer rapido.wayra.test
rapido.wayra.test.	2	IN	A	10.212.6.11
$ sleep 3; dig +noall +answer rapido.wayra.test
rapido.wayra.test.	4	IN	A	10.212.6.11

El número baja en el resolver: un TTL que llega a cero significa que ya no se puede usar, y la tercera consulta lo vuelve a pedir al autoritativo. El log de auth muestra exactamente dos preguntas por ese nombre.

En la práctica. En un resolver real, el TTL puede no bajar tan prolijamente. Le preguntamos dos veces a 8.8.8.8 por www.uni.edu.pe, separadas por 3 segundos, y vimos 7123 y luego 2860. A 1.1.1.1, tres veces en 6 segundos, y vimos 7200 las tres. Una sola dirección de un resolver público es un anycast con muchas máquinas detrás, y no sabemos a cuál de sus caches le toca cada pregunta. Por eso el ejemplo limpio lo hacemos en nuestra red.

Elegir el TTL

Es un compromiso entre dos costos:

TTL Ventajas Riesgos
Corto (segundos a minutos) Un cambio se nota rápido. Permite repartir tráfico y mudarse en caso de falla Más preguntas, más carga en los autoritativos, y resoluciones más lentas. Si el autoritativo cae, los nombres dejan de resolver pronto
Largo (horas o días) Menos carga y más rapidez. Aguanta una caída del autoritativo Un error o una mudanza tarda en corregirse: el dato viejo sigue en caches del mundo entero

La técnica habitual antes de una mudanza es bajar el TTL unos días antes, hacer el cambio, y volver a subirlo después.

Error común. Hablar de “propagación de DNS”, como si el cambio viajara por la red. Nada se propaga: cada resolver sigue usando su copia hasta que le venza el TTL, y recién ahí vuelve a preguntar. Quien cambia un registro con TTL de un día puede ver respuestas viejas durante hasta un día.

Respuestas negativas

“Este nombre no existe” también se guarda en cache. Si no, un nombre mal escrito haría repetir toda la consulta cada vez:

$ dig noexiste.wayra.test
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 40207
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 0

;; AUTHORITY SECTION:
wayra.test.		60	IN	SOA	ns1.wayra.test. admin.wayra.test. 2026110201 3600 600 86400 60

El registro SOA en authority dice cuánto dura esta negativa: el menor entre su propio TTL y su último campo, aquí 60 segundos. Hay una negativa más sutil: el nombre existe, pero no tiene registros de ese tipo. Es NOERROR con ANSWER: 0, y a veces se la llama NODATA:

$ dig +noall +answer +comments wayra.test AAAA
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 20741
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 0

Registros

Un registro (resource record) tiene cinco campos: nombre, TTL, clase, tipo y datos. La clase casi siempre es IN, de Internet. Por ejemplo:

 www.wayra.test.    300    IN    A    10.212.6.10
 nombre             TTL  clase  tipo  datos

Los tipos de esta semana:

Tipo Datos Para qué
A Una dirección IPv4 Traducir un nombre
AAAA Una dirección IPv6 Lo mismo, en IPv6
NS El nombre de un servidor Quién responde por esta zona
CNAME Otro nombre Este nombre es un alias de aquel
MX Preferencia y nombre de un servidor A quién entregarle el correo
TXT Texto libre Datos para otros sistemas
SOA Datos de la zona Inicio de autoridad. Sirve para la cache negativa, entre otras cosas

Nuestra zona, red/zones/wayra.zone, está escrita en un formato simplificado:

wayra.test. 3600 SOA ns1.wayra.test. admin.wayra.test. 2026110201 3600 600 86400 60
wayra.test. 3600 NS ns1.wayra.test.
ns1.wayra.test. 3600 A 10.212.5.10
www.wayra.test. 300 A 10.212.6.10
www.wayra.test. 300 AAAA 2001:db8:212:6::10
blog.wayra.test. 300 CNAME www.wayra.test.
wayra.test. 3600 MX 10 correo.wayra.test.
correo.wayra.test. 3600 A 10.212.6.25
wayra.test. 3600 TXT "curso de redes: nombres de ejemplo"

Y así se ven al consultarlos:

$ dig +noall +answer www.wayra.test AAAA
www.wayra.test.		300	IN	AAAA	2001:db8:212:6::10
$ dig +noall +answer wayra.test NS
wayra.test.		3600	IN	NS	ns1.wayra.test.
$ dig +noall +answer wayra.test MX
wayra.test.		3600	IN	MX	10 correo.wayra.test.
$ dig +noall +answer wayra.test TXT
wayra.test.		3600	IN	TXT	"curso de redes: nombres de ejemplo"
$ dig +noall +answer blog.wayra.test
blog.wayra.test.	300	IN	CNAME	www.wayra.test.
www.wayra.test.		300	IN	A	10.212.6.10

Con un CNAME, el resolver sigue la cadena y devuelve las dos cosas: el alias y lo que encontró al final. En el MX, un número de preferencia menor significa más preferido. Veamos un caso real, el correo de la UNI, con varios servidores para tener respaldo:

$ dig +noall +answer uni.edu.pe MX
uni.edu.pe.		4502	IN	MX	5 ALT1.ASPMX.L.GOOGLE.COM.
uni.edu.pe.		4502	IN	MX	10 ALT4.ASPMX.L.GOOGLE.COM.
uni.edu.pe.		4502	IN	MX	5 ALT2.ASPMX.L.GOOGLE.COM.
uni.edu.pe.		4502	IN	MX	10 ALT3.ASPMX.L.GOOGLE.COM.
uni.edu.pe.		4502	IN	MX	1 ASPMX.L.GOOGLE.COM.

Ese mismo día, uni.edu.pe no devolvía ningún registro AAAA: no todos los nombres tienen IPv6.

Error común. Creer que el MX da la dirección de correo de alguien o que apunta a una dirección IP. El MX da el nombre de un servidor, y ese nombre necesita su propio A o AAAA. Por eso nuestra zona tiene correo.wayra.test. aparte. Tampoco se debe poner un CNAME donde ya hay otros registros: un nombre con CNAME no puede tener nada más, y por eso la raíz de un dominio, que siempre lleva NS y SOA, no puede ser un CNAME.

El mensaje de DNS, byte por byte

Preguntas y respuestas tienen el mismo formato, definido en la sección 4 del RFC 1035:

Los 48 bytes de una respuesta real, agrupados por campo. Encabezado de 12 bytes: ID 59 55, flags 81 80, y los contadores: 1 pregunta, 1 respuesta, 0 de autoridad y 0 adicionales. Pregunta desde el byte 12: 03 www, 05 wayra, 04 test y el 00 de la raíz, seguidos de tipo A y clase IN. Respuesta desde el byte 32: el puntero c0 0c, con una flecha hacia el byte 12, tipo A, clase IN, TTL 299, largo 4 y la dirección 10.212.6.10Los 48 bytes de una respuesta real, agrupados por campo. Encabezado de 12 bytes: ID 59 55, flags 81 80, y los contadores: 1 pregunta, 1 respuesta, 0 de autoridad y 0 adicionales. Pregunta desde el byte 12: 03 www, 05 wayra, 04 test y el 00 de la raíz, seguidos de tipo A y clase IN. Respuesta desde el byte 32: el puntero c0 0c, con una flecha hacia el byte 12, tipo A, clase IN, TTL 299, largo 4 y la dirección 10.212.6.10
Una respuesta real, que capturamos enseguida. Encabezado de 12 bytes y luego cuatro secciones: pregunta, respuestas, autoridad y adicionales, con cuántos registros trae cada una según los contadores.

Para verlo sin ninguna biblioteca, dns_ask.c arma la pregunta a mano, campo por campo y en orden de red, como dice la guía de estilo:

write_u16(query, id);
write_u16(query + 2, argc == 4 ? 0x0000 : 0x0100);  // RD pide recursión
write_u16(query + 4, 1);                            // una pregunta
int name_size = encode_name(argv[2], query + HEADER_SIZE, sizeof query - HEADER_SIZE - 4);

Mientras corre, capturamos en cliente:

$ /w12/bin/dns_ask 10.212.2.10 www.wayra.test
id=22869 qr=1 aa=0 tc=0 rd=1 ra=1 rcode=0
preguntas=1 respuestas=1 autoridad=0 adicionales=0
www.wayra.test. ttl=299 A 10.212.6.10

Y en la captura, leída con tshark -x, la pregunta y luego la respuesta, a partir del byte 0x2a, donde terminan los encabezados de Ethernet, IP y UDP:

pregunta (desde 0x2a):
                                 59 55 01 00 00 01
  00 00 00 00 00 00 03 77 77 77 05 77 61 79 72 61
  04 74 65 73 74 00 00 01 00 01

respuesta (desde 0x2a):
                                 59 55 81 80 00 01
  00 01 00 00 00 00 03 77 77 77 05 77 61 79 72 61
  04 74 65 73 74 00 00 01 00 01 c0 0c 00 01 00 01
  00 00 01 2b 00 04 0a d4 06 0a

Lo leemos:

Bytes Campo Significado
59 55 ID 0x5955 = 22869, el mismo que imprimió dns_ask
01 00 flags de la pregunta Solo RD en 1
81 80 flags de la respuesta QR, RD y RA en 1, rcode 0
00 01 preguntas 1
03 77 77 77 etiqueta Largo 3 y www
05 77 61 79 72 61 etiqueta Largo 5 y wayra
04 74 65 73 74 00 etiqueta y fin Largo 4, test, y el 0 de la raíz
00 01 00 01 tipo y clase A e IN
c0 0c nombre en la respuesta Un puntero
00 00 01 2b TTL 299
00 04 largo de los datos 4 bytes
0a d4 06 0a dirección 10.212.6.10

Una lección de formato: los nombres van con el largo delante de cada etiqueta, no con puntos. Y c0 0c es compresión: los dos primeros bits en 1 indican un puntero, y el resto es la posición donde sigue el nombre. 0x0c es 12, justo después del encabezado, donde ya estaba escrito. Dos bytes en vez de dieciséis.

Esto se presta a errores graves: un puntero que apunta a sí mismo haría entrar en un ciclo sin fin a un programa descuidado. Nuestro decode_name solo acepta punteros hacia atrás, y hay una prueba que lo verifica. La regla es general: nunca confíen en lo que llega de la red.

También comprobamos que dig y la captura dicen lo mismo, con otra consulta. El ID 64226 que muestra dig es el 0xfae2 que muestra tshark:

$ dig blog.wayra.test +noall +comments +answer
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 64226
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0

$ tshark -r dns.pcap -T fields -e dns.id -e dns.flags.response -e dns.qry.name -e dns.a -e dns.cname
0xfae2	False	blog.wayra.test
0xfae2	True	blog.wayra.test	10.212.6.10	www.wayra.test

Por qué UDP, y cuándo TCP

DNS corre sobre el puerto 53, normalmente en UDP. La razón es la que vimos en la semana 9: una pregunta y una respuesta chicas. Con TCP habría que gastar un RTT de handshake antes de preguntar, y un resolver tendría que guardar el estado de miles de conexiones. Si se pierde un datagrama, el cliente vuelve a preguntar después de un timeout. Como la operación no cambia nada en el servidor, repetir es inofensivo: la confiabilidad la pone la aplicación, y le sale barata.

El límite histórico es de 512 bytes por UDP (RFC 1035). ¿Y si la respuesta no cabe? El servidor manda lo que puede, marca el flag TC (truncated), y el cliente repite por TCP, también en el puerto 53. Nuestro registro grande.wayra.test existe para eso. Sin dejar que dig reintente:

$ dig @10.212.5.10 grande.wayra.test TXT +ignore
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 23888
;; flags: qr aa tc rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0

Y dejándolo, a través del resolver:

$ dig grande.wayra.test TXT
;; Truncated, retrying in TCP mode.
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 46062
;; SERVER: 10.212.2.10#53(10.212.2.10) (TCP)
;; MSG SIZE  rcvd: 1774

El log de auth muestra los dos intentos del resolver, primero por UDP y después por TCP:

udp 10.212.2.10:42285 id=32547 grande.wayra.test. TXT -> NOERROR an=1 ns=0 ar=0 TC (1774 bytes)
tcp 10.212.2.10:50308 id=29817 grande.wayra.test. TXT -> NOERROR an=1 ns=0 ar=0

Por TCP, cada mensaje lleva adelante su largo en 2 bytes. Es el “largo al inicio” que vimos en la semana 2: un stream no conserva los límites de los mensajes, y hay que marcarlos.

En la práctica. Hoy, 512 bytes es poco. EDNS es una extensión con la que el cliente anuncia que acepta respuestas más grandes por UDP. El [1au] que muestra tcpdump en las preguntas de dig es ese registro adicional, de tipo OPT. Una respuesta muy grande por UDP se puede fragmentar en IP, y varios fragmentos se pierden o se filtran con más facilidad. Por eso los valores que se usan hoy rondan los 1232 bytes, y se recurre a TCP para lo demás. Un servidor de DNS que bloquea el puerto 53 de TCP en su firewall está mal configurado: fallará justamente con las respuestas grandes. Nuestros servidores no implementan EDNS, y un servidor sin EDNS puede contestar sin él.

Ataques a DNS

El protocolo se diseñó en una red donde todos se conocían. Una respuesta de DNS no trae ninguna prueba de quién la escribió, y por UDP no hay handshake: cualquiera puede mandar un datagrama con la dirección de origen de otro (spoofing). Esto abre varios problemas.

Envenenamiento de cache

Definición. El envenenamiento de cache ocurre cuando un resolver acepta y guarda una respuesta falsa. A partir de ahí, todos sus clientes reciben la dirección equivocada hasta que venza el TTL.

La idea del ataque es una carrera. Un resolver le hace una pregunta a un autoritativo y queda esperando. Durante esos milisegundos, un atacante intenta que le llegue una respuesta falsa antes que la verdadera, con la dirección de origen del autoritativo. Lo que protege al resolver es lo que el atacante no puede ver si no está en el camino: una respuesta solo se acepta si coincide en los datos de la pregunta.

Este es el chequeo de nuestro resolver, en dns_resolver.py:

if peer[0] != server or not reply.flags & w.FLAG_QR or reply.msg_id != msg_id:
    return False
if len(reply.question) != 1:
    return False
name, rtype = reply.question[0]
return w.normalize(name) == w.normalize(question[0]) and rtype == question[1]

Y el puerto lo revisa el kernel, porque el socket está conectado al servidor.

Veamos cuánto aporta cada campo. El atacante conoce el nombre (a veces lo provoca él mismo, pidiéndolo) y las direcciones. Le falta adivinar:

Lo que adivina Combinaciones Si manda 1000 respuestas falsas en la carrera
Solo el ID, 16 bits, con puerto fijo 65 536 1000 / 65 536 = 1,5 %
ID y puerto al azar, entre 32768 y 60999 65 536 × 28 232 = 1 850 212 352 1000 / 1 850 212 352 = 0,000054 %

El rango de puertos es el que usa por defecto el kernel de nuestros nodos (/proc/sys/net/ipv4/ip_local_port_range da 32768 60999).

Esto se ve en una captura en resolver, con cuatro preguntas seguidas hacia auth. Cambian tanto el puerto de origen como el ID:

IP 10.212.2.10.52220 > 10.212.5.10.53: 27211 A? uno.wayra.test. (32)
IP 10.212.2.10.58027 > 10.212.5.10.53: 39509 A? dos.wayra.test. (32)
IP 10.212.2.10.51143 > 10.212.5.10.53: 51393 A? tres.wayra.test. (33)
IP 10.212.2.10.33632 > 10.212.5.10.53: 20125 A? cuatro.wayra.test. (35)

Hay otra defensa, más antigua: la regla de bailiwick. Un servidor solo tiene autoridad sobre su zona, así que nuestro resolver descarta las direcciones de nombres que estén fuera de la zona delegada (usable_glue). Sin esa regla, cualquier respuesta podría colar de paso un dato sobre un banco. Y mientras un dato esté en cache no hay pregunta y no hay carrera, de modo que el TTL parecía limitar los intentos a uno cada tanto.

En 2008, Dan Kaminsky mostró cómo saltarse ese límite respetando el bailiwick. Se provocan preguntas por nombres inventados y distintos de un mismo dominio, que nunca están en cache, y la respuesta falsificada trae en su sección de autoridad una delegación falsa de ese mismo dominio. Como el dato pertenece a la zona, pasa el chequeo. Con el TTL fuera del camino, solo quedaba el tamaño de lo que había que adivinar, y por eso los resolvers del mundo adoptaron rápidamente el puerto al azar.

Idea clave. El ID y el puerto al azar no autentican nada. Solo hacen muy difícil adivinar, y solo frente a quien no ve el tráfico. Quien está en el camino, por ejemplo en una red Wi-Fi abierta, ve el ID y el puerto, y puede responder primero sin adivinar nada. Para eso hace falta criptografía.

DNSSEC: autenticar

DNSSEC agrega firmas digitales a los registros. Cada zona firma sus registros (RRSIG) con una clave que publica (DNSKEY). La zona padre firma un resumen de la clave de la hija (DS), y así se forma una cadena hasta la raíz, cuya clave ya conoce el resolver que valida. Una respuesta falsa no tendría una firma válida, sin importar por dónde llegue.

Lo vemos con un resolver público. El flag ad significa authenticated data, y la respuesta trae su firma:

$ dig @1.1.1.1 +dnssec +noall +comments +answer pe. SOA
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
pe.			3600	IN	SOA	quipu.rcp.net.pe. operador.rcp.pe. 2026110208 1800 300 604800 1800
pe.			3600	IN	RRSIG	SOA 8 1 3600 20261202125006 20261102125006 37239 pe. tcLfh75sKDrqiRfuy0tc5JI3OOb...

delv valida la cadena por su cuenta, y hace ver que no todas las zonas están firmadas:

$ delv @1.1.1.1 pe. SOA
; fully validated
$ delv @1.1.1.1 uni.edu.pe. A
; unsigned answer
uni.edu.pe.		7200	IN	A	45.80.81.43

.pe está firmada; uni.edu.pe, ese día, no. DNSSEC solo protege lo que se firmó.

DoH y DoT: cifrar

Lo que autentica no oculta, y lo que oculta no autentica. DoT (DNS sobre TLS, puerto 853) y DoH (DNS sobre HTTPS, puerto 443) cifran el camino entre el cliente y el resolver. Veremos TLS la próxima semana; por ahora basta saber que quien mira la red ya no puede leer los mensajes.

Error común. Creer que DNSSEC da privacidad. Solo autentica las respuestas: la consulta sigue viajando sin cifrar. El RFC 4033 lo dice explícitamente. Aquí capturamos la consulta DNSSEC de arriba, en el contenedor. El nombre se lee sin ningún problema, a pesar de la firma:

eth0  Out IP 192.168.65.3.61050 > 1.1.1.1.53: 29822+ [1au] SOA? pe. (43)
eth0  In  IP 1.1.1.1.53 > 192.168.65.3.61050: 29822$ 2/0/1 SOA, RRSIG (384)

La misma pregunta por DoH, con dig +https. Da la respuesta y usa otro puerto:

$ dig @1.1.1.1 +https +noall +answer +stats pe. SOA
pe.			3600	IN	SOA	quipu.rcp.net.pe. operador.rcp.pe. 2026110209 1800 300 604800 1800
;; SERVER: 1.1.1.1#443(1.1.1.1) (HTTPS)

Durante esa consulta, los 22 paquetes capturados hacia y desde 1.1.1.1 fueron TCP al puerto 443, y ninguno al 53. Se ve el handshake y se ven los tamaños, pero no el nombre.

Tampoco es privacidad total. El observador todavía ve a qué dirección se conecta después, y el resolver ve todas sus consultas: con DoH, ustedes eligen en quién confiar, no dejan de confiar. Lo comparamos en una tabla:

Autentica la respuesta Cifra la consulta Contra quién protege
ID y puerto al azar No No Un atacante que está fuera del camino
DNSSEC Sí, de extremo a extremo No Respuestas falsas, vengan de donde vengan
DoT y DoH Solo el canal hasta el resolver Sí, hasta el resolver Quien observa o altera la red local

Son complementarios: un resolver puede validar DNSSEC y a la vez responder por DoH.

Otros problemas

  • Amplificación: se manda una pregunta chica con la dirección de origen falsificada, la de la víctima, y un servidor le manda a ella una respuesta mucho más grande. En la respuesta .pe de arriba, la pregunta tuvo 43 bytes de DNS y la respuesta 384. Por eso un resolver no debe aceptar preguntas de todo Internet.
  • Ataques a la infraestructura: los servidores de DNS son un objetivo atractivo para un DoS. El anycast y la cache son las defensas principales.
  • DNS como punto único: en la semana 7, el incidente de Facebook mostró que un nombre perfecto no sirve si nadie puede llegar a sus servidores.

HTTP: petición y respuesta

Empieza la Sesión B. HTTP es un protocolo de petición y respuesta entre un cliente y un servidor, sobre un transporte confiable. Sus mensajes de HTTP/1.1 son texto, y se pueden escribir a mano. Con el servidor mini_web.py en la dirección 10.212.6.10, usamos nc y mandamos cada línea con su \r\n:

$ printf 'GET / HTTP/1.1\r\nHost: www.wayra.test\r\n\r\n' | nc -w1 10.212.6.10 80
HTTP/1.1 200 OK
Server: mini_web
Date: Mon, 02 Nov 2026 14:50:16 GMT
ETag: "d8a8038111f4"
Cache-Control: max-age=60
Content-Type: text/html
Content-Length: 67

<html><img src=/a.png><img src=/b.png><img src=/c.png>wayra</html>

Esa es toda la web. La forma general:

 petición                                respuesta
 GET / HTTP/1.1            línea inicial HTTP/1.1 200 OK
 Host: www.wayra.test      headers       Content-Type: text/html
 Accept: */*                             Content-Length: 67
 (línea vacía)             separador     (línea vacía)
 (cuerpo, si hay)          cuerpo        <html>...

La línea vacía termina los headers. Y un cuerpo se separa del siguiente mensaje con Content-Length: como TCP es un stream de bytes, sin esa cuenta el receptor no sabría dónde termina. Es el problema de los límites de mensaje, otra vez. (Si el tamaño no se conoce al inicio, HTTP/1.1 tiene Transfer-Encoding: chunked, que manda pedazos con el largo delante.)

Métodos

Método Para qué Seguro Idempotente
GET Traer un recurso Sí Sí
HEAD Lo mismo, solo los headers Sí Sí
POST Mandar datos para que se procesen No No
PUT Reemplazar un recurso No Sí
DELETE Borrar un recurso No Sí

Seguro significa que no pretende cambiar nada en el servidor. Idempotente significa que repetirlo tiene el mismo efecto que hacerlo una vez. Importa en una red que pierde cosas: si se corta la conexión antes de la respuesta, un GET se puede repetir sin miedo, pero repetir un POST puede pagar dos veces la misma compra. Es la misma razón por la que repetir una pregunta de DNS es inofensivo.

HEAD devuelve los mismos headers, sin cuerpo, con Content-Length diciendo cuánto habría sido:

$ curl -sI http://www.wayra.test/
HTTP/1.1 200 OK
Server: mini_web
Date: Mon, 02 Nov 2026 14:54:04 GMT
ETag: "d8a8038111f4"
Cache-Control: max-age=60
Content-Type: text/html
Content-Length: 67

Códigos de estado

El primer dígito da la clase; el cliente puede actuar con solo verlo:

Clase Significado Ejemplos
1xx Información, sigue 101 Switching Protocols
2xx Salió bien 200 OK, 204 No Content
3xx Hay que hacer algo más 301 Moved Permanently, 304 Not Modified
4xx Error de quien pidió 400 Bad Request, 404 Not Found, 405 Method Not Allowed
5xx Error del servidor 500 Internal Server Error, 501 Not Implemented, 503 Service Unavailable

Dos respuestas reales de mini_web:

$ printf 'GET /nada HTTP/1.1\r\nHost: a\r\n\r\n' | nc -w1 10.212.6.10 80
HTTP/1.1 404 Not Found
Server: mini_web
Date: Mon, 02 Nov 2026 14:54:04 GMT
Content-Length: 10

no existe

$ printf 'DELETE / HTTP/1.1\r\nHost: a\r\n\r\n' | nc -w1 10.212.6.10 80
HTTP/1.1 501 Unsupported method ('DELETE')
Server: mini_web
Date: Mon, 02 Nov 2026 14:54:04 GMT
Connection: close

No confundan 501 con 405. 501 significa que el servidor no implementa ese método en absoluto. 405 significa que lo conoce, pero que este recurso no lo permite, y debe decir cuáles sí con un header Allow.

Para qué sirve Host

Una sola dirección IP puede servir muchos sitios. Como en el transporte solo viajan direcciones y puertos, el servidor no sabría cuál de ellos quieren si la petición no lo dijera. Por eso HTTP/1.1 agrega el header Host:

$ printf 'GET / HTTP/1.1\r\nHost: blog.wayra.test\r\n\r\n' | nc -w1 10.212.6.10 80
HTTP/1.1 200 OK
Server: mini_web
Date: Mon, 02 Nov 2026 14:42:10 GMT
ETag: "f15455512b1f"
Cache-Control: max-age=60
Content-Type: text/html
Content-Length: 37

<html>el blog, en la misma IP</html>

Es la misma dirección y el mismo puerto de antes, con otra página. ¿Y si falta Host? El RFC 9112 es claro en su sección 3.2: un servidor debe responder 400 a una petición HTTP/1.1 sin Host. Probemos con un servidor real, desde el contenedor:

$ printf 'GET / HTTP/1.1\r\n\r\n' | nc -w3 example.com 80
HTTP/1.1 400 Bad Request
Server: cloudflare
Date: Mon, 02 Nov 2026 14:45:14 GMT
Content-Type: text/html
Content-Length: 155
Connection: close

Y con el nuestro:

$ printf 'GET / HTTP/1.1\r\n\r\n' | nc -w1 10.212.6.10 80
HTTP/1.1 200 OK
Server: mini_web
Date: Mon, 02 Nov 2026 14:42:10 GMT
ETag: "d8a8038111f4"
Cache-Control: max-age=60
Content-Type: text/html
Content-Length: 67

mini_web no cumple el estándar, a propósito: http.server de Python no revisa eso, y es un programa de juguete. Es un buen recordatorio. Que un servidor le conteste algo a un cliente no demuestra que ninguno de los dos cumpla el protocolo. Un programa que se publica en la red debe validar lo que recibe, y eso va a importar en su proyecto.

HTTP no guarda estado; las cookies sí

Definición. HTTP es un protocolo sin estado (stateless): cada petición se entiende sola, y el servidor no necesita recordar nada de las anteriores para responderla.

Esto simplifica mucho un servidor: se puede reiniciar o reemplazar por otro, y pueden atender miles de clientes sin guardar nada de cada uno. Pero una tienda necesita saber de quién es un carrito.

Una cookie resuelve eso sin cambiar el principio: el servidor pide guardar un dato con Set-Cookie, y el cliente lo devuelve en cada petición siguiente con Cookie. El estado viaja en los mensajes.

$ curl -s -D- http://www.wayra.test/visitas
HTTP/1.1 200 OK
Server: mini_web
Date: Mon, 02 Nov 2026 14:54:04 GMT
Set-Cookie: visitas=1; Path=/
Cache-Control: no-store
Content-Length: 9

visita 1

$ curl -s -H 'Cookie: visitas=7' http://www.wayra.test/visitas
visita 8

El servidor nos creyó el 7 sin discutir. Ese es el punto de seguridad: el cliente puede mandar lo que quiera, y por eso una cookie real no suele llevar el dato, sino un identificador largo al azar de una sesión que el servidor guarda aparte. Las cookies también sirven para seguir a alguien de un sitio a otro: por ahí viene la discusión sobre privacidad.

Error común. Decir que una cookie vuelve a HTTP un protocolo con estado. El protocolo sigue igual, y cada petición se entiende sola. El estado lo llevan la aplicación y el cliente.

Cuánto cuesta una página

Una página no es un archivo: es un HTML que referencia otros objetos. La nuestra tiene tres imágenes, y son cuatro objetos en total. ¿Cuánto tarda bajarla? Contemos con RTT, sin contar el tiempo de transmisión, que para objetos chicos es despreciable.

Un objeto por TCP nuevo cuesta 2 RTT: uno para el handshake (el ACK final ya puede llevar la petición) y otro para la petición y su respuesta.

 cliente                           servidor
    |---------- SYN ---------------->|   \
    |<--------- SYN+ACK -------------|    | 1 RTT: la conexión
    |---------- ACK + GET ---------->|   /\
    |<--------- 200 OK + datos ------|     | 1 RTT: el primer byte

Lo comprobamos con curl -w, que separa las etapas. El RTT hacia web es de 50 ms:

$ curl -s -o /dev/null -H 'Connection: close' \
    -w 'dns=%{time_namelookup} con=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total} conexiones=%{num_connects}\n' \
    http://www.wayra.test/
dns=0.024310 con=0.076927 ttfb=0.128040 total=0.128072 conexiones=1
dns=0.000710 con=0.052951 ttfb=0.104767 total=0.104800 conexiones=1
dns=0.000939 con=0.052268 ttfb=0.104289 total=0.104319 conexiones=1

Hay que leer los tiempos como acumulados desde el inicio, no como duraciones separadas. En todas las líneas, la conexión toma unos 52 ms y el primer byte llega unos 52 ms después: exactamente 1 RTT y 2 RTT, más lo que tome resolver el nombre.

¿Y los 24 ms de resolución de la primera línea, de tres corridas seguidas? El registro A ya estaba en cache. Pero curl pregunta por A y también por AAAA, y el AAAA no estaba. Lo comprobamos repitiendo en una red recién levantada: primero un dig del registro A, luego curl, y el log de auth muestra una pregunta de cada tipo. La primera resolución volvió a tardar 25 ms, y las siguientes, un milisegundo.

udp 10.212.2.10:33316 id=63411 www.wayra.test. A -> NOERROR an=1 ns=0 ar=0
udp 10.212.2.10:51867 id=33834 www.wayra.test. AAAA -> NOERROR an=1 ns=0 ar=0

No persistente contra persistente

Definición. Con conexiones no persistentes, cada objeto usa su propia conexión TCP, que se cierra al terminar. Con conexiones persistentes, la misma conexión queda abierta para varias peticiones. HTTP/1.1 usa persistentes por defecto; para cerrar, alguien lo pide con Connection: close.

Con el HTML y 3 imágenes, de a uno:

Modo Cuenta RTT Con 50 ms de RTT
No persistente 4 objetos × 2 RTT 8 400 ms
Persistente 1 RTT de conexión + 4 × 1 RTT 5 250 ms

Medimos los dos. Con -H 'Connection: close', y cuatro curl separados, y luego con un solo curl y cuatro direcciones, que reutiliza la conexión. Tres corridas, sumando los tiempos de cada objeto:

separadas=0.419766 persistente=0.259841
separadas=0.435746 persistente=0.268202
separadas=0.429272 persistente=0.262072
Animación de dos diagramas de tiempo en la misma escala, RTT a RTT, para un HTML y tres imágenes. A la izquierda, sin reutilizar: cada objeto hace SYN, SYN+ACK, ACK con GET y 200, y termina en 8 RTT, 400 ms. A la derecha, una sola conexión: SYN, SYN+ACK, ACK con GET y 200 para el HTML, y luego GET y 200 para cada imagen, y termina en 5 RTT, 250 msAnimación de dos diagramas de tiempo en la misma escala, RTT a RTT, para un HTML y tres imágenes. A la izquierda, sin reutilizar: cada objeto hace SYN, SYN+ACK, ACK con GET y 200, y termina en 8 RTT, 400 ms. A la derecha, una sola conexión: SYN, SYN+ACK, ACK con GET y 200 para el HTML, y luego GET y 200 para cada imagen, y termina en 5 RTT, 250 ms
Lo único que cambia son los tres handshakes de más. En un RTT de 50 ms son los 150 ms de diferencia, y lo medido se acerca: unos 0,42 s contra unos 0,26 s.

La cuenta acierta. Y así se ve objeto por objeto en la conexión reutilizada:

$ curl -s -o /dev/null -o /dev/null -o /dev/null -o /dev/null -w '...' \
    http://www.wayra.test/ http://www.wayra.test/a.png http://www.wayra.test/b.png http://www.wayra.test/c.png
dns=0.000714 con=0.052259 ttfb=0.111525 total=0.111564 conexiones=1
dns=0.000000 con=0.000000 ttfb=0.055842 total=0.055871 conexiones=0
dns=0.000000 con=0.000000 ttfb=0.052721 total=0.052739 conexiones=0
dns=0.000000 con=0.000000 ttfb=0.051877 total=0.051896 conexiones=0

conexiones=0: no hizo falta conectar, y cuesta 1 RTT. No es solo el handshake lo que se ahorra. Una conexión nueva parte en slow start (semana 11), mientras que una ya abierta conserva lo que aprendió de la red. Además, el servidor no gasta memoria en mantener muchas conexiones cerrándose, ni deja tantos sockets en TIME_WAIT, como vimos en la semana 10.

Para escapar de los 2 RTT por objeto, los navegadores abren también conexiones paralelas, típicamente hasta 6 por servidor. Bajan más rápido, pero cada una cuesta un handshake y compite con las demás por el enlace, como vimos con la equidad en la semana 11.

En la práctica. En el código del servidor del laboratorio encontrarán disable_nagle_algorithm = True. Ese servidor escribe los headers y el cuerpo por separado, y sin esa línea el algoritmo de Nagle y el ACK retardado del cliente podrían agregar del orden de 40 ms que no tienen nada que ver con HTTP. Es un clásico al medir un protocolo de aplicación.

El problema de la fila

Con una conexión persistente, las respuestas deben volver en el mismo orden que las peticiones. El cliente puede mandar varias peticiones sin esperar (pipelining), y así lo hicimos con nc, mandando dos seguidas por la misma conexión:

$ printf 'GET / HTTP/1.1\r\nHost: www.wayra.test\r\n\r\nGET /a.png HTTP/1.1\r\nHost: www.wayra.test\r\n\r\n' \
    | nc -w1 10.212.6.10 80 | grep -E '^HTTP|Content-Length'
HTTP/1.1 200 OK
Content-Length: 67
HTTP/1.1 200 OK
Content-Length: 900

Las respuestas no llevan ninguna etiqueta que diga a qué petición corresponden: solo se puede saber por el orden. Si la primera tarda, las demás esperan aunque estén listas. Es bloqueo de la fila (head-of-line blocking) a nivel de HTTP, y por eso los navegadores casi no usaron pipelining. Recuerden este problema: es el que resuelve HTTP/2.

Cache de HTTP

La petición más rápida es la que no se hace. Igual que en DNS, una cache de HTTP guarda respuestas para reutilizarlas. Puede estar en el navegador, o en un proxy: un intermediario por el que pasan las peticiones de muchos clientes. Al guardar lo que pide uno, le ahorra el viaje a los demás, y la institución gasta menos enlace.

Dos preguntas gobiernan todo:

  1. ¿Sigue fresca? Si es así, se usa sin preguntarle a nadie: cuesta 0 RTT hasta el origen.
  2. ¿Sigue siendo válida? Si ya no está fresca, se le pregunta al origen si cambió. Si no cambió, no hace falta volver a bajarla: cuesta 1 RTT, pero casi ningún byte.

Cache-Control

Directiva Qué significa
max-age=60 Está fresca durante 60 segundos
no-cache Se puede guardar, pero siempre hay que revalidarla antes de usarla
no-store No se debe guardar en ninguna parte
private La puede guardar el navegador, pero no un proxy compartido

Error común. Leer no-cache como “no guardar en cache”. Eso es no-store. no-cache sí se guarda: solo prohíbe usarla sin preguntar. Si una página con datos personales lleva no-cache, la copia sigue guardada en el disco.

ETag y la petición condicional

Un ETag es una etiqueta que identifica una versión del recurso. El servidor decide cómo la calcula; mini_web usa un hash del contenido. Cuando un cliente tiene una copia, la pide condicionalmente con If-None-Match, y el servidor compara. Primero, una petición normal:

$ curl -s -D- -o /dev/null http://www.wayra.test/
HTTP/1.1 200 OK
Server: mini_web
Date: Mon, 02 Nov 2026 14:42:51 GMT
ETag: "d8a8038111f4"
Cache-Control: max-age=60
Content-Type: text/html
Content-Length: 67

Y ahora con la etiqueta:

$ curl -sv -o /dev/null -H 'If-None-Match: "d8a8038111f4"' http://www.wayra.test/
> GET / HTTP/1.1
> Host: www.wayra.test
> User-Agent: curl/8.14.1
> Accept: */*
> If-None-Match: "d8a8038111f4"
>
< HTTP/1.1 304 Not Modified
< Server: mini_web
< Date: Mon, 02 Nov 2026 14:42:51 GMT
< ETag: "d8a8038111f4"
< Cache-Control: max-age=60
< Content-Type: text/html
<

Un 304 no tiene cuerpo. Se ahorran los bytes del recurso, pero no el RTT, porque de todos modos hubo que preguntar. En un sitio con imágenes grandes es un gran ahorro; en un objeto de unos pocos bytes no se nota. El RFC 9110 lo plantea así: la meta de un 304 es minimizar lo que se transfiere cuando quien recibe ya tiene la copia.

Un proxy en acción

mini_cache.py es un proxy pequeño, de unas 150 líneas, que entiende justo estas reglas. Agrega un header X-Cache para que se vea qué decidió, y Age, que es un header estándar: cuántos segundos lleva guardada la respuesta. El recurso /noticia tiene max-age=3. Paso por paso:

$ curl -s -D- -o /dev/null -x 10.212.7.10:3128 http://www.wayra.test/noticia     # primera vez
HTTP/1.1 200 OK
ETag: "078cd7cab10a"
Via: 1.1 mini_cache
X-Cache: MISS

$ sleep 1; curl ... /noticia                                # fresca: no toca al origen
HTTP/1.1 200 OK
ETag: "078cd7cab10a"
Age: 1
Via: 1.1 mini_cache
X-Cache: HIT

$ sleep 3; curl ... /noticia                                # venció: pregunta, y recibe 304
HTTP/1.1 200 OK
ETag: "078cd7cab10a"
Age: 0
Via: 1.1 mini_cache
X-Cache: REVALIDATED

$ curl -s http://www.wayra.test/cambiar                     # cambia la noticia en el origen
cambiada
$ curl ... /noticia                                         # sigue fresca: ¡dato viejo!
HTTP/1.1 200 OK
ETag: "078cd7cab10a"
Age: 0
Via: 1.1 mini_cache
X-Cache: HIT

$ sleep 3.2; curl ... /noticia                              # venció y cambió: baja todo
HTTP/1.1 200 OK
ETag: "d38002d35787"
Via: 1.1 mini_cache
X-Cache: MISS

(Solo mostramos algunos headers de cada respuesta.) Miren el penúltimo: el origen ya cambió y el cliente recibe el dato viejo, sin error alguno. No es una falla: es el precio de la cache, y dura como mucho max-age. Es el mismo compromiso que el TTL de DNS.

Animación de la misma corrida entre cliente, proxy y origen, con una línea de tiempo abajo. Primero, MISS: el proxy pide al origen y guarda la versión 078cd7cab10a, fresca por 3 segundos. Al segundo 1, HIT con Age 1, sin tocar al origen. Al segundo 4 la copia venció: el proxy pregunta con If-None-Match, recibe un 304 y el cliente ve REVALIDATED. Luego el origen cambia a d38002d35787 y el proxy sigue dando la copia vieja con un HIT mientras esté fresca. Pasados 3,2 segundos más, la pregunta condicional recibe un 200 con el cuerpo nuevo y el cliente ve MISSAnimación de la misma corrida entre cliente, proxy y origen, con una línea de tiempo abajo. Primero, MISS: el proxy pide al origen y guarda la versión 078cd7cab10a, fresca por 3 segundos. Al segundo 1, HIT con Age 1, sin tocar al origen. Al segundo 4 la copia venció: el proxy pregunta con If-None-Match, recibe un 304 y el cliente ve REVALIDATED. Luego el origen cambia a d38002d35787 y el proxy sigue dando la copia vieja con un HIT mientras esté fresca. Pasados 3,2 segundos más, la pregunta condicional recibe un 200 con el cuerpo nuevo y el cliente ve MISS
Lo fresco se sirve sin preguntar; lo vencido se pregunta. La franja de abajo es el precio: mientras la copia está fresca y el origen ya cambió, el proxy puede dar el dato viejo.

El cliente siempre ve 200, incluso en la revalidación: el 304 es entre el proxy y el origen. Y con no-store no se guarda nada, como debe ser:

$ curl ... /privado ; curl ... /privado
HTTP/1.1 200 OK
Via: 1.1 mini_cache
X-Cache: BYPASS

HTTP/1.1 200 OK
Via: 1.1 mini_cache
X-Cache: BYPASS

¿Cuánto se gana? Medimos /a.png, en una corrida, a través del proxy y directo. La primera vía el proxy es un MISS y paga todo el camino; la segunda es un HIT, porque el proxy está al lado del cliente:

proxy   0.104770    (MISS)
directo 0.104980
proxy   0.000586    (HIT)
directo 0.104756

En la práctica. Las CDN, que veremos la próxima semana, son esto mismo a escala planetaria: caches cerca de los usuarios, y DNS para mandarlos a la más cercana. Hoy casi todo va cifrado con HTTPS, así que un proxy de una institución ya no puede leer las peticiones ni guardar las respuestas sin instalar certificados propios. Por eso, en la práctica, casi toda la cache se hace en el navegador y en la CDN del propio sitio.

HTTP/2

HTTP/1.1 tiene dos problemas para las páginas actuales. Las respuestas salen en fila, como vimos, y los headers se repiten casi iguales en cada petición, como texto. Para evitar la fila, los navegadores abren varias conexiones, y eso es un remedio caro. HTTP/2 resuelve ambos cambiando cómo se escriben los mensajes, sin cambiar lo que significan: los mismos métodos, códigos y headers.

Definición. En HTTP/2, una conexión TCP lleva muchos streams bidireccionales. Cada mensaje se parte en frames, y cada frame lleva el número de su stream. Los frames de streams distintos se pueden mezclar: eso es la multiplexación.

HTTP/1.1, una conexión:     | respuesta A ............... | respuesta B | respuesta C |

HTTP/2, una conexión:       |A1|B1|C1|A2|B2|C2|A3|C3|A4|A5|
                             cada pedazo lleva el número de su stream

Un frame tiene un encabezado binario de 9 bytes (sección 4.1 del RFC 9113): 3 de largo, 1 de tipo, 1 de flags y 4 con el número de stream. Los streams que inicia el cliente usan números impares, y el cero se reserva para la conexión. Nada de esto se escribe a mano con nc. Para mirarlo, h2_frames.py le contesta lo justo a curl y anota cada frame. No es un servidor de HTTP/2: no valida casi nada. Manda un frame DATA por stream en cada turno, para que se vea la mezcla.

Pedimos dos objetos a la vez, sobre TCP sin cifrar, con --http2-prior-knowledge. (Los navegadores solo hablan HTTP/2 sobre TLS.)

$ curl -s --http2-prior-knowledge --parallel -o /dev/null -o /dev/null \
    -w '%{http_version} %{http_code} conexiones=%{num_connects} %{size_download}\n' \
    http://10.212.6.10:8080/uno http://10.212.6.10:8080/dos
2 200 conexiones=1 2000
2 200 conexiones=0 2000

Una sola conexión para las dos. Si repiten esto varias veces, verán que curl a veces abre el segundo stream recién cuando termina el primero: nos pasó en 2 de 120 corridas, y esperar más en el servidor no cambió nada. Es una decisión de curl; HTTP/2 permite mezclar, pero no obliga a nadie a hacerlo. Si no ven los turnos, repitan. Y esto anota el servidor cuando sí se mezclan:

<- prefacio de HTTP/2
<- SETTINGS stream=0 largo=18
<- WINDOW_UPDATE stream=0 largo=4
<- HEADERS stream=1 largo=35
-> HEADERS stream=1 largo=1
<- SETTINGS stream=0 largo=0
<- WINDOW_UPDATE stream=1 largo=4
<- WINDOW_UPDATE stream=1 largo=4
<- HEADERS stream=3 largo=10
-> HEADERS stream=3 largo=1
<- WINDOW_UPDATE stream=3 largo=4
<- WINDOW_UPDATE stream=3 largo=4
-> DATA stream=1 largo=400
-> DATA stream=3 largo=400
-> DATA stream=1 largo=400
-> DATA stream=3 largo=400
-> DATA stream=1 largo=400
-> DATA stream=3 largo=400
-> DATA stream=1 largo=400
-> DATA stream=3 largo=400
-> DATA stream=1 largo=400 END_STREAM
-> DATA stream=3 largo=400 END_STREAM
<- GOAWAY stream=0 largo=17

Hay mucho que leer aquí:

  • El stream 1 y el stream 3 se turnan en la misma conexión. Ninguno espera a que termine el otro. Las respuestas ya no van en fila.
  • SETTINGS y el WINDOW_UPDATE en el stream 0 son de la conexión. HTTP/2 tiene control de flujo por stream, además del de TCP: es la misma idea de la semana 10, un nivel más arriba.
  • La petición del stream 1 ocupa 35 bytes, y la del stream 3, con casi los mismos headers, ocupa 10. Esto es la compresión.

Compresión de headers: HPACK

HPACK es la compresión de headers de HTTP/2. Tiene una tabla estática con los headers más comunes, una tabla dinámica que se llena durante la conexión, y codificación Huffman para el texto nuevo. Si un header ya se mandó, en la siguiente petición basta su índice en la tabla.

Wireshark tiene el mismo estado que los extremos y decodifica los dos mensajes:

$ tshark -r h2.pcap -d tcp.port==8080,http2 -Y 'http2.type==1 && ip.src==10.212.1.10' -O http2
    Stream: HEADERS, Stream ID: 1, Length 35, GET /uno
        Header: :method: GET
        Header: :scheme: http
        Header: :authority: 10.212.6.10:8080
        Header: :path: /uno
        Header: user-agent: curl/8.14.1
        Header: accept: */*
    Stream: HEADERS, Stream ID: 3, Length 10, GET /dos
        Header: :method: GET
        Header: :scheme: http
        Header: :authority: 10.212.6.10:8080
        Header: :path: /dos

(Recortamos la salida.) Los pseudo-headers que empiezan con : reemplazan a la línea inicial de HTTP/1.1. Y la respuesta de nuestro servidor mide un solo byte, 0x88: es la entrada 8 de la tabla estática, :status: 200. Comparen eso con la línea HTTP/1.1 200 OK escrita completa.

Idea clave. HTTP/2 cambia el formato del mensaje, no su significado. Una aplicación que entiende GET, 200 y Cache-Control sigue igual. El costo es que ya no se lee con los ojos: hacen falta herramientas como tshark.

Lo que HTTP/2 no arregló

Todos los streams viajan en una sola conexión TCP, y TCP no sabe de streams. Para TCP hay un solo stream de bytes, que debe entregar en orden. Pensemos qué pasa si se pierde un paquete:

 enviados por TCP, en orden:     A1  B1  C1  A2  B2  C2
 se pierde B1:                   A1  --  C1  A2  B2  C2

 lo que TCP le entrega a HTTP/2: A1   ... y espera la retransmisión de B1
 mientras tanto, C1, A2, B2 y C2 ya están en el host, pero esperan en el buffer de TCP.

Este razonamiento sale de lo que sabemos sobre entrega en orden en TCP; no lo medimos aquí. El propio RFC 9113 advierte que este bloqueo de TCP no se resuelve.

Error común. Creer que HTTP/2 eliminó todo bloqueo. Eliminó el de HTTP: una respuesta lenta ya no detiene a las demás. Pero una pérdida en TCP detiene a todos los streams, aunque los datos que faltan sean de uno solo. Con mucha pérdida, una sola conexión HTTP/2 incluso puede rendir menos que varias de HTTP/1.1, porque una pérdida en una de ellas no frena a las otras. Por eso existe HTTP/3, que veremos la próxima semana: lleva los streams al transporte, sobre QUIC.

Bloqueo HTTP/1.1 HTTP/2 sobre TCP
Una respuesta lenta detiene a las siguientes Sí, en la misma conexión No
Una pérdida de TCP detiene a todos los streams Solo los de esa conexión Sí, todos, porque hay una sola conexión

Laboratorio: DNS paso a paso y HTTP por dentro

Trabajen en code/week-12/. Levanten la red y entren al cliente:

make up
docker exec -it clab-w12-red-cliente sh
  1. Recorrido. Corran dig +trace www.wayra.test, y repitan el recorrido a mano con dig @servidor +norecurse. Después, en la terminal de su computadora o en el contenedor del curso, hagan lo mismo con un nombre .pe.

  2. Registros y TTL. Lean los registros A, AAAA, NS y MX de wayra.test y de un dominio real. Pregunten dos veces por rapido.wayra.test y vean bajar el TTL. Hagan lo mismo con un resolver público y comparen.

  3. Captura. Capturen una consulta en cliente y comparen con dig el ID, la pregunta y la respuesta:

    tcpdump -i eth1 --immediate-mode -w /tmp/dns.pcap udp port 53 &
    dig blog.wayra.test
    kill %1

    Abran el archivo en Wireshark, después de copiarlo con docker cp, o léanlo en el contenedor con tshark. Hagan lo mismo con /w12/bin/dns_ask, y encuentren cada campo de la tabla de este capítulo.

  4. HTTP a mano. Con nc, escriban una petición a 10.212.6.10, puerto 80. Prueben sin Host, y prueben lo mismo contra un servidor real, como example.com, desde el contenedor del curso. Cambien Host a blog.wayra.test. ¿Qué pasa si escriben las líneas con \n en vez de \r\n? Escriban la predicción antes.

  5. Condicional. Con curl -v, vean una respuesta 200, copien el ETag, y consigan un 304 con If-None-Match.

  6. Tiempos. Midan con curl -w la resolución, la conexión y el primer byte, tal como en la sección “Cuánto cuesta una página”. Repitan con una conexión reutilizada, y confirmen los 8 y los 5 RTT.

  7. Delegación. Es la parte que se entrega al final del laboratorio. Vean la sección siguiente.

  8. P4. Sigan avanzando con el proyecto: se entrega la próxima semana.

Algunas de estas actividades se basan en los laboratorios de DNS y HTTP de Kurose y Ross.

Delegar una zona nueva

En lab/ hay dos archivos con un TODO:

  • lab/test.zone: la zona del TLD. Falta delegar killa.test. en el servidor ns1.killa.test., en la dirección 10.212.8.10.
  • lab/killa.zone: la zona nueva, en la que faltan sus registros. El comentario del archivo dice cuáles.

Para probar a mano, arranquen la red con sus zonas y sigan el recorrido:

make down
cd red && containerlab deploy -t red.clab.yml --reconfigure && cd ..
sh red/start.sh lab lab
docker exec clab-w12-red-cliente dig +trace www.killa.test

make test también comprueba su trabajo: mientras un archivo siga con TODO, esas pruebas se saltan con un mensaje. Las pruebas revisan que el padre delegue con glue, que el hijo responda con autoridad y que el resolver encuentre cada registro.

Cuando funcione, quiten el glue de lab/test.zone, reinicien y comprueben el SERVFAIL que mostramos en este capítulo. Después expliquen por qué ocurre, con sus propias palabras.

Archivos y make test

Archivo Qué es
dns_wire.py El formato de los mensajes: codificar y decodificar, con compresión
dns_auth.py Servidor autoritativo, por UDP y TCP
dns_resolver.py Resolver recursivo con cache, ID y puerto al azar, y chequeos de la respuesta
dns_ask.c Una pregunta armada a mano, en C
mini_web.py Sitio de juguete, en memoria
mini_cache.py Proxy con cache
h2_frames.py Lista los frames de HTTP/2 y responde lo justo
red/ La topología, sus zonas y el script de arranque
lab/ Las zonas que deben completar
test_wire.py Pruebas sin red: formato, autoritativo, chequeos, TTL y reglas de cache
test_net.py Pruebas sobre la red: cada afirmación de este capítulo que se puede comprobar corriéndola
make            # compila dns_ask y revisa la sintaxis de los programas de Python
make test       # corre las pruebas, y siempre destruye lo que levantó
make up         # levanta la red y arranca los servidores
make down       # destruye la red y su red de Docker
make clean      # lo anterior, y borra lo generado

make test tarda menos de un minuto y usa márgenes amplios, porque los tiempos dependen de la máquina. No pide cifras exactas: pide, por ejemplo, que una pregunta repetida tarde menos de la mitad que una en frío.

En la práctica. Estos programas son para leerlos. Para servir nombres de verdad, se usan programas como BIND, Unbound o Knot, y para la web, nginx o similares. Ninguno de los programas de Python de esta semana debería publicarse en una red real.

Resumen

  • DNS es una base de datos distribuida y jerárquica. Separa los nombres de las direcciones, y también da alias, correo y reparto de carga.
  • Jerarquía: raíz, TLD y autoritativos. Una zona es lo que administra una autoridad, y puede delegar partes. El árbol de nombres y el de zonas no son el mismo.
  • El resolver recorre la jerarquía por sus clientes. El cliente hace una consulta recursiva, y el resolver hace consultas iterativas.
  • Referral y glue: quien no sabe manda a quien sabe. Si el servidor está dentro de la zona delegada, el glue es indispensable; sin él hay SERVFAIL.
  • Cache y TTL: en frío, 3 RTT en nuestra red; repetida, casi nada. También se guardan las delegaciones y las respuestas negativas. El TTL equilibra rapidez y carga contra lo rápido que se nota un cambio. Nada se “propaga”.
  • Registros: A, AAAA, NS, CNAME, MX, TXT y SOA. NXDOMAIN, NODATA y SERVFAIL son cosas distintas.
  • El mensaje: 12 bytes de encabezado y cuatro secciones. Etiquetas con su largo, y compresión con punteros, que hay que validar.
  • UDP porque es una pregunta y una respuesta, y repetir es inofensivo. TCP cuando la respuesta no cabe (TC). Por TCP, cada mensaje lleva su largo delante.
  • Envenenamiento de cache: con ID y puerto al azar, un atacante fuera del camino pasa de 65 536 a unos 1850 millones de combinaciones. No detiene a quien ve el tráfico.
  • DNSSEC autentica y no cifra. DoH y DoT cifran hasta el resolver y no autentican el dato de origen. Son complementarios.
  • HTTP/1.1 es texto: línea inicial, headers, línea vacía y cuerpo. Host permite varios sitios en una dirección; sin él, un servidor debe responder 400. HTTP no guarda estado; las cookies llevan el estado en los mensajes.
  • Costo: un objeto por conexión nueva son 2 RTT. Una página con 3 imágenes: 8 RTT no persistente y serial, 5 RTT persistente. Lo medimos: unos 420 y 260 ms con 50 ms de RTT.
  • Cache de HTTP: max-age evita preguntar; ETag e If-None-Match evitan volver a bajar, con un 304 sin cuerpo. no-cache revalida y no-store no guarda. Mientras esté fresca, una copia vieja se sirve sin error.
  • HTTP/2: frames y streams mezclados en una conexión, y HPACK (35 bytes contra 10 en nuestra captura). Elimina el bloqueo de HTTP, pero una pérdida en TCP todavía detiene todos los streams. Por eso existe HTTP/3.

Ejercicios

  1. Un resolver sin nada en cache busca www.uni.edu.pe. ¿A qué servidores pregunta y en qué orden, según la salida de dig +trace de este capítulo? ¿Y la segunda vez? ¿Y si después alguien pregunta por correo.uni.edu.pe?
  2. Decodifiquen a mano, byte a byte, esta pregunta: ab cd 01 00 00 01 00 00 00 00 00 00 03 75 6e 69 04 74 65 73 74 00 00 0f 00 01. Digan el ID, los flags, el nombre, el tipo y la clase. Comprueben armándola con dns_wire.py.
  3. Una zona tiene un TTL de un día en su registro A. Deben mudar el servidor el viernes a las 18:00, y quieren que como máximo un 1 % de los clientes vea la dirección vieja pasada una hora. ¿Qué harían, y desde cuándo? ¿Qué costo tiene?
  4. En dns_resolver.py, usable_glue descarta las direcciones que no estén dentro de la zona delegada. Construyan una respuesta de ejemplo que muestre qué se podría colar sin esa regla. No hace falta ninguna red: basta una prueba como las de test_wire.py.
  5. Repitan la cuenta de la tabla de envenenamiento con 16 bits de ID y solo 1000 puertos posibles, y luego con 100 000 respuestas falsas en la carrera, para cada fila. ¿Por qué un NAT que reescribe los puertos de forma predecible puede arruinar esta defensa?
  6. Un compañero dice: “ya activamos DNSSEC en nuestra universidad, así que nadie sabe qué páginas visitamos”. Corrijan esa frase. ¿Qué garantiza DNSSEC, qué agregaría DoH y qué seguiría viendo el proveedor de Internet aunque usen los dos?
  7. Una página tiene un HTML y 10 imágenes, con 30 ms de RTT. Calculen los RTT y el tiempo total sin transmisión: (a) no persistente y serial; (b) persistente sin pipelining; (c) con 5 conexiones paralelas no persistentes; (d) HTTP/2 con una conexión, si las 10 imágenes se piden juntas después del HTML. Comprueben (a) y (b) en la red de la semana, cambiando la lista de direcciones de curl.
  8. Predigan y comprueben: en el proxy, pidan /noticia, cambien la noticia en el origen con /cambiar y repitan cada segundo durante 8 segundos. Escriban primero la secuencia de X-Cache. ¿Cuánto tiempo puede ver un cliente el dato viejo, como máximo?
  9. El recurso /hora no lleva Cache-Control. Nuestro proxy no lo guarda, pero el RFC 9111 permite a una cache estimar una frescura (heurística) en ciertos casos. Lean la sección 4.2.2 de ese RFC y expliquen qué condición usa y por qué es un riesgo para un recurso así. ¿Qué header agregarían a la respuesta?
  10. Modifiquen mini_cache.py para que, cuando el origen no responda, entregue una copia vencida con un aviso, en vez de un 502. Lean la sección 4.2.4 del RFC 9111 antes de hacerlo. ¿Cuándo es buena idea y cuándo no? Para probarlo, detengan mini_web con pkill en el nodo web.
  11. Con h2_frames.py, pidan tres objetos en vez de dos. ¿Qué número de stream recibe el tercero? ¿Cuánto mide su HEADERS y por qué? Cambien :authority usando otra dirección en la URL: ¿cómo cambia el tamaño?
  12. Sobre el bloqueo: hagan en un papel una tabla como la de “Lo que HTTP/2 no arregló”, con seis conexiones de HTTP/1.1 frente a una de HTTP/2, y una pérdida del 2 % en el enlace. ¿Qué ventaja de HTTP/2 se reduce? ¿Por qué un sitio podría preferir una sola conexión de todos modos? Vuelvan a este ejercicio la próxima semana, con QUIC.