Fundamentos de Redes: Protocolos TCP, UDP y Gestión de Sockets

Protocolo UDP: Fundamentos y Comportamiento

¿Qué sucede si por error UDP recibiera un datagrama destinado a otra máquina?

La cabecera UDP incluye un checksum opcional, que utiliza para su cálculo la dirección IP de destino, entre otros campos, para comprobar la integridad del datagrama UDP y si ha llegado al destino correcto. Hay dos opciones:

  • Checksum a cero: Si el checksum es cero, indica que no se ha calculado o verificado. Por lo tanto, el proceso UDP intentará entregar los datos a la aplicación que esté escuchando en el puerto especificado en el datagrama UDP, sin verificar la integridad ni el destino.
  • Checksum distinto de cero: Si el checksum tiene un valor, al comprobarlo, se detectaría el error (ya sea por corrupción de datos o por una dirección IP incorrecta en la pseudocabecera) y el datagrama se descartaría.

¿Por qué es necesario incluir el checksum en IP, TCP y, opcionalmente, en UDP, cuando a nivel de trama ya se aplica uno?

El checksum a nivel de trama (capa de enlace) solo detecta errores a nivel de transmisión dentro de un mismo segmento de red. Sin embargo, cuando un router intermedio extrae el datagrama IP, lo comprueba y lo vuelve a encapsular, pueden ocurrir errores de procesamiento en las cabeceras IP, TCP o UDP que no serían detectados por el checksum de la trama. Los checksums de IP, TCP y UDP, en cambio, verifican la integridad de sus respectivas cabeceras y datos a lo largo de todo el camino, desde el origen hasta el destino, detectando errores que podrían surgir en los nodos intermedios (como routers) durante el procesamiento y reencapsulación de los paquetes.

¿Cómo se puede distinguir a qué aplicación debe entregar UDP el datagrama que acaba de llegar?

Tras verificar si el checksum es correcto (en caso de que se haya utilizado), UDP entregará el contenido del datagrama a la aplicación que se encuentre escuchando en el puerto de destino. Esta información (el número de puerto de destino) se especifica claramente en la cabecera del mensaje UDP.

¿Tiene algún sentido hablar de conexión entre dos computadoras que se comunican mediante UDP?

No, no lo tiene, puesto que UDP no es un servicio orientado a conexión, sino un protocolo sin conexión (connectionless). Esto significa que no establece una sesión previa ni mantiene un estado de conexión entre el emisor y el receptor. Cada datagrama UDP se envía de forma independiente, sin garantías de entrega, orden o duplicación.

Si la pseudocabecera no se transmite, ¿cómo se puede comprobar en el destino si el checksum es correcto?

En el origen, se calcula el checksum sobre el datagrama UDP, al que se le anexa una pseudocabecera (que no se transmite por la red, solo se usa para el cálculo). Cuando el destino recibe el datagrama UDP, primero debe reconstruir la pseudocabecera, solicitando al protocolo IP la información necesaria (direcciones IP de origen y destino, y el campo de protocolo). Para la verificación, se guarda el valor del checksum del datagrama UDP entrante y en su lugar se coloca un 0. Luego, se adjunta la pseudocabecera reconstruida al datagrama UDP y se calcula un nuevo checksum. Si el checksum calculado no coincide con el que venía en el datagrama UDP original, significa que hay un error y el datagrama se descarta. Si coincide, se continúa con el procesamiento del datagrama UDP.

Protocolo TCP: Control de Flujo y Congestión

¿Por qué es necesario usar timeouts que se adapten a las condiciones dinámicas de la red?

Dado que el RTT (Round Trip Time) de una conexión TCP puede cambiar radicalmente debido a factores como la congestión de la red, la carga de los servidores o la distancia geográfica, no podemos tener un timeout fijo. Un timeout demasiado pequeño provocaría retransmisiones innecesarias de segmentos que aún no se han perdido, aumentando la carga en la red. Por el contrario, un timeout demasiado grande resultaría en una lenta reacción ante segmentos perdidos, lo que reduciría drásticamente el rendimiento de la conexión. Por ello, TCP ajusta dinámicamente el valor del timeout basándose en las mediciones del RTT.

Control de Congestión TCP: ¿En qué consiste el procedimiento conocido como Slow-Start? ¿Cuándo se aplica? Explicar brevemente.

Slow-Start es un algoritmo fundamental de control de congestión del protocolo TCP. Su objetivo es evitar la saturación de la red al inicio de una conexión o después de una pérdida significativa de paquetes. Ni el emisor ni el receptor tienen forma de saber cuál es el máximo volumen de datos que la red puede transmitir en un momento dado, ya que carecen de información sobre la capacidad de los elementos intermedios de la red. Si la red se satura, comenzará a descartar paquetes, lo que provocará retransmisiones y puede incrementar aún más la congestión.

La solución que plantea este algoritmo consiste en comenzar enviando un volumen de datos pequeño (una ventana de congestión inicial reducida, típicamente 1 MSS). Esta ventana se irá aumentando exponencialmente (duplicando su tamaño por cada ACK recibido) hasta que se detecte congestión (por pérdida de paquetes o ACKs duplicados) o hasta que se alcance un umbral predefinido (ssthresh). Se aplica al inicio de una conexión TCP y después de un timeout de retransmisión.

En el protocolo TCP, la duración de los temporizadores para la retransmisión es crítica. ¿Qué sucede si su duración es demasiado corta? ¿Qué pasa si, por el contrario, su duración es demasiado larga?

  • Timeout largo: Provoca una lenta reacción ante la pérdida de segmentos, lo que reduce el rendimiento y la eficiencia de la conexión TCP.
  • Timeout corto: Genera retransmisiones innecesarias de segmentos que aún no se han perdido, aumentando la carga en la red y en los endpoints, y potencialmente contribuyendo a la congestión.

El protocolo TCP utiliza un control de flujo basado en ventana deslizante. Las ventanas de recepción son de tamaño variable, pudiendo cerrarse completamente. ¿Qué utilidad puede tener esto? ¿Por qué no se definen de tamaño fijo, facilitando su manejo?

En TCP, el tamaño de las ventanas de recepción es variable. Cuando se establece la conexión, se negocian los tamaños iniciales de las mismas, pudiendo variar a lo largo de la conexión. Esto permite realizar un control de flujo extremo a extremo que TCP gestiona dinámicamente en función de los recursos disponibles en el receptor (como la memoria buffer). Si el receptor se ve sobrecargado y no puede procesar los datos a la misma velocidad que los recibe, puede reducir el tamaño de su ventana de recepción (o incluso cerrarla completamente) para indicar al emisor que detenga o ralentice el envío de datos. Esto es crucial para evitar la pérdida de paquetes por desbordamiento del buffer del receptor, optimizando así la fiabilidad y el rendimiento de la comunicación. Un tamaño fijo no permitiría esta adaptación dinámica a las condiciones cambiantes del receptor.

En una LAN con RTT estimado de 1ms, indica la velocidad máxima de transmisión que debe alcanzar esta red (ancho de banda efectivo) para que se logre una utilización del 100% de su ancho de banda con una única conexión TCP.

Para lograr una utilización del 100% del ancho de banda con una única conexión TCP, la velocidad máxima de transmisión (ancho de banda efectivo) se calcula utilizando la fórmula de rendimiento de la ventana TCP:

  • Ventana TCP máxima (típica): 64 KB (65.536 bytes)
  • RTT (Round Trip Time): 1 ms (0.001 segundos)
  • Velocidad máxima = Ventana / RTT
  • Velocidad = 65.536 bytes / 0.001 s = 65.536.000 bytes/s
  • Convertido a bits/s: 65.536.000 bytes/s * 8 bits/byte = 524.288.000 bits/s
  • Resultado: Aproximadamente 524 Mbps (Megabits por segundo).

Una conexión TCP abierta, está caracterizada en uno de sus extremos por los siguientes parámetros:

(Nota: Los parámetros iniciales de la conexión no fueron proporcionados en el documento original. Se asume un CWND inicial y un ssthresh para ilustrar los cambios.)

Asumiendo un CWND inicial de 4 MSS y un ssthresh inicial de 10 MSS, y una RWND constante de 20 MSS:

  • T1 – Se recibe un nuevo ACK válido:

    Si se recibe un ACK válido durante la fase de Slow-Start, la ventana de congestión (CWND) aumenta exponencialmente. Si partimos de 4 MSS, al recibir un ACK, la CWND aumentaría a 8 MSS. Los demás parámetros (RWND, ssthresh) permanecen constantes.

  • T2 – Se recibe un nuevo ACK válido:

    La ventana de congestión (CWND) pasaría a 9 MSS (si el incremento es de 1 MSS por ACK en fase de Congestion Avoidance, o si sigue en Slow-Start y se duplica a 16 MSS, pero el ejemplo sugiere un incremento lineal). La ventana de transmisión efectiva sería la menor entre la ventana de recepción (RWND = 20 MSS) y la ventana de congestión (CWND = 9 MSS), por lo tanto, sería 9 MSS.

  • T3 – Se recibe un segmento con indicación de ventana de recepción de 20 MSS:

    La ventana de congestión (CWND) seguiría aumentando (por ejemplo, a 10 MSS si sigue en Congestion Avoidance). La ventana de recepción (RWND) se actualiza a 20 MSS. La ventana de transmisión efectiva (min(CWND, RWND)) sería 10 MSS.

  • T4 – Se produce un timeout:

    Al producirse un timeout, la ventana de congestión (CWND) se reiniciaría a 1 MSS y se aplicaría nuevamente el algoritmo de Slow-Start. El umbral (ssthresh) se reduciría a la mitad del CWND anterior al timeout (por ejemplo, si el CWND era 10 MSS, el ssthresh se establecería en 5 MSS). La ventana de recepción (RWND) seguiría siendo 20 MSS.

Programación de Sockets y Llamadas al Sistema

¿Qué diferencia hay entre las llamadas a close() y shutdown()? ¿Cuándo utilizarías shutdown() en vez de close()?

La diferencia principal radica en su alcance y propósito:

  • close(): Esta llamada elimina el socket y libera todos los recursos del sistema operativo asociados a él. Una vez que se llama a close(), el socket ya no puede ser utilizado para ninguna operación de envío o recepción, y si es el último descriptor de archivo para ese socket, la conexión se termina completamente.
  • shutdown(): Realiza un cierre de conexión más detallado y controlado. Permite cerrar solo una dirección de la conexión (solo envío, solo recepción, o ambas) sin liberar completamente el socket y sus recursos. Esto significa que el socket puede seguir siendo utilizado para la dirección que no se ha cerrado.

Se utilizaría shutdown() en vez de close() para sockets de tipo SOCK_STREAM (TCP) en situaciones donde se desea:

  • Indicar al par remoto que no se enviarán más datos, pero aún se desea recibir datos de él (SHUT_WR).
  • Indicar que no se recibirán más datos, pero aún se desea enviar datos (SHUT_RD).
  • Cerrar ambas direcciones de la conexión de forma controlada, pero mantener el descriptor del socket abierto por alguna razón (por ejemplo, para esperar a que el par remoto cierre su lado de la conexión antes de liberar completamente los recursos con close()).

Un hacker transmite mensajes TCP con el bit RST activado en el puerto 80. ¿Qué pretende? ¿Por qué no tiene efecto? ¿Qué le recomendarías?

  • ¿Qué pretende? El hacker pretende reiniciar o abortar abruptamente una conexión TCP existente en el puerto 80 (típicamente HTTP). Esto podría ser parte de un ataque de denegación de servicio (DoS) o un intento de interrumpir comunicaciones legítimas.
  • ¿Por qué no tiene efecto? Un paquete con el bit RST activado es ignorado por el receptor si no corresponde a una conexión TCP establecida o si los números de secuencia y ACK en el paquete RST no son válidos o no están dentro de la ventana esperada para una conexión activa. TCP es robusto ante RSTs espurios para evitar que conexiones legítimas sean terminadas fácilmente por atacantes que no conocen el estado exacto de la conexión.
  • ¿Qué le recomendarías? Para que un RST tenga efecto, el atacante debería haber establecido previamente una conexión TCP válida o, al menos, conocer los números de secuencia y ACK esperados por el receptor para esa conexión específica. Sin esta información precisa, el paquete RST es simplemente descartado por el sistema operativo del receptor.

¿Por qué al hacer la llamada bind() en un servidor siempre debemos especificar la dirección del socket local? ¿Por qué no es necesario especificar la dirección local cuando el bind() se hace a través de un connect() en un cliente?

  • En un servidor: Se especifica la dirección del socket local (bind()) para asociarlo a una dirección IP específica (o INADDR_ANY para escuchar en todas las interfaces disponibles) y un número de puerto conocido y fijo. Esto es fundamental para que los clientes puedan encontrar y conectarse a ese servicio específico en la red.
  • En un cliente: No es necesario llamar explícitamente a bind() antes de connect(). La función connect() asigna automáticamente al socket del cliente una dirección IP local (la de la máquina) y un número de puerto efímero (libre y temporal) del sistema operativo. El cliente no necesita un puerto conocido de antemano, ya que es él quien inicia la conexión hacia un servidor con un puerto conocido.

¿Qué efecto tiene una llamada a la función connect() sobre un socket UDP?

Aunque UDP es un protocolo sin conexión, la llamada a connect() en un socket UDP tiene un efecto particular:

  • Establece una asociación predeterminada con una dirección IP y un puerto de destino específicos.
  • Tras el connect(), se pueden enviar datos a ese destino predefinido sin necesidad de especificar la dirección en cada llamada a send() (se puede usar send() en lugar de sendto()).
  • Además, el socket UDP solo aceptará datagramas entrantes que provengan de esa dirección y puerto específicos, filtrando otros datagramas. Esto añade una capa de «pseudo-conexión» o filtrado al socket UDP.

¿Para qué sirve la llamada al sistema bind()?

La llamada al sistema bind() se utiliza para asignar una dirección local (una dirección IP y un número de puerto) a un socket recién creado. Esto es esencial para:

  • Identificar de forma única el socket dentro del sistema local.
  • En el caso de los servidores, permite que el socket escuche en un puerto específico, haciendo que el servicio sea accesible para los clientes.

¿Para qué sirve la llamada al sistema listen()?

La llamada al sistema listen() se utiliza en servidores TCP para:

  • Poner el socket en modo pasivo, indicando que está listo para aceptar conexiones entrantes.
  • Especificar el tamaño máximo de la cola de conexiones pendientes (clientes que han intentado conectarse pero aún no han sido aceptados por accept()).

Es un paso crucial en la secuencia de creación de un servidor TCP, ejecutándose después de bind() y antes de accept(), y es exclusiva para sockets de tipo SOCK_STREAM (TCP).

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.