P5 · wasi

wasi (“casa” en quechua) es un servidor HTTP/1.1 en C. Corre sobre rimay y se publica en Qhapaq Ñan con un nombre en DNS. Aquí se juntan todas las partes del proyecto. Se usa lo visto en las semanas 12 y 13. Entrega en la semana 15.

Antes de empezar

Traigan lo que ya hicieron:

cp ../p4-rimay-congestion/src/* rimay/   # todo su src/ de P4, con los nombres que tenga
cp -R ../p2-qhapaq-nan/as as             # su AS de P2

rimay/rimay.c es un marcador que compila y falla: al copiar, lo reemplazan. El mundo simulado no se copia: make lo toma de ../p2-qhapaq-nan/mundo. Si su carpeta de P2 está en otro lado, usen make test P2=<ruta>.

1. El servidor

wasi <puerto> <carpeta> [--tcp] sirve archivos de una carpeta. Corre sobre rimay y, con --tcp, sobre TCP. Por eso el código de HTTP no sabe sobre qué transporte funciona: src/http.c solo usa las dos funciones de http_stream, en src/http.h. Las pruebas lo compilan con un transporte falso, en memoria, y revisan con nm que no llame a rimay_* ni a los sockets.

Cuando ya escucha, escribe en stdout exactamente wasi escuchando en <puerto>, y lo vacía con fflush. Con argumentos inválidos o una carpeta que no existe, termina con un código distinto de 0.

Peticiones

  • Una petición mide como máximo 8192 bytes, contando la línea inicial, los headers y la línea vacía. Las líneas terminan en CRLF. No se exige aceptar LF solo.
  • Solo HTTP/1.1. Cualquier otra versión, también HTTP/1.0, da 400. Lo correcto en otro servidor sería 505, pero aquí la lista de códigos es corta.
  • Los nombres de los headers no distinguen mayúsculas. Los métodos sí: get da 405.
  • La ruta empieza con /. Una URL completa (GET http://...) da 400.
  • Lo que sigue a ? no es parte de la ruta.

Si varias reglas aplican, gana la primera:

Orden Caso Respuesta ¿Cierra?
1 Pasa de 8192 bytes sin terminar 400 Sí
2 Línea inicial mal formada, otra versión, header sin :, espacio antes de :, nombre vacío, un byte de control 400 Sí
3 Sin Host, Host vacío o repetido 400 Sí
4 Content-Length que no es un número 400 Sí
5 Otro método 405 con Allow: GET, HEAD Solo si trae cuerpo (Content-Length mayor que 0 o Transfer-Encoding), porque el cuerpo no se lee
6 GET o HEAD con cuerpo 400 Sí
7 Un % sin dos dígitos hexadecimales, o un byte de control en la ruta ya decodificada, como %00 400 Sí
8 Un segmento .., un archivo que no existe, o una ruta que sale de la carpeta 404 No
9 El archivo existe y no se puede leer 500 No
10 If-None-Match que calza 304 No
11 Todo bien 200 No

Con Connection: close en la petición, cualquier respuesta lleva Connection: close y después se cierra. Toda respuesta que cierra lo avisa con ese header.

Rutas

Una ruta nunca sale de la carpeta. El orden es este:

  1. Se decodifica %xx una sola vez. %252e es el texto %2e, un nombre de archivo normal, no un punto.
  2. Se parte por /. Si algún segmento es exactamente .., la respuesta es 404, sin tocar el disco y aunque la ruta se quede adentro: /sub/../index.html da 404. No se normaliza. a..b.txt es un nombre normal.
  3. Se pega a la carpeta, también si la ruta empieza con //. Se resuelve con realpath, y el resultado debe estar dentro de la carpeta ya resuelta. Si no, 404. Ojo con comparar solo el prefijo: /www-privado empieza igual que /www.
  4. Una carpeta se sirve con su index.html, con barra final o sin ella, y esa ruta pasa por la misma revisión. Sin index.html, 404: no se listan.
  5. Solo se sirven archivos regulares. Una tubería da 404, y abrirla sin revisar deja al hilo esperando para siempre.

Un enlace simbólico que se queda adentro se sirve. Uno que sale de la carpeta da 404, igual que un bucle de enlaces.

Respuestas

  • 200: Content-Length, Content-Type y ETag, y el archivo. Se lee por partes: servir un archivo de 256 MB no puede llevar la memoria del proceso a más de 32 MiB (en --tcp, que es donde se mide).
  • HEAD: los mismos headers que GET, y ningún byte de cuerpo. También en un 404.
  • 304: ETag, sin Content-Length y sin cuerpo.
  • 400, 404, 405 y 500: Content-Length correcto y un texto corto como cuerpo (salvo en HEAD).
  • Content-Type sale de la extensión, sin distinguir mayúsculas: .html y .htm text/html; .txt text/plain; .css text/css; .js text/javascript; .json application/json; .png image/png; .jpg y .jpeg image/jpeg; .gif image/gif; .svg image/svg+xml. El resto, application/octet-stream.

ETag. Es "<tamaño>-<segundos>.<nanosegundos>", los tres en hexadecimal, con el tamaño y st_mtim del archivo. Así cambia si cambia el tamaño o la fecha.

If-None-Match. Vale en GET y HEAD. Puede traer una lista separada por comas, etiquetas débiles (W/"...", que se comparan sin el W/) o *. Si calza alguna, 304. Si el archivo no existe, 404.

Conexiones

  • Persistentes por defecto: varias peticiones en la misma conexión.
  • Pipelining, obligatorio. Si llegan varias peticiones juntas, se responden todas, en orden. Lo que sobre en el buffer es la siguiente petición. No hace falta responderlas en paralelo.
  • Por partes. Una petición puede llegar de a un byte: recv devuelve lo que haya.

Hilos, clientes que se van y cómo terminar

Los identificadores de rimay no sirven con poll, así que hay un hilo por cliente, y el hilo principal solo acepta. Eviten fork, porque un solo hilo de la biblioteca atiende todas las conexiones.

Un hilo de cliente puede quedarse bloqueado mucho tiempo, y rimay no tiene forma de abortar una llamada:

Situación Dónde se bloquea Hasta cuándo
Un cliente conectado que no pide nada, o que dejó una petición a medias rimay_recv Hasta que cierre, o 30 s después de que desaparezca
Un cliente que desapareció a mitad de la respuesta rimay_send o rimay_close Unos 30 s (10 s si su FIN ya llegó)
Un cliente vivo que no lee nunca rimay_send o rimay_close Sin límite: responde las sondas con la ventana en cero

No hay tiempo de espera por petición: la API no permite interrumpir rimay_recv. Lo que se exige es que todo eso bloquee solo a ese hilo:

  • rimay_close se llama desde el hilo del cliente, nunca desde el que acepta.
  • Los hilos van desacoplados (pthread_detach): nadie espera a un hilo para seguir.
  • Al terminar con un cliente, su hilo libera todo y termina.
  • Un rimay_send o rimay_recv que devuelve -1 termina la conexión, y no el servidor. En --tcp, usen MSG_NOSIGNAL o ignoren SIGPIPE: escribirle a un cliente que se fue mata al proceso.

Para terminar, SIGINT y SIGTERM deben terminar el proceso con código 0 en menos de 5 s, aunque haya hilos bloqueados. Como no se les puede avisar, no se los espera:

// Antes de crear cualquier hilo: todos heredan las señales bloqueadas.
pthread_sigmask(SIG_BLOCK, &signals, NULL);
// Un hilo solo para esto: sigwait(&signals, &received); exit(0);

Con --tcp, cerrar con datos sin leer manda un RST que puede borrarle la respuesta al cliente. Antes de close, conviene shutdown(fd, SHUT_WR) y leer lo que quede, con un tope de tiempo.

Cualquiera de los dos extremos puede cerrar primero, y los dos pueden cerrar a la vez. Con Connection: close, el servidor cierra al terminar de enviar, y el cliente puede cerrar apenas tiene todo el cuerpo: sus FIN se cruzan. Si rimay_close se queda colgado en ese caso, el error está en su rimay.

2. El cliente

wasi_get [--tcp] [--dns <ip>] [-o <archivo>] http://<host>[:<puerto>]/<ruta>

Sin puerto, el 80. Sin ruta, /. El cuerpo sale por stdout, o a <archivo> con -o. Pide con GET, y Host lleva el nombre y el puerto tal como vienen en la URL.

Resolución. Si el host es una IPv4, no se pregunta a nadie. Si es un nombre, wasi_get manda su propia consulta: una pregunta de tipo A por UDP, al puerto 53 de --dns, con un ID aleatorio. Después usa la dirección numérica con rimay_connect. Sin --dns, un nombre es un error. No se usa getaddrinfo, y dns/README.md explica por qué.

  • Hace hasta 3 intentos de 2 s cada uno.
  • Una respuesta con otro ID se ignora, y se sigue esperando.
  • Las respuestas pueden traer otros registros antes del A, como un CNAME, y nombres comprimidos.
  • Un nombre con punto final es válido: en la consulta es el mismo nombre.

Resumen. Al terminar bien, escribe en stderr, como últimas líneas, exactamente esto, con tres decimales y punto decimal:

estado: 200
resolucion: 12.345 ms
conexion: 3.210 ms
primera_respuesta: 8.001 ms
bytes: 1048576
  • resolucion: desde que se envía la primera consulta hasta tener la dirección. 0.000 si la URL trae una IPv4.
  • conexion: lo que demora rimay_connect o connect.
  • primera_respuesta: desde que se empieza a enviar la petición hasta el primer byte de la respuesta.
  • bytes: los bytes del cuerpo.

Código de salida.

Código Cuándo
0 Respuesta completa con 200
1 Respuesta completa con otro código. Se escribe el resumen, y el cuerpo sale igual
2 Cualquier otra cosa: argumentos, DNS, no se pudo conectar, se cerró antes de responder, o llegaron menos bytes que Content-Length. No se exige el resumen

Sin Content-Length, lee hasta que cierren.

3. Publicarlo

  1. Agreguen su registro a dns/qhapaq.zone, siguiendo dns/README.md: www.equipoX.qhapaq. con la dirección pública de su srv.

  2. Levanten todo. wasi corre en la red de srv, con sus programas de este contenedor (nsenter), y el servidor DNS corre en la red de peer, en 198.18.3.1.

    make mundo-up                     # su AS en el mundo de P2
    make publicar                     # wasi en srv, por rimay y por TCP, y el DNS. Sirve www/
  3. Desde afuera, un host en otro AS, bajen una página:

    make bajar RUTA=/index.html
    make bajar RUTA=/index.html OPCIONES=--tcp

El filtro de su AS debe dejar pasar UDP y TCP hacia srv, en el puerto que usen (por omisión, 8080; make publicar PORT=...).

4. La demostración

En la semana 15, en la Sesión B:

  • Muestran su página desde otro AS, cruzando BGP y usando rimay.
  • El docente corta un enlace mientras se baja un archivo grande. La transferencia debe terminar correctamente.
  • Comparan rimay con TCP y explican los resultados, en su README.md.

Qué hace falta para que el corte funcione

rimay abandona a los 30 s sin recibir ningún paquete válido. Por eso la red debe encontrar otro camino antes, y después rimay tiene que notarlo con su siguiente retransmisión, hasta 10 s más tarde en el peor caso. Con lo que P1 y P2 ya exigen, alcanza:

  • Hold time de BGP de 10 s o menos, y Dead de OSPF de 3 s.
  • El enlace cortado tiene otro camino. make enlaces lista los que se pueden cortar. El de srv con r3 y el de afuera con troncal no tienen otro camino: un corte de más de 30 s termina la transferencia con error, y eso es lo correcto.
  • El enlace cortado está en el camino. P2 pide salir por el proveedor A, así que los datos de srv hacia afuera pasan por proveedor-a. Cortar peer, por ejemplo, no detiene nada. Con traceroute se ve el camino de cada sentido, que no tiene por qué ser el mismo.

En la red sin retardo, un archivo de 30 MB pasa demasiado rápido, y no hay tiempo de cortar nada. Por eso el enlace de afuera se hace lento antes de empezar:

head -c 30000000 /dev/urandom > www/grande.bin
make lento                                 # 40 ms de RTT y 8 Mbit/s en el enlace de afuera
make bajar RUTA=/grande.bin SALIDA=copia &
sleep 3
make corte ENLACE=proveedor-a              # el enlace deja de pasar paquetes, sin que nadie avise
wait
cmp www/grande.bin copia
make sin-corte sin-lento

Lo medido con la solución de referencia en una laptop, con 30 MB y el enlace lento de make lento. Sin corte, esa bajada demora 42 s. El corte es a los 3 s, silencioso y en los dos extremos. Su rimay dará otros tiempos, pero la cuenta es la misma:

Enlace cortado Quién lo nota Tiempo sin avanzar Bajada completa
proveedor-a BGP, hold de 3 s 4,4 s 56 s
proveedor-a BGP, hold de 10 s 9,1 s 62 s
proveedor-b BGP, hold de 3 s 4,4 s 56 s
proveedor-b BGP, hold de 10 s 9,0 s 61 s
r3-r4 OSPF, Dead de 3 s 4,5 s 57 s
peer Nadie: no está en el camino No se detiene 42 s

Aun con el hold más largo que permite P2, sobra margen frente a los 30 s. Esa es la razón de esa regla de P2: con un hold de 120 s, la bajada termina con error.

Reglas que revisan las pruebas

  • curl puede hablar con wasi --tcp, y los headers y códigos cumplen el enunciado. Reutiliza la conexión.
  • src/http.c funciona sobre un transporte falso, y no llama a rimay_* ni a los sockets.
  • wasi_get baja por rimay un archivo idéntico al original, incluso con pérdida.
  • Se revisan rutas peligrosas, crudas y codificadas, enlaces simbólicos, peticiones mal formadas o partidas y clientes que se desconectan.
  • Un GET con If-None-Match correcto devuelve 304 sin cuerpo.
  • Dos clientes simultáneos reciben datos correctos. También 32 a la vez por TCP y 8 por rimay.
  • Un cliente callado, a medias, que no lee o que desapareció no detiene a los demás.
  • Los hilos de los clientes que se fueron terminan: unos 30 s después con un cliente muerto en rimay, y se acepta hasta 100 s.
  • SIGTERM y SIGINT terminan con código 0 en menos de 5 s, con clientes bloqueados.
  • Valgrind no reporta errores ni fugas (definitely lost e indirectly lost) en un ciclo de peticiones, por TCP y por rimay, en servidor y cliente.
  • En el mundo: su nombre resuelve desde otro AS, la página llega por rimay y por TCP, los datos pasan por el proveedor A y el corte silencioso de proveedor-a y de r3-r4 no daña la bajada.

Tamaños y pérdida

Las pruebas bajan 1 MB con 5 % de pérdida, con un tope de 300 s (600 s con más pérdida). Las privadas usan otras tasas, siempre dentro de lo que rimay soporta: hasta 12 %. Más allá, un programa correcto puede abandonar, porque pasa 30 s sin recibir nada.

Lo medido con la solución de referencia, con cuatro bajadas a la vez por tamaño, en segundos:

Pérdida 256 KB 1 MB 5 MB
1 % 0 a 1 0 a 2 1 a 2
5 % 0 a 3 2 a 4 10 a 14
10 % 1 a 8 12 a 31
12 % 5 a 29 28 a 41
15 % 7 a 26 59 a 152

Varía mucho y es normal: por eso los límites tienen margen. A 15 % ninguna de estas ocho bajadas abandonó, pero ya no es seguro: sin congestión, con la ventana fija de P3, una de ocho sí abandonó. Para mostrar algo con pérdida, usen hasta 5 MB con 5 %, o 1 MB con 12 %. Para la demostración del corte, sin pérdida, 30 MB están bien.

Archivos

Archivo Qué hace ¿Lo modifican?
src/http.h La frontera entre HTTP y el transporte No
src/http.c El servidor HTTP. No sabe de transporte Sí
src/wasi.c, src/wasi_get.c Los programas. Cada uno es un main Sí
src/*.c, src/*.h adicionales Lo que quieran agregar. Todo .c de src/ se enlaza con los programas Sí
rimay/ Todo su src/ de P4. rimay.h no cambia Solo para corregir errores
as/ Su red de P2, copiada aquí Sí
dns/qhapaq.zone La zona del mundo Solo su registro
dns/dns_server.py, dns/README.md El servidor DNS y sus instrucciones No
www/ Lo que make publicar sirve Sí
Makefile, p5_public_test.py, topo/, tools/, tests/ Compilan y prueban No

Agreguen sus pruebas en archivos test_*.py, en esta misma carpeta: make test las corre con la red pequeña todavía arriba.

Cómo se corren las pruebas

Dentro del contenedor del curso (env/run.sh), desde esta carpeta:

make             # compila en build/
make test        # las tres tandas, y destruye todo también si algo falla o con Ctrl+C
make test-local  # solo lo que no usa topologías
make test-red    # solo la red pequeña
make test-mundo  # solo el mundo de P2 con su as/
make clean       # destruye todo y borra lo generado

make test demora unos 4 minutos con la solución de referencia en una laptop. La tanda del mundo levanta 18 nodos en unos 15 s, y ocupa unos 600 MiB de memoria: alcanza una laptop de 8 GiB, sin otros laboratorios arriba.

La tanda del mundo falla con un mensaje si falta as/, si as/equipo.env no tiene su número o si no encuentra el mundo. Sin P2 terminado, esa tanda no puede pasar.

La revisión corre además pruebas privadas con otra red, otras tasas de falla, otras semillas, otros archivos y otro número de equipo.

Para correr una sola prueba mientras trabajan:

make && python3 -m unittest -v p5_public_test.LocalSecurity.test_raw_dotdot

Probar a mano

c1 10.95.1.10 ---.
                  r --- srv 10.95.3.10
c2 10.95.2.10 ---'
make topo-up
SRV=$(docker inspect -f '{{.State.Pid}}' clab-p5-wasi-srv)
C1=$(docker inspect -f '{{.State.Pid}}' clab-p5-wasi-c1)
nsenter -n -t $SRV build/wasi 8080 www &
nsenter -n -t $C1 build/wasi_get http://10.95.3.10:8080/index.html
nsenter -n -t $SRV valgrind build/wasi 8081 www --tcp &
nsenter -n -t $C1 curl -v http://10.95.3.10:8081/
make topo-down

Para hablar a mano por TCP: printf 'GET / HTTP/1.1\r\nHost: a\r\n\r\n' | nc 127.0.0.1 8081.

Si una prueba falla

  • wasi terminó al arrancar o rimay_connect falló. No han copiado su src/ de P4 a rimay/.
  • src/http.c llama a .... Muevan eso a wasi.c o a otro archivo: http.c solo usa http_stream.
  • Una ruta con .. da 200 en vez de 404. La revisión va después de decodificar, y sobre los segmentos.
  • Un 304 o un HEAD desordena la respuesta siguiente. Están enviando un cuerpo, o falta uno. Las pruebas leen la respuesta siguiente en la misma conexión.
  • test_pipelining falla. Están borrando el buffer después de cada petición, en vez de quitar solo lo que ya usaron.
  • El servidor muere en test_reset_mid_response. Es SIGPIPE.
  • otro cliente esperó más. Algo se bloquea en el hilo que acepta, muchas veces rimay_close.
  • Con SIGTERM, sigue corriendo. Está esperando a un hilo que no va a volver.
  • Valgrind reporta definitely lost solo con clientes que se van. Es el camino de error: el buffer o el archivo del hilo que salió por una falla de envío.
  • rimay_close se queda colgado en test_both_ends_close_at_once. Es el cierre de su rimay con los FIN cruzados.
  • La bajada falla a los 30 s con el corte. Su red demoró demasiado en encontrar otro camino. Revisen el hold time de BGP y el Dead de OSPF.
  • dns/qhapaq.zone no tiene el registro. Falta el paso 1 de la publicación.