P0 · Calentamiento

Dos programas en C para practicar sockets, medir la red y conocer la forma de entregar. Se usa lo visto en las semanas 1 y 2. Entrega en la semana 3.

1. eco: un servicio con TCP

eco_server <puerto> atiende a varios clientes a la vez, usando poll, y eco_client <host> <puerto> lee mensajes de la entrada estándar, uno por línea, y muestra lo que el servidor devuelve.

Cada mensaje viaja así, porque TCP no conserva fronteras entre mensajes:

Campo Tamaño Contenido
length 4 bytes Largo de payload, sin signo y en orden de red
payload length bytes Los datos, de hasta 1 MiB (1 048 576 bytes), incluido el 0

El payload son bytes cualquiera, no un string: puede traer ceros y fines de línea.

Reglas del servidor que revisan las pruebas

  • Devuelve cada mensaje idéntico y completo, sin importar en cuántos pedazos llegue. Varios mensajes juntos en un solo send también valen, y vuelven en orden.
  • Atiende al menos 100 conexiones abiertas a la vez, y los mensajes de un cliente nunca se mezclan con los de otro.
  • Un cliente lento no detiene a los demás. Eso incluye dos casos: uno que se detiene a la mitad de un mensaje, y uno que envía sin leer sus ecos.
  • Un cliente que envía sin leer no infla el servidor: mientras su respuesta no salga, se deja de leer de esa conexión. Con un cliente así, el servidor se queda por debajo de 64 MiB de memoria. Cuando ese cliente vuelve a leer, recibe todos sus ecos, completos y en orden.
  • Si length pasa de 1 MiB, el servidor cierra solo esa conexión, apenas lee el encabezado, y sigue atendiendo.
  • Si un cliente se desconecta a la mitad de un mensaje, o sin leer su eco, el servidor libera sus recursos (descriptor y memoria) y sigue.
  • Si un cliente cierra solo su lado de escritura (shutdown), igual recibe los ecos pendientes; después, el servidor cierra.
  • Se puede reiniciar de inmediato en el mismo puerto (SO_REUSEADDR).
  • Con argumentos inválidos (el puerto va de 1 a 65535) o con el puerto ocupado, termina con un código distinto de 0 y un mensaje en stderr.

Reglas del cliente que revisan las pruebas

  • Cada línea es un mensaje, sin su fin de línea. Una línea vacía es un mensaje de largo 0. La última línea cuenta aunque no termine en fin de línea.
  • Muestra cada eco seguido de un fin de línea, apenas lo recibe, aunque la entrada siga abierta.
  • <host> puede ser una dirección o un nombre. Si el nombre tiene varias direcciones, las prueba hasta que una responda.
  • Al terminar la entrada y mostrar los ecos, termina con código 0.
  • Termina con un código distinto de 0 y un mensaje en stderr si: no se puede conectar, los argumentos están incompletos, una línea pasa de 1 MiB (esa línea no se envía), o el servidor cierra a la mitad de un eco. También termina con error si el servidor anuncia un length mayor que 1 MiB.

2. traza: una versión propia de traceroute

traza [-m max_saltos] [-w espera_ms] <destino> envía datagramas UDP con TTL creciente y lee las respuestas ICMP con un socket crudo. Solo IPv4. <destino> es una dirección o un nombre.

Por defecto, max_saltos es 30 y espera_ms es 1000. max_saltos va de 1 a 255 y espera_ms debe ser al menos 1. Se envía una sola sonda por salto. Cada salto imprime una línea, y el programa termina al recibir Port Unreachable del destino o al llegar a max_saltos:

<salto> <dirección> <rtt en ms, con 3 decimales>
<salto> *

Reglas que revisan las pruebas

  • En los dos casos de término, el código de salida es 0. Con argumentos inválidos, un nombre que no se resuelve o sin permisos para abrir el socket crudo, termina con un código distinto de 0 y un mensaje en stderr.
  • <salto> * significa que no llegó respuesta en espera_ms, contados desde que se envió esa sonda. Los paquetes ajenos que lleguen en ese lapso no renuevan el plazo.
  • Cuentan como respuesta Time Exceeded y cualquier Destination Unreachable que citen una sonda de esta ejecución y de este salto. <dirección> es el origen del ICMP.
  • Port Unreachable termina el programa solo si viene del destino. Si viene de otro router, o si es otro Destination Unreachable, se imprime la línea y se sigue con el salto siguiente.
  • El socket crudo recibe todo el ICMP de la máquina. Una respuesta es de ustedes solo si el paquete citado dentro del ICMP coincide: destino, puerto de origen y puerto de destino. La respuesta a otro programa, por ejemplo un ping u otro traceroute con los mismos puertos de destino, no cuenta. Tampoco cuenta una respuesta que llega tarde, cuando ya pasó su salto.
  • Cada línea sale apenas se conoce ese salto, no al final.

Sugerencia: usen el puerto de destino 33434 + salto - 1, como el traceroute clásico, y fijen el puerto de origen con bind antes de la primera sonda. Lean el encabezado IP con su campo IHL: no asuman que mide 20 bytes.

Archivos

Archivo Qué hace ¿Lo modifican?
src/eco_server.c, src/eco_client.c, src/traza.c Sus programas Sí
src/*.c, src/*.h adicionales Código común: cualquier otro .c se enlaza con los tres programas Si quieren
Makefile Compila y corre las pruebas No
p0_public_test.py Pruebas públicas No
topo/ Topología de las pruebas de traza No

Agreguen sus pruebas en archivos test_*.py, en esta misma carpeta: make test las corre. Corren después de las públicas, con la topologí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        # compila, levanta la topología, corre todo y destruye la topología
make test-eco    # solo eco, sin topología
make test-traza  # solo traza
make clean       # destruye la topología y borra lo generado

make test compila con -Wall -Wextra -std=c17 y exige que no haya advertencias. Corre el servicio con varios clientes, mensajes partidos y clientes que fallan, y corre los tres programas con valgrind. Por último, compara la salida de traza con el camino que dicen las tablas de los routers, también con un router callado, con un enlace lento y con otro tráfico ICMP en paralelo. Las pruebas tienen márgenes amplios y ninguna depende de un tiempo exacto. Si una se queda esperando, falla sola a los 120 s.

Las pruebas corren en una red propia, con buffers de TCP chicos a propósito. En loopback normal, Linux da varios MiB de buffer a cada conexión y un mensaje de 1 MiB siempre cabe, así que un send que se bloquea o escribe a medias pasaría desapercibido. Por eso, un servidor que funciona a mano con nc o eco_client puede fallar aquí: no es la prueba, es lo que pasaría en una red real.

La revisión corre además pruebas privadas con otra topología, otras direcciones, otro largo de camino y otros tamaños. Las pruebas leen el camino de las tablas de los routers: no escriban direcciones a mano en su código.

Probar traza a mano

Los routers de la topología son contenedores Alpine, sin gcc y con otra biblioteca de C, así que un programa compilado aquí no corre adentro. No hace falta: traza se compila y se corre en el contenedor del curso, y solo entra a la red de un nodo con nsenter. Sigue usando los archivos y las bibliotecas de este contenedor, así que funciona igual en amd64 y arm64:

make topo-up
make traza ARGS="-m 8 10.91.3.10"     # corre en h1, hacia h2
h1 10.91.1.10 --- r1 --- r2 --- r3 --- 10.91.3.10 h2

El mismo truco sirve para gdb, valgrind o tcpdump en la red de h1:

H1=$(docker inspect -f '{{.State.Pid}}' clab-p0-traza-h1)
nsenter -n -t $H1 gdb --args build/traza 10.91.3.10
nsenter -n -t $H1 tcpdump -n -i eth1 icmp or udp

Para hacer ping o mirar las tablas de otro nodo: docker exec clab-p0-traza-r2 ip route. Al terminar, make topo-down.

Si una prueba falla

  • eco_client demora unos 40 ms por mensaje. Es el algoritmo de Nagle junto con el ACK retrasado: un send del encabezado y otro del payload hacen esperar al segundo. Armen el mensaje en un solo buffer o activen TCP_NODELAY.
  • El servidor muere sin mensaje con clientes que se van. Es SIGPIPE: revisen send con MSG_NOSIGNAL, o ignoren la señal.
  • La salida aparece recién al final. Si stdout es un pipe, printf guarda en un buffer: llamen a fflush.
  • traza encuentra asteriscos en un camino que anda. Abran el socket crudo antes de enviar la primera sonda. Si no, se pierden respuestas.