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énHTTP/1.0, da400. Lo correcto en otro servidor sería505, pero aquí la lista de códigos es corta. - Los nombres de los headers no distinguen mayúsculas. Los métodos sí:
getda405. - La ruta empieza con
/. Una URL completa (GET http://...) da400. - 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:
- Se decodifica
%xxuna sola vez.%252ees el texto%2e, un nombre de archivo normal, no un punto. - Se parte por
/. Si algún segmento es exactamente.., la respuesta es404, sin tocar el disco y aunque la ruta se quede adentro:/sub/../index.htmlda404. No se normaliza.a..b.txtes un nombre normal. - Se pega a la carpeta, también si la ruta empieza con
//. Se resuelve conrealpath, y el resultado debe estar dentro de la carpeta ya resuelta. Si no,404. Ojo con comparar solo el prefijo:/www-privadoempieza igual que/www. - Una carpeta se sirve con su
index.html, con barra final o sin ella, y esa ruta pasa por la misma revisión. Sinindex.html,404: no se listan. - 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-TypeyETag, 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 queGET, y ningún byte de cuerpo. También en un404.304:ETag, sinContent-Lengthy sin cuerpo.400,404,405y500:Content-Lengthcorrecto y un texto corto como cuerpo (salvo enHEAD).Content-Typesale de la extensión, sin distinguir mayúsculas:.htmly.htmtext/html;.txttext/plain;.csstext/css;.jstext/javascript;.jsonapplication/json;.pngimage/png;.jpgy.jpegimage/jpeg;.gifimage/gif;.svgimage/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:
recvdevuelve 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_closese 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_sendorimay_recvque devuelve -1 termina la conexión, y no el servidor. En--tcp, usenMSG_NOSIGNALo ignorenSIGPIPE: 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.000si la URL trae una IPv4.conexion: lo que demorarimay_connectoconnect.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
-
Agreguen su registro a
dns/qhapaq.zone, siguiendodns/README.md:www.equipoX.qhapaq.con la dirección pública de susrv. -
Levanten todo.
wasicorre en la red desrv, con sus programas de este contenedor (nsenter), y el servidor DNS corre en la red depeer, en198.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/ -
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
rimaycon TCP y explican los resultados, en suREADME.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 enlaceslista los que se pueden cortar. El desrvconr3y el deafueracontroncalno 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
srvhaciaafuerapasan porproveedor-a. Cortarpeer, por ejemplo, no detiene nada. Contraceroutese 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
curlpuede hablar conwasi --tcp, y los headers y códigos cumplen el enunciado. Reutiliza la conexión.src/http.cfunciona sobre un transporte falso, y no llama arimay_*ni a los sockets.wasi_getbaja porrimayun 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
GETconIf-None-Matchcorrecto devuelve304sin 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. SIGTERMySIGINTterminan con código 0 en menos de 5 s, con clientes bloqueados.- Valgrind no reporta errores ni fugas (
definitely losteindirectly lost) en un ciclo de peticiones, por TCP y porrimay, en servidor y cliente. - En el mundo: su nombre resuelve desde otro AS, la página llega por
rimayy por TCP, los datos pasan por el proveedor A y el corte silencioso deproveedor-ay der3-r4no 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 arrancarorimay_connect falló. No han copiado susrc/de P4 arimay/.src/http.c llama a .... Muevan eso awasi.co a otro archivo:http.csolo usahttp_stream.- Una ruta con
..da200en vez de404. La revisión va después de decodificar, y sobre los segmentos. - Un
304o unHEADdesordena la respuesta siguiente. Están enviando un cuerpo, o falta uno. Las pruebas leen la respuesta siguiente en la misma conexión. test_pipeliningfalla. 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. EsSIGPIPE. otro cliente esperó más. Algo se bloquea en el hilo que acepta, muchas vecesrimay_close.- Con
SIGTERM, sigue corriendo. Está esperando a un hilo que no va a volver. - Valgrind reporta
definitely lostsolo 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_closese queda colgado entest_both_ends_close_at_once. Es el cierre de surimaycon 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.