Entendiendo iptables hashlimit

De Jose Castillo Aliaga
Ir a la navegación Ir a la búsqueda

Cuando nos proponemos limitar el ancho de banda o las conexiones a o desde Internet, hay algunas opciones a tener en cuenta. De forma general, todo el mundo te recomienda leer el lartc y tienen razón. Es bastante complicado hacerlo bien y algunos han hecho herramientas como Wondershaper. Esta ha quedado obsoleta ([1]) y no funciona correctamente. Otra alternativa es echar mano de IPtables y su extensión hashlimit.

Antes de empezar, voy a suponer un caso real sobre el que voy a explicar las alternativas: Tenemos un aula con unos 25 pcs y algunos móviles conectados por Wifi. Todos pasan por un Ubuntu server que hace de router. La conexión al exterior es relativamente buena, pero si algunos alumnos usan mucho el youtube o otros servicios de streaming, comienza a ir lento. Nos proponemos limitar el ancho de banda de la forma más respetuosa posible y sin dañar a los que quieren trabajar.

Iptables tiene una extensión llamada hashlimit. Esta es parecida a otra llamada limit. La diferencia es que hashlimit directamente puede trabajar con múltiples IPs de origen y de destino y limit es para toda la tarjeta de red.

El funcionamiento es el siguiente:

  1. Hashlimit registra cada conexión entre ips de origen y de destino.
  2. Si esta supera el límite de conexiones por segundo o de velocidad, ejecuta la regla. (Posiblemente DROP)
  3. Cuando pasa un tiempo, permite de nuevo conectar, pero si se supera la velocidad, vuelve a cortar.

Aquí hay un problema: Muchas conexiones pueden ir muy rápido pero no ser muy pesadas. Podríamos estar cortando conexiones sin parar sólo por la velocidad de la red o del servidor remoto. Para solucionar este problema, hashlimit cuenta con la opció --hashlimit-burst que permite una conexión rápida durante una cantidad máxima de datos.

Entorno de pruebas

Para probar el funcionamiento voy a usar containers LXD. Estos serian los comandos para crear un container que hace de cortafuegos y uno que se conecta a través de él a Internet:

$ lxc launch images:ubuntu/18.04 firewall
$ lxc launch images:ubuntu/18.04 cliente
$ lxc launch images:ubuntu/18.04 server

$ lxc network create switch1 ipv6.address=none ipv4.address=none ipv4.nat=false
$ lxc network create switch2 ipv6.address=none ipv4.address=none ipv4.nat=false
$ lxc network attach switch1 firewall eth1 eth1
$ lxc network attach switch1 cliente eth1 eth1
$ lxc network attach switch1 firewall eth2 eth2
$ lxc network attach switch1 server eth2 eth2

$ lxc exec firewall -- sed -i 's/#net.ipv4.ip_forward=1/net.ipv4.ip_forward=1/' /etc/sysctl.conf
$ lxc exec firewall -- apt update
$ lxc exec firewall -- iptables -A FORWARD -j ACCEPT
$ lxc exec firewall -- iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
$ lxc exec firewall -- apt install iptables-persistent
$ lxc exec firewall -- bash -c 'iptables-save > /etc/iptables/rules.v4'
$ lxc exec firewall -- reboot

Ejemplos con paquetes

Límite en el firewall

Aquí vamos a establecer un límite de 2 paquetes por minuto y un burst de 5.

root@firewall:~# iptables -I FORWARD -m hashlimit --hashlimit-above 2/minute --hashlimit-burst 5 --hashlimit-mode srcip,dstip --hashlimit-name bwlimit -j DROP 

A continuación, desde el cliente hacemos ping a otra màquina (en el ejemplo lo hago cada 5 segundos para tener menos líneas).

root@cliente:~# ping -O -i 5 10.182.234.51
PING 10.182.234.51 (10.182.234.51) 56(84) bytes of data.
64 bytes from 10.182.234.51: icmp_seq=1 ttl=63 time=0.124 ms
64 bytes from 10.182.234.51: icmp_seq=2 ttl=63 time=0.116 ms
64 bytes from 10.182.234.51: icmp_seq=3 ttl=63 time=0.116 ms
64 bytes from 10.182.234.51: icmp_seq=4 ttl=63 time=0.120 ms
64 bytes from 10.182.234.51: icmp_seq=5 ttl=63 time=0.104 ms
no answer yet for icmp_seq=6
64 bytes from 10.182.234.51: icmp_seq=7 ttl=63 time=0.102 ms
no answer yet for icmp_seq=8
no answer yet for icmp_seq=9
no answer yet for icmp_seq=10
no answer yet for icmp_seq=11
no answer yet for icmp_seq=12
64 bytes from 10.182.234.51: icmp_seq=13 ttl=63 time=0.116 ms

Como se puede ver, los primeros 5 pings funcionan, pero luego van fallando todos hasta que, después de un tiempo va dejando pasar otros pero no más de 2 por minuto.

Este es el fichero /proc/net/ipt_hashlimit/bwlimit

59 10.20.30.1:0->10.182.234.51:0 283648 4800000 960000
51 10.182.234.51:0->10.20.30.1:0 283648 4800000 960000
  • El primer número son los segundos que quedan hasta que la regla desaparezca. Sólo bajan si no se producen nuevas peticiones.
  • Después van las IP de origen y destino.
  • El tercer número es el crédito que queda. Si está por debajo del quinto número eliminará los paquetes que entren. Este número aumenta mientras no llegan paquetes nuevos o está eliminando los que van llegando. De esta manera, cuando llega a ser mayor que el quinto número deja pasar un paquete y se decrementa en 96000.
  • El cuarto número es el crédito máximo que se puede tener con el burst.
  • El quinto es la cantidad de crédito que se pierde cada vez que detecta un nuevo paquete. Hay un cálculo interesante que hacer aquí: 4800000/960000 = 5 (El burst)

Límite en el servidor

El ejemplo anterior está hecho en el contenedor que hace de cortafuegos, ya que el ejemplo era el de un aula que se quiere controlar desde ahí. Pero puede estar en el servidor de destino:

# iptables -A INPUT -p icmp -m hashlimit --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-above 20/minute --hashlimit-burst 25 --hashlimit-htable-expire 30000 --hashlimit-name LIMITARICMP -j DROP
##            |          |                                   |                       |                      |                         |                             |                       |
##  (Corta la entrada) (protocolo icmp)  (un hash para cada ip) (Con 32 bits de máscara corta sólo esa IP) (20 paquetes/m)  (25 paquetes inicialmente) (La tabla expira a los 30 segundos) (/proc/net/ipt_hashlimit/LIMITARICMP)   

Miremos el fichero /proc/net/ipt_hashlimit/LIMITARICMP:

 29 10.182.234.6:0->0.0.0.0:0 108672 2400000 96000
  • 29 segundos para que expire (se refresca a 29 cada vez que el cliente intenta un ping)
  • IP del cliente hacia cualquier IP, en este caso la del servidor.
  • 108672: El crédito que tenemos actualmente. Es menor que el crédito de cada token, por tanto, el siguiente paquete será eliminado.
  • 2400000: El crédito total contando el burst.
  • 96000: El crédito que vale cada token y que se descuenta al primer valor cada vez que se acepta un paquete.

Esta regla, cuando se envía un ping cada segundo, elimina 2 de cada 3 pings, excepto los 25 primeros. Es decir, 20 por minuto.

Límite de intentos de login

En el siguiente ejemplo, lo que hacemos es cortar los intentos de SSH:

iptables -A INPUT -p tcp -m tcp --dport 22 -m state --state NEW -m hashlimit --hashlimit-above 2/hour --hashlimit-burst 3 --hashlimit-mode srcip --hashlimit-name SSH --hashlimit-htable-expire 60000 -j DROP
##            |          |                                   |                       |                      |                                                      |                       |
##  (Corta la entrada) (protocolo tcp)      (Conexiones nuevas)          (2 conexiones nuevas por hora) (3 la primera vez)                   (/proc/net/ipt_hashlimit/SSH) (La tabla expira a los 60 segundos)

Esto evita ataques constantes, pero si el atacante espera 60 segundos después de cada 3 intentos, puede intentarlo 3 veces por minuto. Para que sólo se permitan 3 intentos cada hora, habría que poner 3600000 en el tiempo de expiración. Pero el problema es que el administrador si se equivoca 3 veces o hay cortes de comunicación, ha de esperar una hora entera para entrar.

Ejemplos con velocidad

Vamos a analizar esta regla:

root@firewall:~# iptables -I FORWARD -m hashlimit --hashlimit-above 3000kb/s --hashlimit-burst 5mb --hashlimit-mode srcip,dstip --hashlimit-name bwlimit -j DROP

Aquí estamos permitiendo conexiones a velocidades menores de 3000kb/s. Si se detecta una descarga a más velocidad, enviará sus paquetes al DROP. Pero no corta la conexión, ya que después de un poco de tiempo, vuelve a permitirla. Se reanuda y si vuelve a esa velocidad volverá a eliminar los paquetes.

Pero si las descargas son menores a 5mb no se aplica la regla, ya que tiene un hashlimit-burst de 5mb. Esto permite que funcione correctamente en webs poco pesadas.

Para esta prueba, hago una petición desde el cliente por wget a un servidor que tiene un fichero de 100MB:

root@cliente:~# wget 10.182.234.51/100MB.txt

Al principio, wget informa de mucha velocidad, después va bajando la media hasta situarse un poco por debajo de los 3MB/s. Esa velocidad alta inicial son los primeros 5MB (el burst). La conexión, a partir de 5MB comienza a perder muchos paquetes hasta que se alcanza una velocidad media de 3MB/s. Estos paquetes perdidos son reclamados por el protocolo TCP y no se pierden los datos.

El funcionamiento en detalle se puede analizar observando el fichero /proc/net/ipt_hashlimit/bwlimit en él se detallan todas las conexiones detectadas. Vamos a fijarnos en esta línea. Ha aparecido mientras se descargaba un archivo de la 10.20.2.2:

45 192.168.9.100:0->10.20.2.2:0 4194304000 2 65534

El primer número es un contador de 60 segundos hacia atrás. Cuando se produce una conexión a esa velocidad se registra. Si esa conexión se acaba, el contador va hacia atrás hasta que es 0 y desaparece. Mientras existe el contador, los paquetes a más velocidad que 3000kb/s que superen el límte se eliminarán. Mientras continue la conexión y se supere el límite, el contador se refrescará a 60.

Hay un problema con esto, y es que se pierden muchos paquetes. En una conexión como esta, la transferencia se hace a unos 3MB/s, pero el servidor transmite a unos 3,8MB/s. Esa diferencia son paquetes perdidos. Con vnstat podemos comprobar que se han enviado 124MB. Para solucionarlo podemos combinar esto con colas hechas con tc.

Limitar la velocidad con Hashlimit y tc

El comando tc permite crear colas en las que ralentiza la velocidad de envío de paquetes. Estas colas, si los flujos no son muy grandes, no pierden los paquetes y no deben ser reenviados. Al mismo tiempo, reparte de manera más justa el ancho de banda.

Para entender totalmente estas colas, debemos leer la documentación: Linux Advanced Routing & Traffic Control

El siguiente script, utilizado para controlar la velocidad en un aula, muestra cómo configurar hashlimit y tc para reducir la velocidad de descarga desde un ordenador que hace de puerta de enlace:

#!/bin/bash
PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin'
v=$1    # Velocidad
s=$2    # Velocitat del streaming
t=$3    # Tipo de QoS
b=$4    # Burst
m=$5    # Mode

[[ $t == "r" ]] && v=$s
[[ $m == "ban" ]] && mode='dstip'
[[ $m == "streaming" ]] && mode='dstip,srcip'

echo "Borrando iptables"
iptables -t mangle -F
iptables -t mangle -X BULKCONN 
iptables -t mangle -X BULKDIST 

echo "Añadiendo iptables"
iptables -t mangle -N BULKCONN     
iptables -t mangle -N BULKDIST     
iptables -t mangle -I BULKCONN -p tcp -m tcp --sport 80 -j MARK --set-mark 5  # web
iptables -t mangle -I BULKCONN -p tcp -m tcp --sport 443 -j MARK --set-mark 5  # web
iptables -t mangle -I BULKCONN -p tcp -m tcp --sport 22 -j MARK --set-mark 8  # SSH
iptables -t mangle -I BULKCONN -p tcp -m tcp --dport 22 -j MARK --set-mark 8  # SSH 
iptables -t mangle -A BULKDIST -m hashlimit --hashlimit-above 30kb/s --hashlimit-burst ${b}mb --hashlimit-mode $mode --hashlimit-name bwlimit --hashlimit-htable-expire 20000 ! -s 10.100.22.0/24 -j MARK --set-mark 7

iptables -t mangle -A PREROUTING -i eth0 -j BULKCONN
iptables -t mangle -A FORWARD -j BULKDIST

echo "Esborrant cues"
tc qdisc del dev eth1 root    2> /dev/null > /dev/null

echo "Afegint cues"
tc qdisc add dev eth1 root handle 1: htb default 12

tc class add dev eth1 parent 1: classid 1:1 htb rate ${v}kbps ceil ${v}kbps   # 1000kb = 8000Kbits
tc class add dev eth1 parent 1:1 classid 1:10 htb rate ${v}kbps ceil ${v}kbps  # Per a les Webs
tc class add dev eth1 parent 1:1 classid 1:11 htb rate ${v}kbps ceil ${v}kbps # Per al SSH
tc class add dev eth1 parent 1:1 classid 1:12 htb rate ${s}kbps ceil ${s}kbps   ######### 1:12 Streaming #########

echo "Afegint filtre"

tc filter add dev eth1 parent 1: prio 1 protocol ip handle 5 fw flowid 1:10 # port 80
tc filter add dev eth1 parent 1: prio 4 protocol ip handle 8 fw flowid 1:11 # ssh
tc filter add dev eth1 parent 1: prio 3 protocol ip handle 7 fw flowid 1:12 ###### hashslimit streaming #########

echo "Afegint politiques a les cues"
tc qdisc add dev eth1 parent 1:10 handle 20: sfq perturb 10
tc qdisc add dev eth1 parent 1:11 handle 30: sfq perturb 10
tc qdisc add dev eth1 parent 1:12 handle 40: sfq perturb 10

La versión actualizada la podemos ver aquí: [2]

Utilidad

Cortar conexiones que superen una cantidad de paquetes o una cierta velocidad es útil sobretodo para evitar ataques DOS o por fuerza bruta. En los ejemplos hechos en un entorno local y controlado, las velocidades se corresponden con lo que se pide en la regla y la cantidad de paquetes también se ve limitada.

En el caso de FORWARD y limitando la velocidad, en un entorno real con conexiones al exterior, se puede ver algún cliente más perjudicado de lo deseable. Estas son algunas ideas para ajustarlo mejor:

  • Poner un límite muy bajo pero un burst alto. De esta manera, las webs con no muchas cosas siguen funcionando correctamente mientras que el streaming, que es una descarga continuada se va viendo perjudicado al poco tiempo.
  • Poner un límite a dstip sólo para limitar la velocidad de toda una IP de cliente, ya que si ponemos srcip también, permitimos que un cliente tenga muchas conexiones simultáneas.
  • Lo contrario de lo anterior también puede funcionar al limitar una IP de origen, puede que servicios de streaming se perjudiquen más, ya que varios clientes pueden intentar acceder a ellos simultáneamente.

En resumen, para cortar por paquetes para evitar fuerza bruta o DOS es ideal, para limitar la velocidad puede que tc sea más correcto. En concreto, las colas TBF se parecen mucho a esto, pero en vez de eliminar paquetes, los retrasan.

Limitar el tráfico entrante es muy complicado, ya que la única manera es eliminar paquetes válidos. Esto provoca que el Control de Congestión de TCP detecte que el cliente no puede absorber todos los paquetes que envía y puede que retrase más de lo debido los que quedan por enviar. El protocolo TCP/IP no negocia la velocidad, sino que va enviando a toda la velocidad que puede hasta que el receptor no puede ir tan rápido.

Enlaces

tc

http://tlfabian.blogspot.com.es/2014/06/how-does-iptables-hashlimit-module-work.html

https://unix.stackexchange.com/questions/215903/what-do-the-fields-in-proc-net-ipt-hashlimit-file-mean

https://making.pusher.com/per-ip-rate-limiting-with-iptables/