LinuxParty

Inicio desactivadoInicio desactivadoInicio desactivadoInicio desactivadoInicio desactivado
 

Cada segundo, el sistema pub-sub principal de Pusher maneja 9,000 nuevas conexiones WebSocket. Sin sudar. Pero a principios de este año, cuando el sistema comenzó a recibir picos de 20,000 conexiones nuevas cada segundo por servidor , el sudor comenzó a caer en nuestras frentes. ¿Cuáles o quiénes eran estas nuevas conexiones? ¿Fueron maliciosos o equivocados? Lo más importante, ¿cómo podríamos mantener el sistema funcionando para todos los demás mientras tratamos con esta nueva fuerza misteriosa? Esta es la historia de cómo sofocamos la mayor amenaza para el tiempo de actividad de nuestro servicio durante varios años. El héroe que conocerá hoy es iptables , la herramienta poderosa (pero peligrosa) de Linux para interactuar con la pila de redes. ¡Ven con nosotros y tú también aprenderás a manejar iptables , y sus armas secretas conntrack y hashlimit , para implementar la limitación de velocidad por IP!

De repente, una tarde tranquila en marzo ...

En Pusher, una de nuestras métricas clave de salud es "nuevas conexiones WebSocket por segundo". Cada nueva conexión es, por ejemplo, una página web que hace pusher.connect() . Para nuestro clúster principal, esto funciona a 50 nuevas conexiones por servidor por segundo. Así que nos preocupaba cuando, en el transcurso de un día en marzo, los servidores aleatorios comenzaron a experimentar picos de 1.500 conexiones nuevas por segundo.

picos de conexión

¿Qué es? Un DDOS? Parecía la técnica DDOS llamada "inundación SYN", en la cual el atacante abre muchas conexiones TCP falsas.

Resultó que este no era un DDOS malicioso. En realidad, los clientes defectuosos de WebSocket se implementaron en algún lugar de la naturaleza que estaban atascados en bucles de reintento de conexión, ¡abriendo decenas de miles de conexiones por segundo a una sola aplicación Pusher! No todas estas conexiones se muestran en los gráficos anteriores porque se interrumpieron antes de que nuestro proceso del servidor las informara.

Ruidosos vecinos en el apartamento multiinquilino

El producto insignia de Pusher es un sistema multiinquilino: un solo grupo de servidores Pusher ejecuta aplicaciones para miles de clientes. El arrendamiento múltiple tiene beneficios de eficiencia, pero viene con un inconveniente significativo: ahora existe la posibilidad de que un solo cliente use más que su "parte justa" del sistema y reduzca la calidad del servicio para nuestros otros clientes. Este es el problema del "vecino ruidoso".

Normalmente, los vecinos ruidosos son silenciados por el sistema de límites de Pusher. Para cada inquilino, Pusher limita tanto el número de conexiones como el número de mensajes. Cuando un proceso del servidor recibe una conexión WebSocket, el proceso primero verifica si la cuenta ha alcanzado sus límites, y si es así, cierra la conexión.

Pero en este caso, nuestro sistema de límites no fue suficiente. Nuestros servidores rechazaban las nuevas conexiones de clientes defectuosos, ¡pero solo el acto de manejar y cerrar estas nuevas conexiones generaba una sobrecarga significativa! Descubrimos que los servidores no podían mantenerse al día con las conexiones cuando se propagaban a nuestro proceso de servidor de espacio de usuario.

La única opción que nos quedaba era bloquear las conexiones en el núcleo. La solución que buscamos fue utilizar el sistema de firewall de Linux, Netfilter, que a su vez puede ser manipulado por la herramienta iptables .

Corrección n° 1: bloquear una IP con iptables

Netfilter permite que los módulos del núcleo definan funciones de devolución de llamada que se ejecutan cuando los paquetes son enviados o recibidos por la pila de red del núcleo. Estas funciones comúnmente realizarán operaciones como la traducción de direcciones o puertos, y lo más importante para nosotros, pueden descartar paquetes por completo. Aquí está la tarea para esta sección: descartar todos los paquetes de una IP específica en la lista negra. Lo guiaremos a través de la implementación de esto.

iptables es un programa de usuario y una herramienta de línea de comandos para manipular las funciones de devolución de llamada de Netfilter. Conceptualmente, iptables se basa en los conceptos de reglas y cadenas . Una regla es una pequeña pieza de lógica para hacer coincidir paquetes. Una cadena es una serie de reglas con las que se comprueba cada paquete, en orden. Los paquetes eventualmente terminan en uno de un conjunto predefinido de objetivos , que determinan qué se hace con el paquete. Los objetivos clave son ACCEPT el paquete o DROP el paquete 1 . Cada regla define dónde "saltar" si un paquete coincide; Esto puede ser a otra cadena o un objetivo. Si la regla no coincide, el paquete se compara con la siguiente regla de la cadena. Cada cadena tiene una política , que es a la que se dirigen los paquetes objetivo si llegan al final de la cadena sin coincidir con ninguna regla.

Veamos cómo puede agregar una regla que descarte todos los paquetes de la dirección IP 123.123.123.123 . La regla se puede agregar con el siguiente comando:

$ sudo iptables --append INPUT --source 123.123.123.123 --jump DROP

Una palabra de advertencia: iptables puede ser peligroso. (¡El sudo debería indicar esto!) Para seguir en casa, debe usar una máquina virtual desechable. La clásica historia de terror de iptables está cambiando a la máquina, agregando una regla que no funciona y luego, de repente, la conexión ssh se rompe. ¡Felicitaciones, su regla de iptables rota bloqueó sus paquetes SSH! Si tiene suerte, puede tomar un taxi hasta el centro de datos para reparar la máquina. Lección: ¡siempre prueba tus comandos de iptables a fondo en una VM local!

Advertencias con, analicemos el comando anterior. En inglés, se traduce como: agregar una regla a la cadena INPUT (la cadena a la que llegan todos los paquetes destinados a este servidor) y si un paquete coincide con la IP de origen 123.123.123.123 , luego suéltelo.

Vamos a visualizar cómo se ve esta cadena ahora. Siga el diagrama a continuación: si recibe un paquete de IP 1.2.3.4 , ¿a qué objetivo llega?

cadenas anotadas

En el shell, no obtienes un diagrama bonito como ese. En su lugar, ejecuta iptables --list . Aquí está el equivalente textual del diagrama anterior:

$ sudo iptables --list
Chain INPUT (policy ACCEPT)
target     prot opt source               destination
DROP       all  --  123.123.123.123      anywhere

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

Con la regla anterior, podemos bloquear las direcciones IP en la lista negra para que nunca se comuniquen con nuestro servicio. Esto es genial de una vez, ¡pero es bastante brutal! Primero, solo necesitábamos bloquear nuevas conexiones (identificadas por el indicador TCP SYN) y dejar intactas las conexiones actuales. En segundo lugar, solo necesitábamos limitar la velocidad de estas conexiones, no bloquearlas por completo.

Resulta que dicha limitación de velocidad no es posible con iptables . Todas las reglas de iptables tienen estado, pero la limitación de velocidad requiere un estado (para contadores). Para obtener más potencia y flexibilidad, necesitamos módulos de iptables . Estos módulos son necesarios para definir reglas más complejas y con estado, como un limitador de velocidad de conexión. Primero está un módulo llamado conntrack . conntrack al rescate!

Módulos warmup: los módulos conntrack y log

Como introducción a los módulos de iptables , creemos una regla que registre todas las conexiones TCP nuevas. Para hacer esto, usaremos dos módulos: conntrack para encontrar nuevas conexiones TCP y log para registrarlas.

Las reglas normales de iptables funcionan independientemente en cada paquete IP. Pero para calificar las conexiones de límite, necesitamos rastrear las "conexiones", así como los paquetes. Para esto, existe el sistema de seguimiento de conexión Netfilter . Esto mantiene un conjunto de todas las conexiones y actualiza los estados de conexión a medida que llegan nuevos paquetes. La información de conexión de capa 3 y 4 identifica una conexión única, por ejemplo, la dirección IP y el puerto, respectivamente. Para acceder a esta información de conexión, utilizamos conntrack , que es un módulo de iptables . Con conntrack , puede crear reglas que accedan a la conexión del paquete actual. Por ejemplo, --match conntrack --ctstate NEW solo coincidirá con los paquetes donde la conexión esté en el estado NEW . Aquí, -m define un módulo para usar en la regla, y los parámetros posteriores se aplican a ese módulo.

El registro es proporcionado por un módulo llamado log . El módulo de log define un nuevo objetivo: LOG . Si una regla salta a este objetivo, se registra una cadena en el syslog con la función kern (en /var/log/kern.log en Debian / Ubuntu por defecto). LOG es un "objetivo sin terminación", lo que significa que la siguiente regla se verifica en la cadena inmediatamente antes de saltar a LOG , independientemente de si la regla de registro coincide o no. Agreguemos una nueva regla para registrar cada paquete descartado:

$ sudo iptables --flush  # start again
$ sudo iptables --append INPUT --protocol tcp --match conntrack \ 
--ctstate NEW --jump LOG --log-prefix "NEW TCP CONN: "

conntrack + log

Compruebe su comprensión: si la tabla anterior recibe un paquete de 10.0.0.51 , ¿se registrará? Echa un vistazo en el registro del sistema:

$ tail -f /var/log/kern.log
Aug 30 15:47:32 ubuntu-xenial kernel: [10766.412639] NEW TCP CONN: \ 
IN=enp0s8 OUT= MAC=08:00:27:dd:80:b3:08:00:27:e2:1b:be:08:00 \
SRC=10.0.0.51 DST=10.0.0.50 LEN=60 TOS=0x00 PREC=0x00 \
TTL=64 ID=20232 DF PROTO=TCP SPT=33528 DPT=1234 WINDOW=29200 RES=0x00 SYN URGP=0
Aug 30 15:48:39 ubuntu-xenial kernel: [10833.521111] NEW TCP CONN: \
IN=enp0s8 OUT= MAC=08:00:27:dd:80:b3:08:00:27:e2:1b:be:08:00 \
SRC=10.0.0.51 DST=10.0.0.50 LEN=60 TOS=0x00 PREC=0x00 \
TTL=64 ID=42506 DF PROTO=TCP SPT=42156 DPT=1233 WINDOW=29200 RES=0x00 SYN URGP=0

Advertencia: puede parecer que funciona correctamente, pero puede que no. Si lo deja por un tiempo, es posible que vea líneas como esta:

nf_conntrack: table full, dropping packet.

Nunca le dijimos a conntrack que dejara caer paquetes; ¡solo le dijimos que registrara nuevas conexiones! Resulta que conntrack tiene un número máximo de conexiones rastreadas, y una vez que conntrack alcanza este límite, ¡se conntrack todas las conexiones nuevas! Los detalles son bastante feos, 2 pero en resumen debe establecer el máximo lo suficientemente alto para evitar conexiones caídas, y debe establecer un "número de cubos" lo suficientemente alto como para evitar un bajo rendimiento:

$ conn_count=$(sysctl --values net.netfilter.nf_conntrack_count)
$ sysctl --write net.netfilter.nf_conntrack_max=${conn_count}  
# Set this much higher than your conn count!
# ¡Ajuste esto mucho más alto que su cuenta de conexión!
$ sysctl --write net.netfilter.nf_conntrack_buckets=$((${conn_count}/4))
# Technical reasons, see footnote
# Razones técnicas, ver nota al pie.

Después de todo esto, tiene un sistema que distingue NEW conexiones de las ESTABLISHED ( y las de otros estados ). Con esto, podríamos saltar a DROP lugar de LOG para bloquear todas las conexiones nuevas. Esto sería una mejora con respecto a nuestro intento anterior, que bloqueó brutalmente todas las conexiones. Pero todavía no es lo que queremos. No queremos bloquear todas las conexiones nuevas, solo aquellas que excedan una cierta tasa. Para esto, necesitamos otro módulo: el módulo de limit .

Arreglo # 2: Limitación de velocidad con el módulo de limit

El módulo de limit permite la limitación de velocidad contra todos los paquetes que alcanzan una regla. Primero crearemos una nueva cadena, RATE-LIMIT . Enviaremos paquetes a la cadena RATE-LIMIT si están en el NEW estado de conexión. Luego, en la cadena RATE-LIMIT , agregaremos la regla de limitación de velocidad.

$ sudo iptables --flush  # start again
$ sudo iptables --new-chain RATE-LIMIT
$ sudo iptables --append INPUT --match conntrack --ctstate NEW --jump RATE-LIMIT

Luego, en la cadena RATE-LIMIT , cree una regla que no coincida con más de 50 paquetes por segundo. Estas son las conexiones que aceptaremos por segundo, así que salte a ACCEPT . (Explicaré --limit-burst luego).

$ sudo iptables --append RATE-LIMIT --match limit --limit 50/sec --limit-burst 20 --jump ACCEPT

La tasa limit regla limita los paquetes al no coincidir con ellos, por lo que caen a la siguiente regla. Estos paquetes los descartaremos:

$ sudo iptables --append RATE-LIMIT --jump DROP

límite

El límite de velocidad (arriba, 50/sec ) se aplica de la siguiente manera. Cada regla de limit tiene una cuenta bancaria que almacena "créditos". Los créditos son fichas que se gastan en paquetes coincidentes. La regla de límite solo gana créditos de una manera: a través de su salario, que es un crédito por "tick". La regla anterior gana un crédito cada 20 ms, lo que restringe su gasto a un máximo de 50 partidos por segundo. Cuando llega un nuevo paquete, si la regla tiene al menos un crédito, el crédito se gasta y el paquete coincide. De lo contrario, el paquete se cae debido a fondos insuficientes. Ahora, siga el diagrama anterior: ¿se aceptará o descartará un paquete de 5.5.5.5 ?

Este esquema de "crédito" permite el tráfico "explosivo". Si se conectan 100 usuarios diferentes al mismo tiempo, queremos permitirles a todos. La tolerancia del esquema de crédito a la fluctuación aleatoria es deseable, pero tiene un efecto secundario indeseable. Tenga en cuenta que, durante la noche, cuando los usuarios están dormidos, la regla podría ganar una gran cantidad de créditos, que luego se pueden gastar para permitir a los usuarios sobrecargar el sistema por la mañana cuando todos abren las conexiones a la vez. Queremos permitir el tráfico "explosivo", pero solo hasta un límite.

Esto es exactamente lo que: --limit-burst . El límite de ráfaga es un límite en la cantidad de créditos que la regla puede tener en su cuenta. La regla anterior solo permite 20 créditos. Si la regla ya tiene 20 créditos cuando ocurre una marca, no gana más créditos. Esta lógica de "úselo o piérdalo" evita enormes acumulaciones de crédito y, por lo tanto, evita la sobrecarga del sistema. (El límite de ráfaga también es el número inicial de créditos de la regla, por lo que no tiene que esperar para acumular créditos). Vea un ejemplo:

límite de explosión

Otra prueba! Si una regla de limit se configura como --limit 50/sec --limit-burst 20 , y luego recibe 1 paquete cada milisegundo durante un período de 1 segundo, ¿cuántos paquetes se emparejarán?

Sin el módulo de limit , estábamos bloqueando todas las conexiones nuevas. Esto fue inútil, y el módulo de limit es una gran mejora: ahora podemos evitar que nuestro servidor sea destruido por un gran número de nuevas conexiones. Pero hay una limitación fundamental con el limit en nuestro sistema multiinquilino: aplica este límite de tasa a nivel mundial. Si un solo cliente excede el límite, se eliminarán las conexiones de los clientes que usan su parte justa.

Una solución sería hacer coincidir una lista negra de direcciones IP de origen. Pero esto requeriría que agreguemos manualmente nuevas direcciones IP a las tablas (o implementemos nuestro propio sistema para hacerlo). Idealmente, queremos limitar la velocidad de cada dirección IP de origen por separado. Para esto es exactamente el módulo hashlimit .

Solución # 3: hashlimit velocidad por dirección IP con hashlimit

hashlimit generaliza el módulo de limit . Mientras que limit aplica un límite único globalmente a todas las conexiones, hashlimit aplica límites a grupos de conexiones (un grupo puede ser una única dirección de origen). Esto funciona de manera similar al módulo de limit , pero ahora cada grupo de conexión obtiene su propia cuenta de crédito.

Todas las reglas de limit se pueden definir con hashlimit poniendo todas las conexiones en un gran grupo global. Esto es lo que hace hashlimit por defecto. Entonces podemos definir la regla de limit anterior con hashlimit como este:

$ sudo iptables --flush  # start again
$ sudo iptables --new-chain RATE-LIMIT
$ sudo iptables --append RATE-LIMIT \
    --match hashlimit \
    --hashlimit-upto 50/sec \
    --hashlimit-burst 20 \
    --hashlimit-name conn_rate_limit \
    --jump ACCEPT
$ sudo iptables --append RATE-LIMIT --jump DROP

Para limitar el IP de origen, debemos indicarle a hashlimit que hashlimit por dirección de IP de origen. Hacemos esto con el parámetro --hashlimit-mode , que define cómo agrupar los paquetes. Con --hashlimit-mode srcip , creamos un grupo por IP de origen:

$ sudo iptables --append RATE-LIMIT \
    --match hashlimit \
    --hashlimit-mode srcip \
    --hashlimit-upto 50/sec \
    --hashlimit-burst 20 \
    --hashlimit-name conn_rate_limit \
    --jump ACCEPT
$ sudo iptables --append RATE-LIMIT --jump DROP

hashlimit

Sigue el diagrama. Si entra un paquete en la conexión desde 1.2.3.4:3456 , ¿qué pasa con el paquete?

Al igual que conntrack , hashlimit tablas hashlimit tienen un número máximo de entradas, y no debe permitir que la tabla se llene. Puede ver las entradas de la tabla hash actual en /proc/net/ipt_hashlimit/conn_rate_limit . Debe establecer --hashlimit-htable-max más alto que el número de líneas. También debe establecer --hashlimit-htable-size en max / 4. 3

¡Éxito!

Finalmente tuvimos las herramientas para calificar nuevas conexiones de límite: conntrack y hashlimit . Este es el momento satisfactorio cuando se implementaron las reglas:

picos de conexión desaparecidos

Este fue un gran éxito: nuestros servidores dieron un gran suspiro de alivio y el ruidoso vecino fue silenciado efectivamente. Además, no hubo un impacto notable en el rendimiento de estas reglas de iptables , en relación con todo lo demás que estamos ejecutando. ¡Esto es sorprendente teniendo en cuenta que estas reglas se ejecutan contra cada paquete que llega al sistema!

Vigilando tus conexiones caídas

La eliminación de paquetes es arriesgada porque si se comete un error en la regla, las conexiones legítimas se eliminarán en silencio. Para vigilar sus conexiones de tarifa limitada, tiene un par de opciones. Uno es iptables --list --verbose , que muestra la cantidad de paquetes que han coincidido con una regla; el recuento está debajo de la columna pkts . Para obtener más información, puede usar el módulo de log de antes. Agreguemos una nueva regla para registrar cada paquete descartado:

$ sudo iptables --append RATE-LIMIT \
    --match hashlimit \
    --hashlimit-mode srcip \
    --hashlimit-upto 50/sec \
    --hashlimit-burst 20 \
    --hashlimit-name conn_rate_limit \
    --jump ACCEPT
$ sudo iptables --append RATE-LIMIT --jump LOG --log-prefix "IPTables-Rejected: "
$ sudo iptables --append RATE-LIMIT --jump REJECT

Puede resultarle excesivo registrar cada paquete que se descarta; lo más probable es que sea tan útil si registramos una muestra. ¿Cómo podemos hacer esto? Respuesta: el módulo de limit nuevo! Veamos cómo podemos hacer esto:

$ sudo iptables --append RATE-LIMIT --match limit --limit 1/sec --jump LOG --log-prefix "IPTables-Rejected: "

Esto significa que solo se registrará un paquete descartado por segundo. Creo que esta es una demostración clara de cómo estos módulos simples y generales se pueden componer en reglas; Hemos utilizado el módulo de limit para lograr dos cosas que son superficialmente muy diferentes: ¡límite de velocidad y registro!

hashlimit + log

Iptables persistentes

iptables cadenas y reglas de iptables se almacenan en la memoria. Si reinicia la máquina, se pierden. Por esa razón, iptables tiene una herramienta para guardar y cargar definiciones de reglas. Para guardar el conjunto actual de reglas en un archivo, ejecute

$ sudo iptables-save | tee rules.txt
*filter
:INPUT ACCEPT [66:3398]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [45:3516]
:RATE-LIMIT - [0:0]
-A RATE-LIMIT --match hashlimit --hashlimit-upto 50/sec --hashlimit-burst 20 \
--hashlimit-mode srcip --hashlimit-name conn_rate_limit -j ACCEPT -A RATE-LIMIT --match limit --limit 1/sec -j LOG --log-prefix "IPTables-Rejected: " -A RATE-LIMIT -j REJECT --reject-with icmp-port-unreachable COMMIT

Para restaurar las reglas ejecutadas: 4

$ sudo iptables-restore < rules.txt

Tenemos esta ejecución en el inicio como una tarea inicial .

Conclusión

Esperemos que hayamos demostrado el poder y la flexibilidad de iptables : con solo unos pocos comandos, implementamos un limitador de velocidad por IP muy eficiente, y los casos de uso van mucho más allá de la limitación de velocidad. iptables tiene fama de ser hostil. Sí, es fácil matar su servidor con un comando incorrecto, ¡pero también es posible salvar su servidor de los atacantes con solo un buen comando! Sí, iptables es incómodo, de bajo nivel y pierde muchos detalles de implementación, pero su eficiencia va mucho más allá de lo que puede lograr en su proceso de usuario. Y sí, iptables está mal documentado, ¡pero espero que esta publicación de blog ayude a remediar eso!

Notas al pie

  1. También puede REJECT paquetes enviando una respuesta de error ICMP , que podría informar al cliente de lo que está haciendo mal. En estos ejemplos nos apegaremos a DROP por simplicidad. ↩

  2. El módulo conntrack rastrea sus struct estado de conexión en una tabla hash global. La mayoría de las implementaciones de tablas hash aseguran un buen rendimiento al redimensionar dinámicamente su número de cubos, pero el módulo conntrack no implementa este cambio de tamaño. En cambio, el número de cubos de la tabla hash se establece estáticamente con nf_conntrack_buckets . Esto deja a conntrack abierto a la degradación del rendimiento si el número de entradas en la tabla aumenta más que el número de depósitos. Para protegerse contra esta degradación del rendimiento, conntrack tiene este número máximo artificial de entradas, nf_conntrack_max . La relación entre las dos configuraciones, nf_conntrack_buckets y nf_conntrack_max , es importante para el rendimiento. La documentación de conntrack recomienda nf_conntrack_max = nf_conntrack_buckets*4 , pero deja este problema al usuario.

    ¡Esto impone una gran carga al usuario de conntrack ! Debe evitar los paquetes descartados configurando nf_conntrack_max encima del número máximo de entradas de la tabla hash de su servidor. Esto significa que debe saber ese número máximo, pero esto depende de muchas cosas desordenadas: conexiones concurrentes, varios TTL configurados y patrones de acceso a tablas hash. nf_conntrack_buckets , debe evitar la degradación del rendimiento al configurar nf_conntrack_buckets en línea con su nf_conntrack_max elegido. También debe asegurarse de que esta configuración se conserve en su archivo de configuración sysctl . ↩

  3. Al igual que conntrack , hashlimit utiliza tablas hash que no cambian de tamaño dinámicamente y protege contra el mal rendimiento con un número máximo de entradas configurables. También como conntrack , hashlimit te permite dispararte en el pie con un número incorrecto de cubos. ↩

  4. iptables-restore no es totalmente idempotente: restablecerá los recuentos de paquetes. ↩

Pin It

Escribir un comentario


Código de seguridad
Refescar



Redes:



 

Suscribete / Newsletter

Suscribete a nuestras Newsletter y periódicamente recibirás un resumen de las noticias publicadas.

Donar a LinuxParty

Probablemente te niegues, pero.. ¿Podrías ayudarnos con una donación?


Tutorial de Linux

Nos obligan a moslestarte con la obviedad de que este sitio utiliza Cookies. Ver política