Configuración de redes en debian

Propósito

El documento pretende mostrar cómo pueden configurarse las interfaces de red de un servidor montado sobre una distribución debian, desde la necesidad más simple que es dotar a una interfaz de una configuración válida a casos muchísimo más complicados en que se tienen varias interfaces y se quiere que el tráfico transite por ellas. No hay, porque no es habitual, mención a la configuración de interfaces inalámbricas.

La mejor forma de probar todo lo expuesto aquí es hacer uso de máquinas virtuales que permitan emular el comportamiento completo de un ordenador y la existencia de redes separadas. A este efecto tanto qemu como VirtualBox son adecuados. También es posible hacerlo sobre LinuX Containers. Si lo que se quiere tan sólo es comprobar la validez de la sintaxis de los comandos es posible usar interfaces dummy.

Es importante reseñar que para acabar de manipular y encauzar completamente el tráfico, quizás se necesite redirigir y enmascarar tráfico modificando las orígenes y destinos de los paquetes. Esto último se hace fundamentalmente con iptables, aunque en los casos en que hay definidas interfaces bridge habrá también que echar mano de ebtables.

Al hilo de la configuración de interfaces el autor ha desarrollado un script global de configuración en python que a partir de una declaración de interfaces en formato xml genera el fichero /etc/network/interfaces pertinente. En realidad el script tiene un propósito mucho más amplio y de esta tarea se encarga uno de sus módulos.

La última parte del documento está dedicada al control de tráfico, también conocido como calidad de servicio.

Hay algunas partes del texto dedicado a la configuración de interfaces marcadas como desaconsejadas. Esto es así, porque presentan scripts propios quizás no lo suficientemente comprobados y que pueden no funcionar en todos los casos o crear incompatibilidades con otros scripts oficiales para ifupdown que provean los paquetes de debian. Si se usan, han de usarse con la previsión de que, si ocurre algún error, debe intentarse también la configuración manual para averiguar si el error está en la propia configuración ideada o en la escritura deficiente de los scripts.

Interfaces

Nombre de las interfaces

Por lo general, en linux las interfaces ethernet cableadas reciben el nombre de ethX, siendo X un número natural incluido el 0: 0, 1, 2, 3, etc. Además, suele ser indispensable tener definida la interfaz virtual de loopback (o de bucle interno), que recibe el nombre de lo.

El número que reciba cada interfaz física de forma natural depende del orden en que sean sean detectadas durante el arranque, de manera que la primera recibirá el nombre eth0, la segunda eth1, etc. Sin embargo, como se verá más adelante, el sistema conserva la memoria de las interfaces que tuvo conectadas, de modo que esta regla general comúnmente acaba por no cumplirse. Por ejemplo, supongamos una máquina con una tarjeta de red, cuyo sistema ya ha sido ejecutado al menos una vez. En este caso, la tarjeta se llamará eth0, puesto que no hay ni ha habido otra. Si tiempo después conectamos otra interfaz de red, podría darse el caso de que esta nueva interfaz sea detectada antes durante el proceso de arranque. Sin embargo, la interfaz primitiva ya tiene por nombre eth0 y lo conservará, así que esta nueva se nombrará eth1, a pesar de ser detectada antes. De hecho, la conclusión no habría cambiado si hubiéramos hecho desaparecer la interfaz primitiva: aunque ya no existiera seguiría ocupando el índice 0 y la única interfaz disponible (la nueva interfaz) sería eth1. Por supuesto, todo esto es manipulable y ya se verá cómo: lo primero es conocer cuáles son las interfaces disponibles en nuestro sistema.

En lo referente a estas interfaces disponibles, hay dos consideraciones distintas: las interfaces de red que fisicamente existen en nuestro sistema; y las interfaces que verdaderamente están disponibles. No son exactamente las mismas, puesto que puede haber interfaces disponibles que sean virtuales y no físicas (lo es el mejor ejemplo); o interfaces físicas que no estén disponibles, porque no haya driver para ellas.

Para consultar las interfaces físicas detectadas por el sistema (todo el hardware moderno es PnP) basta con usar lspci, si la tarjeta es pci, o lsusb, si es usb (esto último será muy poco habitual en servidores):

# lspci | grep -i ethernet
01:01.0 Ethernet controller: Atheros Communications Inc. Atheros AR5001X+ Wireless Network Adapter (rev 01)
01:05.0 Ethernet controller: Realtek Semiconductor Co., Ltd. RTL-8110SC/8169SC Gigabit Ethernet (rev 10)

En el ordenador en que ahora mismo escribo hay dos interfaces físicas: una interfaz wireless con chip de Atheros AR5001X+, y otra cableada con chip de Realtek RTL-8110SC/8169SC.

Por otro lado, la consulta de las interfaces disponibles, puede hacerse listado el contenido de /sys/class/net:

$ ls /sys/class/net
eth0  lo  wlan0

O bien, observando la salida del comando ip:

$ ip link show | grep -oP '\w+(?=: \<)'
lo
eth0
wlan0

En ambos casos se obtienen obviamente las mismas interfaces: la de loopback (lo), la interfaz de cable (eth0) y la interfaz wireless (wlan0), cuya descripción se sale del propósito de este documento.

Manipulación de los nombres

En ocasiones, el nombrado que hace linux de las interfaces no es el que nos parece más conveniente: bien porque tenemos interés en asignarle a una determinada interfaz un nombre concreto, bien porque han desaparecido interfaces cuyo nombre queremos recuperar.

El culpable de asignar estos nombres es udev que apunta en el fichero /etc/udev/rules.d/70-persistent-net.rules los nombres que se asignan a cada interfaz

El contenido de este fichero es como el que sigue:

$ cat /etc/udev/rules.d/70-persistent-net.rules
# This file was automatically generated by the /lib/udev/write_net_rules
# program, run by the persistent-net-generator.rules rules file.
#
# You can modify it, as long as you keep each rule on a single
# line, and change only the value of the NAME= key.

# PCI device 0x10ec:0x8167 (r8169)
SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="00:1a:4d:32:4f:04", ATTR{dev_id}=="0x0", ATTR{type}=="1", KERNEL=="eth*", NAME="eth0"

# PCI device 0x168c:0x0013 (ath5k)
SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="00:1b:11:b4:71:db", ATTR{dev_id}=="0x0", ATTR{type}=="1", KERNEL=="wlan*", NAME="wlan0"

En el fichero se asocian direcciones MAC con nombres de interfaz. Basta con alterar estas asociaciones para cambiar el nombre real de la interfaz. Nótese que se ha añadido el calificativo real, porque este nombre a su vez podrá ser cambiado con el comando ip.

Interfaces dummy

Linux dispone de un módulo llamado dummy que genera interfaces ficticias: no tienen utilidad como interfaces de accesos a la red, pero sí dentro de la propia máquina. Podemos, por ejemplo, poner a escuchar servicios en estas interfaces y hacer oruebas locales de conexión. O, para el caso que nos ocupa, usarlas para configurarlas y desconfigurarlas, sin más utilidad que comprobar que efectivamente funcionan nuestros comandos.

Lo primero es cargar el módulo:

# modprobe dummy numdummies=2

El parámetro numdummies le indica al módulo cuántas interfaces ficticias de red queremos crear. Si no lo especificamos, se creará sólo una. Hecho lo anterior, podemos comprobar que ahora disponemos de dos interfaces más:

# ls /sys/class/net/
br0  dummy0  dummy1  eth0  lo

A partir de ahora podemos tratar estas interfaces como otra interfaz de red ethernet más, aunque sabiendo que no conectan en realidad con ninguna red. Si se quiere probar alguno de los comandos que se presentan a continuación y no se dispone de software de virtualización, se puede echar mano de estas interfaces.

Lo que se avecina

Hemos comenzado afirmando que las interfaces de cable ethernet en linux se denominan con el prefijo eth seguido de un número consecutivo. Siempre ha sido así, pero muy recientemente, por mor de systemd, tan recientemente que no ha pillado a jessie, pero sí se verá en stretch, la nomeclatura ha cambiado.

Así pues, si su propósito es tratar con jessie (como lo es en general el de estos apuntes) o una versión aún más antigua, no tiene porque cuidarse de lo expuesto bajo el presente epígrafe: sus interfaces se llamarán ethX y no hay mucho más que saber. En caso contrario, o que le pìque la curiosidad y no desee encontrarse con sorpresas en el futuro, continué leyendo.

El problema de la nomenclatura antigua es que el sistema va asignado los nombres a las interfaces según van siendo cargadas en el sistema con el driver apropiado, pero este orden de carga es impredecible; así que no podemos asegurar que una interfaz que ha sido llamada eth0, siga siendo eth0 en el futuro. Por ejemplo, podría darse el caso de que en un sistema de una interfaz pincháramos una tarjeta adicional y que al arrancarlo esta nueva tarjeta fuera la primera en cargarse. Como consecuencia, la nueva tarjeta sería eth0 y la antigua pasaría a ser eth1. Como en nuestro sistema teníamos configurada la interfaz eth0, dicha configuración que se pensó para una interfaz se aplicaría a la otra. Esto puede derivar, incluso, en problemas de seguridad.

La defensa de jessie frente a este problema está en el fichero /etc/udev/rules.d/70-persistent-net.rules, que ya se vio que guerdaba el nombre de las interfaces asociandolo a la mac.

systemd ensaya otro método distinto: asignar nombres de manera que estos estén más ligado al rol de la propia interfaz (léanse los principios que argumentan los responsables de freedesktop.org). La consecuencia es que el prefijo de las interfaces pasará a ser eno para interfaces integradas, ens para interfaces PCI, enx para interfaces en que se use un nombre formado a partir de su propia MAC, o eth si se decide renunciar e estas novedades y hacer que linux siga asignando los nombres del mismo modo que hasta ahora.

Si se desea esto último, basta añadir a las opciones del kernel net.ifnames=0, lo cual puede hacerse cómodamente en debian, editando el fichero /etc/default/grub:

GRUB_CMDLINE_LINUX_DEFAULT="quiet net.ifnames=0"

En caso contrario, las interfaces recibirán nombres que comienzan por eno o ens. Por ejemplo, en una máquina virtual qemu con esta tarjeta:

$ lspci | grep Ethernet
00:10.0 Ethernet controller: Intel Corporation 82540EM Gigabit Ethernet Controller (rev 03)

La tarjeta recibe el nombre ens16 (ya que 0x10 es 16 en decimal). No obstante, puede seguir escribiéndose el fichero 70-persistent-net.rules para asignar los nombres que deseemos (incluidos los tradicionales).

Si deseamos usar los nombres basados en la MAC, podremos también copiando y alterando el fichero /lib/udev/rules.d/80-net-setup-link.rules:

# cp /lib/udev/rules.d/80-net-setup-link.rules /etc/udev/rules.d/

La modificación consiste en alterar una línea de la copia para dejarla así:

NAME=="", NAME="$env{ID_NET_NAME_MAC}"

Podemos conocer los posibles nombres que puede adoptar una interfaz con la orden:

$ udevadm test-builtin net_id /sys/class/net/eth0 2>/dev/null
ID_NET_NAME_MAC=enxdeadbeef9634
ID_NET_NAME_PATH=enp0s16
ID_NET_NAME_SLOT=ens16

Nótese que la interfaz no tiene por qué llamarse eth0.

Configuración básica

Para la configuración de las interfaces podemos usar una herramienta universal, válida para cualquier sistema linux o utilizar el método de definción que proporcina debian y sirve en cualquier distribución que derive de ella. Es recomendable usar este segundo método, pero conviene conocer el primero, por si nos encontramos en algún momento frente a una distribución distinta.

Método universal

El método universal se basa en el uso del comando ip (de iproute2) cuando la configuración es estática; y en el uso de un cliente dhcp cuando la configuración es dinámica.

Configuración estática

La configuración estática de una interfaz puede desglosarse en tres aspectos:

  1. La asignación de ip a la interfaz.
  2. La asignación de una puerta de enlace.
  3. La definición de los servidores DNS para la resolución de nombres.

Para asignar ip a una interfaz basta con usar el comando ip dos veces: una para la asignación en sí y otra para levantar la interfaz:

# ip addr add 192.168.1.25/24 dev eth0
# ip link set eth0 up

Para ver lo que hemos hecho, podemos pedir la configuración de la dirección de eth0 (sin expresar qué interfaz se quiere ver se muestran todas):

$ ip addr show eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether de:ad:be:ef:84:3b brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.25/24 brd 192.168.1.255 scope global eth0
       valid_lft forever preferred_lft forever

Obsérvese que se muestra la dirección ip, pero también el estado de la interfaz: UP significa que está habilitada y LOWER_UP que el cable se haya conectado al otro extremo a un dispositivo de red. Así pues, si la interfaz tiene una configuración correcta, deberíamos poder conectar a los dispositivos de la misma red. Si queremos salir de la red, sin embargo, tendremos que definir también la puerta de enlace predeterminada (supongamos que es la 192.168.1.1):

# ip route add 0.0.0.0/0 via 192.168.1.1

Habría sido posible usar la palabra default en vez de declarar explícitamente 0.0.0.0/0:

# ip route add default via 192.168.1.1

Por último, es necesario definir los servidores DNS que se usarán para resolver nombres dentro del fichero /etc/resolv.conf. Este fichero deberá tener este aspecto:

$ cat /etc/resolv.conf
domain milan.com
search milan.com
nameserver 8.8.8.8
nameserver 8.8.4.4

Los servidores se declaran con líneas que comienza con la palabra nameserver. La línea domain debería contener el nombre del dominio al que pertenece la propia máquina, y la línea search los dominios de búsqueda con los que se completará un nombre cuando de este no se indique ningún dominio, sino sólo el nombre de la máquina.

Para desactivar y desconfigurar una interfaz podemos recurrir otra vez a ip:

# ip link set eth0 down
# ip addr flush eth0

Al desactivar una interfaz, todas las entradas en la tabla de encaminamiento que tengan una puerta de enlace en la misma red que la interfaz desactivada, desaparecerán.

Aunque es recomendable usar el comando ip, es habitual que aún esté disponible en linux el comando ifconfig o quizás, si es muy antiguo, sólo esté disponible este último. En este caso, el primero y segundo paso se realizan del siguiente modo:

# ifconfig eth0 192.168.1.25 netmask 255.255.255.0 up
# route add default gw 192.168.1.1

Para bajar y desactivar la interfaz:

# ifconfig eth0 0.0.0.0 netmask 0.0.0.0 down

Configuración dinámica

Para obtener la configuración automática de un servidor DHCP es necesario usar un cliente. El más habitual es dhclient, aunque en algunos linux mínimos, como los empotrados, se usa udhcpc.

Para pedir una configuración con dhclient:

# dhclient -v eth0

Esto configurará todo lo necesario, incluidos el encaminamiento y los servidores DNS. Si se quiere cancelar la petición:

# dhclient -r

Método de debian

Consiste en el uso del paquete ifupdown que utiliza la configuración de interfaces que de declaren dentro del fichero /etc/network/interfaces.

Configuración estática

Para configurar estáticamente una interfaz se pueden incluir las siguientes líneas:

auto eth0
iface eth0 inet static
   address 192.168.1.25
   gateway 192.168.1.1

Como no se ha indicado ningún valor para la máscara (para lo cual se puede usar la palabra netmask), se presupone la máscara predeterminada 255.255.255.0. Además, la línea auto eth0 indica que la interfaz debe será activada automáticamente durante el arranque. Una variante a esto es allow-hotplug eth0. La diferencia es que en este caso se activará (y desactivará) según los eventos que reciba el sistema sobre el enlace, dicho de otro modo: si se detecta el cable conectado, se procederá a activar la interfaz; si se detecta en algún momento la desconexión del cable, se procederá a la desactivación.

Si quisiéramos que la interfaz perteneciera a la red 192.168.0.0/23 entonces habría que indicar la máscara forzosamente:

auto eth0
iface eth0 inet static
   address 192.168.1.25
   netmask 255.255.254.0
   gateway 192.168.0.1

o bien, usar la notación CIDR:

auto eth0
iface eth0 inet static
   address 192.168.1.25/23
   gateway 192.168.0.1

También pueden indicarse las ip de red (network) y de broadcast (broadcast), pero no es necesario puesto que debian es capaz de calcularlas.

Por último, si se quiere activar o desactivar una interfaz, se pueden usar las órdenes ifup e ifdown:

# ifdown eth0    # Desactiva eth0, si estaba activa
# ifup eth0      # Activa eth0, si estaba desactiva

La configuración de los servidores dns puede hacerse escribiendo manualmente en /etc/resolv.conf o bien escribirse directamente en /etc/network/interfaces, si se tiene instalado el paquete resolvconf:

auto eth0
iface eth0 inet static
   address 192.168.1.25
   dns-nameservers 8.8.8.8 8.8.4.4

Configuración dinámica

La configuración por dhcp es semejante, aunque en este caso no hay que especificar ningún dato:

auto eth0
iface eth0 inet dhcp

Consulta de interfaces

Cuando se configuran interfaces a través de ifupdown, es útil conocer qué interfaces tenemos declaradas en el fichero de configuración y cuáles hemos habilitado y deshabilitado con ifup e ifdown. Para ello vale el comando ifquery.

Supongamos que tenemos declaradas tres interfaces: lo, eth0 y eth1, pero sólo lo y eth0 se habilitaron con ifup (o la directiva auto que las habilitó durante el arranque). En este caso:

# ifquery --list
lo
eth0
eth1

y, sin embargo:

# ifquery --state
lo=lo
eth0=eth0

Lo primero nos da todas las interfaces declaradas en /etc/network/interfaces con auto; lo segundo, sin embargo, nos muestra las interfaces que se habilitaron mediante ifup. Incluso si eth1 estuviera habilitada de forma manual, no aparecería en el segundo listado de interfaces habilitadas.

Si se desea listas las interfaces declaradas con allow-hotplug se puede hacer lo siguiente:

# ifquery --list --allow=hotplug

Configuración avanzada

Si echamos un vistazo a versiones recientes de debian, veremos que existe un directorio llamado /etc/network/interfaces.d, que permite separar las definiciones de las distintas interfaces en distintos ficheros. Esto es así, porque dentro de /etc/network/interfaces existe incluida de serie una directiva source para que se lean todos los ficheros que se incluyen en dicho directorio:

source interfaces.d/*

Puede ser buena idea usar esta posibilidad si tenemos muchas interfaces definidas en nuestro servidor.

debian permite ejecutar órdenes antes de levantar una interfaz (pre-up), tras haberla levantado (up), antes de bajar la interfaz (down) o juntamente después de haberla bajado (post-down). Hay dos modos de indicarlo:

  • Directamente en el fichero en que se declaran las interfaces. Por ejemplo, si quisiéramos enmascarar todo lo que saliera por la interfaz eth0, podríamos hacer lo siguiente:
    auto eth0
    iface eth0 inet static
       address 172.22.0.2
       pre-up   iptables -t nat -A POSTROUTING -o $IFACE -j SNAT --to-source $IF_ADDRESS
       post-down iptables -t nat -D POSTROUTING -o $IFACE -j SNAT --to-source $IF_ADDRESS
  • Creando ejecutables dentro de los directorios if-pre-up.d, if-up.d, if-down.d y if-post-down.d que hay en /etc/network.

Si se observan las líneas de ejemplo escritas, se verá que se han usado las variables IFACE e IF_ADDRESS, que almacenan el nombre y la ip de la interfaz, respectivamente. Esto es posible porque el sistema de gestión de interfaces de debian facilita una serie de variables para que puedan ser usadas tanto en las líneas empotradas en el fichero interfaces, como en los ejecutables independientes. Pueden consultarse aquí todas, aunque las más interesantes son:

NombreSignificado
$IFACENombre de la interfaz.
$METHODMétodo de consecución de la ip (static, dhcp, etc.).
$MODEstart/stop según activación o desactivación.
$PHASEpre-up, up, down o post-down.
$IF_<OPT>Valor de la opción <OPT>.

Hay una diferencia bastante importante entre incluir directamente los comandos o crear un ejecutable aparte: cuando se incluyen líneas, estas pertenecen a la configuración de una interfaz y, por tanto, sólo se ejecutan cuando se levanta o baja la interfaz. En cambio, cuando se crean ejecutables, estos se ejecutan sea cual sea la interfaz que se levanta o baja, y hay que obrar en consecuencia. Por ejemplo, lo equivalente a las dos líneas anteriores, sería crear dos ficheros:

$ cat /etc/network/if-pre-up.d/masquerade
[ "$IFACE" = "eth0" ] || exit 0
iptables -t nat -A POSTROUTING -o $IFACE -j SNAT --to-source $IF_ADDRESS

$ cat /etc/network/if-post-down.d/masquerade
[ "$IFACE" = "eth0" ] || exit 0
iptables -t nat -D POSTROUTING -o $IFACE -j SNAT --to-source $IF_ADDRESS

En los que si la interfaz no es eth0 se acaba la ejecución. Dado que ambos ficheros son exactamente iguales salvo por el hecho de que hay que añadir (-A) o borrar (-D) la regla, podríamos crear un único fichero que analizara si estamos levantando o bajando una interfaz:

$ cat /etc/network/if-pre-up.d/masquerade
[ "$IFACE" = "eth0" ] || exit 0
[ "$MODE" = "start" ] && A="A" || A="D"
iptables -t nat -$A POSTROUTING -o $IFACE -j SNAT --to-source $IF_ADDRESS

y crear un enlace simbólico:

# cd /etc/network/if-post-up.d
# ln -s ../masquerade

Linux como bridge

Nuestro servidor linux puede configurarse con dos (o más) interfaces en una misma red, de modo que cada cable conectado a una de esas interfaces pertenezca a un segmento distinto:

Servidor acuando como puente

El esquematizado es el caso más sencillo en que el servidor se comporta como un switch de dos puertos (eth0 y eth1), esto es, como bridge o puente.

Para resolver esto, lo que se hace es crear una interfaz virtual bridge (a las que se les dar por nombre br0, br1, etc.) a la que se añaden todas las interfaces que queramos que participen en la red. En este caso, no se produce encaminamiento de capa 3, como se produciría si el servidor separara dos redes distintas, sino conmutación de capa 2.

Además, si queremos que el servidor sea accesible desde la red, a la interfaz bridge se le puede dar una dirección ip de dicha red.

Método universal

Tradicionalmente se ha venido usando el comando brctl del paquete bridge-utils. Sin embargo, iproute2 ya soporta la creación y gestión de interfaces bridge, de modo que con él preferentemente será como se ilustre lo que debe hacerse.

Partiendo de que no hay ninguna interfaz configurado ni habilitada, lo primero es crear la interfaz bridge:

# ip link add name br0 type bridge

Creada la interfaz, opcionalmente, se le puede asignar una dirección MAC concreta a voluntad. Por ejemplo, si queremos asignarle la MAC de eth0:

# ip link set br0 addr $(< /sys/class/net/eth0/address)

Ahora podemos añadir las dos interfaces que participarán en el puente:

# ip link set eth0 master br0
# ip link set eth1 master br0

Por último, debemos habilitar las tres interfaces y asegurarnos de que las dos interfaces físicas están en modo promiscuo:

# ip link set eth0 promisc on
# ip link set eth0 up
# ip link set eth1 promisc on
# ip link set eth1 up
# ip link set br0 up

Con esto ya tendríamos creado y activado el puente y debería haber comunicación entre las máquinas ambos segmentos. Podemos echas un vistazo a cómo ha quedado la configuración con el comando:

# bridge link show br0
2: eth0 state UP : <BROADCAST,MULTICAST,PROMISC,UP,LOWER_UP> mtu 1500 master br0 state forwarding priority 32 cost 19 
3: eth1 state UP : <BROADCAST,MULTICAST,PROMISC,UP,LOWER_UP> mtu 1500 master br0 state forwarding priority 32 cost 19

También podemos asignarle una ip a la interfaz bridge para poder acceder al servidor a través de la red (supongamos que es la 172.22.0.0/16):

# ip addr add 172.22.0.2/16 dev br0

Si queremos deshacer el puente, basta desligar las interfaces físicas del puente y eliminarlo finalmente:

# ip link set eth0 nomaster
# ip link set eth1 nomaster
# ip link del br0

Para usar brctl hay que instalar antes el paquete bridge-utils:

# aptitude install bridge-utils

El proceso es equivalente al anterior: crear la interfaz bridge, añadir las interfaces requeridas y levantarlas. brctl nos permite realizar las operaciones de creación y adición (y las contrarias de desligado y destrucción):

# brctl addbr br0
# brctl addif br0 eth0
# brctl addif br0 eth1

Para levantar las interfaces, en cambio, usaremos ip (o ifconfig). brctl también permite ver los componentes del bridge:

# brctl show
bridge name      bridge id                STP enabled     interfaces
br0              8000.deadbeefc22e        no              eth0
                                                          eth1

Por último, el proceso inverso de deshacer el puente es también sencillo:

# brctl delif eth0
# brctl delif eth1
# brctl delbr br0

Algo que no puede hacerse con bridge o ip de iproute2 es habilitar el protocolo SPT:

# brctl stp br0 on

Método de debian

Se necesita tener instaladas las bridge-utils, que contienen scripts en if-pre-up.d y if-post-down.d para ifupdown:

# aptitude install bridge-utils

Para el sencillo bridge anterior, basta esta configuración mínima, que se encarga de levantar las interfaces y poner las en modo promiscuo sin necesidad de declararlo:

auto eth0
iface eth0 inet manual

auto eth1
iface eth1 inet manual

auto br0
iface br0 inet manual
   bridge_ports eth0 eth1
   bridge_maxwait 2

Podemos añadir una directiva address a la declaración de br0, si queremos que el bridge disponga de ip en la red.

Aunque el anterior es el método recomendable y estándar, si deseamos prescindir de bridge-utils, podemos intentar construir dos scripts que usen iproute2. El primero:

Script para if-pre-up-d y if-post-down.d

deberá colocarse en if-pre-up.d y en if-post-down.d hacerse un enlace simbólico al él:

# cd /etc/network
# cp /donde/se/encuentre/if-pre-up-bridge-iproute2.txt if-pre-up.d/bridge-iproute2
# chmod +x if-pre-up.d/bridge-iproute2
# ln -s ../if-pre-up.d/bridge-iproute2 if-post-down.d/bridge-iproute2

El segundo script:

Script para if-pre-up-d y if-post-down.d

debe colocarse en if-up.d y hacerse un enlace simbólico a él en if-down.d:

# cd /etc/network
# cp /donde/se/encuentre/if-up-bridge-iproute2.txt if-up.d/bridge-iproute2
# chmod +x if-up.d/bridge-iproute2
# ln -s ../if-up.d/bridge-iproute2 if-down.d/bridge-iproute2

Estos scripts esperan que nos refiramos a las interfaces involucradas en el fichero interfaces de esta manera:

auto br0
iface br0 inet manual

allow-hotplug eth0
iface eth0 inet manual
   master br0

allow-hotplug eth1
iface eth1 inet manual
   master br0

Hay algunas puntualizaciones que hacer sobre los scripts de automatización:

  • Si se instala el paquete bridge-utils, se deshabilitan los scripts.
  • El script, para adivinar si queremos crear una interfaz bridge, mira que se cumpla una de estas dos cosas:
    1. Que el nombre de la interfaz empiece por br- y no haya declarada una opción bridge a no. Si el nombre empieza por br-, pero contiene un punto (por ejemplo br0.10), se entiende que no se pretende contruir un puente, sino añadir al puente una subinterfaz en una VLAN.
    2. Que, con independencia de cómo se llame la interfaz, haya declarada una opción bridge a yes.
  • Para que una interfaz actúe como puerto de un bridge, basta con que incluya la opción master con valor el nombre de la interfaz bridge.
  • Si se activa una interfaz bridge, se activan solidariamente todas los puertos.
  • Si se activa una interfaz que actúa como puerto, sin que esté levantada la interfaz bridge, ésta se activa automáticamente y solidariamente, el resto de puertos. Si el puerto estaba ya levantado, sólo se activará la interfaz requerida.
  • Si se desactiva una interfaz bridge, todos sus puertos y todas sus subinterfaces VLAN, se desactivan también.
  • Es posible habilitar el protocolo STP en la interfaz puente añadiendo la opción bridge_stp on.
  • Es posible fijar la MAC de la interfaz puente añadiendo la opción bridge_hw XX:XX:XX:XX:XX:XX.
  • Existen bastantes otros parámetros configurables para un bridge, pero no pueden ser configurados a través de los scripts. Si son necesarios pueden configurarse a través de opciones up y down.

VLANs en linux

En linux, es sumamente fácil crear, para una misma interfaz física, subinterfaces en distinta VLAN. Esto permite dividir el tráfico en varias redes lógicas que viajan por el mismo cable. Si quiere luego desdoblarse este tráfico etiquetado (según la norma IEEE 802.1q) en distintos cables, puede disponerse al otro extremo del cable un switch gestionable que soporte esta misma norma, o bien, un router neutro de los que soportan dd-wrt, tomato u openwrt. Esto último se sale del propósito del documento, pues sólo se pretende aquí dejar indicado cómo crear las subinterfaces en el servidor.

De nuevo podemos usar un método general o el método propio de debian basado en escribir en /etc/network/interfaces.

Configuración

Método universal

Tradicionalmente para esta tarea se ha venido usando el comando vconfig; pero como lo conveniente es ir migrando iproute2 se indicará aquí con preferencia cómo hacerlo con este último comando.

Para crear una subinterfaz para eth0 en la vlan 100 basta con lo siguiente:

# ip link add link eth0 name eth0.100 type vlan id 100

Obsérvese el nombre: se ha usado el que se ha usado por seguir una de las variantes con que permite crear nombres vconfig (eth0.100, eth0.0100, vlan100 o vlan0100). Sin embargo, con ip no debemos ceñirnos a ninguna forma concreta y podemos poner el nombre a la interfaz que deseemos.

Una vez creada la interfaz podemos configurarla (y desconfigurarla) exactamente igual que si se tratara de una interfaz real:

# ip addr add 192.168.100.1/24 dev eth0.100
# ip link set dev eth0.100 up

Una vez creada podemos comprobar su configuración y a qué VLAN pertenece añadiendo la opción -d al comando ip:

# ip -d link show eth0

Si en algún momento queremos eliminarla, basta desactivarla antes:

# ip link set dev eth0.100 down
# ip link del eth0.100

Si recurriéramos a vconfig para hacer estas tareas, lo primero sería instalarlo:

# aptitude install vlan

Hecho esto podemos usar vconfig tanto para crear una interfaz:

# vconfig add eth0 100

que crearía la subinterfaz eth0.100, como para destruirla:

# vconfig rem eth0.100

vconfig ha usado este tipo de nombre porque es el que tiene habilitado de modo predeterminado. Si se quisiera utilizar una variante distinta habría que especificarlo antes:

# vconfig set_name_type VLAN_PLUS_VID_NO_PAD
# vconfig add eth0 100

lo cual crearía la interfaz con nombre vlan100.

Método de debian

Para poner en práctica este método debemos escribir algo así en el fichero /etc/network/interfaces:

auto eth0.1
iface eth0.1 inet static
   address 192.168.100.1

Esto permitiría crear la interfaz eth0.1 que, obviamente, es subinterfaz de eth0 en la VLAN 1. debian, de modo sencillo, sólo permite este tipo de nombres. Si quisiéramos otro, entonces tendríamos que recurrir a órdenes de preconfiguración y postconfiguración:

auto vlan1
iface vlan1 inet static
   address 192.168.100.1
   pre-up    ip link add link eth0 name $IFACE type vlan id 1
   post-down ip link del $IFACE

Los scripts propuestos para crear puentes sin recurrir al paquete bridge permiten también crear una interfaz vlan con un nombre arbitrario, aunque ambos sean distinto tipo de interfaz. Sin embargo, dado que estos scripts permiten resolver el problema de crear un bridge vlan completo, se ha considerado oportuno incluir esta solución también en ellos:

iface vlan1 inet static
   address 192.168.100.1
   vlan    eth0.1

Como se ve, basta con incluir el parámetro vlan con el nombre que debería haber tenido la interfaz. En este caso eth0.1, porque se pretende crear un subinterfaz de eth0 para sostener la vlan 1.

Hasta no hace demasiado tiempo, ifupdown usaba para crear interfaces vlan el comando vconfig, de ahí que en muchos tutoriales de internet citen la opción vlan_raw_device (o vlan-raw-device que es equivalente) para declarar la interfaz física para la que se definía la subinterfaz de la vlan. Ya no tiene efecto, no al menos, si no se instala el paquete vlan en el que se incluye vconfig.

Multiplexación de VLANs

Vista la configuración, supongamos que queremos disponer, por un lado, de un enlace troncal por el que viajan etiquetadas distintas VLANs y, por otro, de un conjunto de interfaces físicas, cada una de las cuales pretendemos asociar a una VLAN distinta. Por estas interfaces el tráfico ya no sale etiquetado. Esquemáticamente podríamos dibujarlo así:

Multiplexación de VLANs con linux

Este problema puede resolverse de dos modos distintos: uno más tradicional en que usamos un puente para cada VLAN; y otro que consiste en construir un verdadero swith con soporte para VLANs.

Bridge por cada VLAN

La primera solución consiste en definir tantas subinterfaces a eth0 como VLANs viajen por el enlace troncal, para después ir creando interfaces bridge, de modo que a cada una añadamos una subinterfaz y la interfaz física que se corresponde con esa subinterfaz (vlan). Por ejemplo, para el caso mostrado habría que incluir dentro de un mismo puente la subinterfaz de eth0 correspondiente a la VLAN 10 (eth0.10) y la interfaz física eth1; y así mismo con las otras dos VLANs.

Multiplexación de VLANs hecha con varios bridge

Procedamos creando primero las subinterfaces de eth0:

# ip link add link dev eth0 name eth0.10 type vlan id 10
# ip link add link dev eth0 name eth0.20 type vlan id 20
# ip link add link dev eth0 name eth0.30 type vlan id 30

Y activándolas para que funcionen (quizás haya además que ponerlas explícitamente en modo promiscuo):

# ip link set eth0 up
# ip link set eth0.10 up
# ip link set eth0.20 up
# ip link set eth0.30 up

Luego basta con crear interfaces puente para cada VLAN y asignar las interfaces correspondientes:

# ip link add vlan10 type bridge
# ip link set eth0.10 master vlan10
# ip link set eth1  master vlan10
# ip link add vlan20 type bridge
# ip link set eth0.20 master vlan20
# ip link set eth2  master vlan20
# ip link add vlan30 type bridge
# ip link set eth0.30 master vlan30
# ip link set eth3  master vlan30

Que no despiste el nombre de las interfaces vlanXX: aunque las hemos nombrado así, son interfaces bridge. Por último, habrá que activarlas:

# ip link set vlan10 up
# ip link set vlan20 up
# ip link set vlan30 up

Basta con esto: linux se encarga de etiquetar y desetiquetar el tráfico según se salga por una u otra interfaz.

Adicionalmente, si queremos que el servidor tenga una ip en cada VLAN, podemos asignarles a vlan10, vlan20 y vlan30 una dirección adecuada.

Esta resolución es bastante sencilla traducirla un fichero /etc/network/interfaces para que ifupdown se encargue por nosotros de ejecutar las órdenes. En el caso de usar brctl esta sería la configuración:

auto eth.10
iface eth0.10 inet manual

auto eth.20
iface eth0.20 inet manual

auto eth.30
iface eth0.30 inet manual

allow-hotplug eth1
iface eth1 inet manual

allow-hotplug eth2
iface eth2 inet manual

allow-hotplug eth3
iface eth3 inet manual

auto vlan10
iface vlan10 inet manual
   bridge_ports eth1 eth0.10
   bridge_maxwait 1

auto vlan20
iface vlan20 inet manual
   bridge_ports eth2 eth0.20
   bridge_maxwait 1

auto vlan30
iface vlan30 inet manual
   bridge_ports eth3 eth0.30
   bridge_maxwait 1

Usando nuestros scripts quedaría así:

auto eth.10
iface eth0.10 inet manual
   master vlan10

auto eth.20
iface eth0.20 inet manual
   master vlan20

auto eth.30
iface eth0.30 inet manual
   master vlan30

allow-hotplug eth1
iface eth1 inet manual
   master vlan10

allow-hotplug eth2
iface eth2 inet manual
   master vlan20

allow-hotplug eth3
iface eth3 inet manual
   master vlan30

auto vlan10
iface vlan10 inet manual
   bridge yes

auto vlan20
iface vlan20 inet manual
   bridge yes

auto vlan30
iface vlan30 inet manual
   bridge yes

Bridge vlan

Esta solución es mucho más completa, porque permite resolver todo con un único puente. De hecho, se pueden crear switches muchísimo más complejos sin apenas complicación, como que, por ejemplo, una hipotética interfaz eth4 sea también un enlace troncal que permita conectar en cascada con otro switch.

En este caso lo que se hace es crear un verdadero switch con soporte para VLANs de modo que cada interfaz física es un puerto del switch. El tráfico que entra por eth0 está etiquetado como de una de las tres VLANs, mientras que en los otros tres puertos se hace algo distinto: entra tráfico no etiquetado y se etiqueta como de la VLAN que le corresponde; al salir se hace lo contrario, el tráfico de la VLAN que tiene asignada se desetiqueta.

Multiplexación de VLANs hecga con un bridge vlan

Lo primero es crear el puente y asignarle los cuatro puertos:

# ip link add br0 type bridge
# ip link set eth0 master br0
# ip link set eth1 master br0
# ip link set eth2 master br0
# ip link set eth3 master br0

Además hay que habilitar el filtrado vlan en el bridge:

# echo 1 > /sys/class/net/br0/bridge/vlan_filtering

Al puerto eth0 hay que asignarle las VLANs 10, 20 y 30:

# bridge vlan add dev eth0 vid 10
# bridge vlan add dev eth0 vid 20
# bridge vlan add dev eth0 vid 30

Y a los otros tres puertos su VLAN correspondiente pero indicando que se añada la etiqueta al entrar y se elimine al salir:

# bridge vlan add dev eth1 vid 10 pvid untagged
# bridge vlan add dev eth2 vid 20 pvid untagged
# bridge vlan add dev eth3 vid 30 pvid untagged

pvid se encarga de etiquetar al entrar y untagged de desetiquetar al salir.

Por último, se declaran las VLAN en el bridge:

# bridge vlan add vid 10 dev br0 self
# bridge vlan add vid 20 dev br0 self
# bridge vlan add vid 30 dev br0 self

Debe quedar algo así:

# bridge vlan
port    vlan ids
eth0     10
         20
         30

eth1     10 PVID Egress Untagged

eth2     20 PVID Egress Untagged

eth3     30 PVID Egress Untagged

br0      10
         20
         30

Por supuesto habrá que levantar todas las interfaces (y quizás poner las interfaces físicas en modo promiscuo):

# ip link set br0 up
# ip link set eth0 up
# ip link set eth1 up
# ip link set eth2 up
# ip link set eth3 up

En este caso, si se quiere disponer de direcciones ip pertenecientes a las tres VLANs en el servidor, hay que crear subinterfaces VLAN sobre la interfaz bridge:

# ip link add link br0 name vlan10 type vlan id 10
# ip link add link br0 name vlan20 type vlan id 20
# ip link add link br0 name vlan30 type vlan id 30

Y asignarles una dirección adecuada con ip addr tal como se ha visto.

Si queremos automatizar todo este proceso, podemos echar mano de los scripts propuestos para crear un puente y escribir en el fichero de declaración de las interfaces algo más o menos así:

auto br0
iface br0 inet manual
   bridge_vlan 10 20 30

allow-hotplug eth0
iface eth0 inet manual
   master br0
   vlan 10
   vlan 20
   vlan 30

allow-hotplug eth1
iface eth1 inet manual
   master br0
   vlan 10 pvid untagged

allow-hotplug eth2
iface eth2 inet manual
   master br0
   vlan 20 pvid untagged

allow-hotplug eth3
iface eth3 inet manual
   master br0
   vlan 30 pvid untagged

Si, además, pretendemos dotar al puente de direcciones ip en las VLANs, será necesario crear interfaces VLAN para él y configurarlas. Por ejemplo:

auto br0.10
iface br0.10 inet static
   address 192.168.10.1

auto br0.20
iface br0.20 inet static
   address 192.168.20.1

auto br0.30
iface br0.30 inet static
   address 192.168.20.1

Es muy importante que estas interfaces se intenten crear y activar una vez ya exista el puente, de modo que habrá incluirlas en el fichero después de la declaración de br0.

Linux como router

Además de convertir nuestro servidor en un perfecto switch para dividir redes en distintos segmentos, también podemos convertirlo en un router que enlace distintas redes:

Máquina como router

A diferencia del caso anterior, el encaminamiento es un mecanismos de capa de red (capa 3); y. por tanto. interviene en la decisión de enviar un paquete por una u otra interfaz la dirección ip de destino

Antes de empezar con ello hay que aclarar un término que se usará a lo largo de todos estos apartados: el de interfaz de salida. En principio, una interfaz de salida es aquella por la que sale un paquete de nuestra máquina, como contraposición a interfaz de entrada, que es por aquella por la que entra. De hecho, desde este punto de vista todas las interfaces del router son de entrada y de salida, su papel depende únicamente del sentido del tráfico. Si observamos el dibujo cuando una máquina de la red 2 hace una petición a una máquina de la red 1, la interfaz de entrada es eth1 y la de salida eth0. Sin embargo, para la respuesta que realiza el camino inverso la interfaz de entrada es eth0 y la de salida eth1. En muchas ocasiones nos podremos referir a este concepto usando ese término. En cambio, en ciertos casos podremos referir con el término interfaz de salida a aquella que toman los paquetes para salir a internet, o dicho de otro modos, aquella por la que saldrán camino de la puerta de enlace predeterminada. Esta forma de entender interfaz de salida es la que ha animado a dividir entres epígrafes este apartado: el primero presenta el caso más simple en el que hay una única salida a internet; y los dos siguientes aquel en el que existen varias salidas. Cuando esto último ocurre hay dos estrategias: agregar las salidas, de manera que se comporten a efectos prácticos como una sola, o bien mantenerlas por separado y crear reglas que encaminen el tráfico a internet por una u otra según distintos criterios.

Generalidades

De modo predeterminado, linux está configurado para rechazar cualquier paquete cuyo destino no sea la propia máquina:

$ cat /proc/sys/net/ipv4/ip_forward
0

de modo que, si deseamos que encamine tráfico (que es tráfico ajeno), tenemos que alterar este comportamiento. Para ello debe cambiarse el contenido de ip_forward pero, aunque vale para ello un simple echo:

# echo 1 > /proc/sys/net/ipv4/ip_forward

no es el método más apropiado si queremos hacer permanentes los cambios, puesto que al reiniciar la máquina, volveremos a encontrar el 0.

Para modificar valores de comportamiento dentro de la jerarquía /proc/sys, lo mejor es recurrir al fichero /etc/sysctl.conf, donde pueden declararse variables y valores que, gracias al comando sysctl, se asignarán durante el proceso de arranque. En concreto, para el caso que nos ocupa, la variable ya está declarada y tan sólo debemos descomentar la línea:

net.ipv4.ip_forward=1

Para que el cambio tenga efecto, podemos ejecutar el comando:

# sysctl -p

Con esto tendremos preparado nuestra máquina para que el tráfico no dirido a ella, pueda circular libremente. No volverá a citarse de nuevo la obligación de realizar este cambio, así que es importante entender que en todas las instrucción que se den a partir de ahora, se presupone que se ha realizado esta operación. Ahora toca indicar cómo elegir la interfaz adecuada. Para ello, cualquiera familiarizado con estos temás sabe que existen las tablas de encaminamiento. Supongamos este esquema de red:

Esquema de una red

En el que hay tres redes locales (e internet) y tres routers que las unen. El problema presentado es trivial y cualquier estudiante de redes mediamente aplicado sabrá resolverlo sin mucho esfuerzo, pero es útil tomarlo como punto de partida para entender cómo tratar el encaminamiento en linux. Del dicho al hecho, plasmemos la tabla de encaminamiento del segundo router etiquetato de R2:

Tabla de encaminamiento (Router 2)
TipoDestinoPuerta de enlaceInterfaz de salidaMétrica
*** Entradas para bucle local y broadcast ***
Conectadas192.168.0.0/24192.168.0.2eth00
192.168.1.0/24192.168.1.1eth10
No conectadas0.0.0.0/0192.168.0.1eth00
192.168.2.0/24192.168.1.2eth10

La tabla sobredicha resuelve el encaminamiento del router 2, aunque no se han desglosado las entradas necesarias para el bucle local y el broadcast. Obsérvese que, aunque las tablas suelen escribirse así, indicar la interfaz de salida es absolutamente gratuito, puesto que cuál es se puede deducir a partir del valor de la puerta de enlace. En linux, en cambio, no existe una única tabla, sino varias que se van consultando según un determinado orden y según un determinado criterio. Si consultamos el fichero /etc/iproute2/rt_tables, podremos ver lo siguiente:

$ grep '^[^#]' /etc/iproute2/rt_tables
255     local
254     main
253     default
0       unspec

O sea, cuatro tablas distintas (local, main, default y unspec) cada una con un byte identificativo único. De hecho, para referirnos a una tabla, podemos usar tanto su nombre como su identificador. Estas son las tablas definidas inicialmente y cada una tiene una utilidad precisa:

local
Registra las entradas para el bucle local y el broadcast. Estas entradas son automáticas y no tendremos que manipularlas nunca.
main
Esta es la tabla en la que se almacenan de modo predeterminado las entradas para las redes conectadas (que se añaden automáticamente al configurar las interfaces) y las entradas para redes no conectadas, las cuales hay que añadir a mano (o esperar a que se añadan, si hay habilitados protocolos como RIP).
default
Vacía.
unspec
Es una tabla especial de resumen: si consultamos su contenido, nos listará las entradas contenidas en las restantes tablas.

Ahora bien, si hay varias, ¿cómo se aplican? La respuesta está en la salida del comando:

$ ip rule show
0:      from all lookup local 
32766:  from all lookup main 
32767:  from all lookup default

El primer número indica el orden de aplicación y el from all significa que la tabla se aplicará a todo paquete venga de donde provenga. Por lo tanto, si se debe encaminar un paquete se comprueban las entradas de la tabla local, si no se logra encaminar el paquete se prueban las entradas de main y, si no hay éxito, se intenta con las de la tabla default. En la práctica, la tabla local no la tocamos y es automática, la tabla default está vacía y si no le indicamos a nuestra herramienta de consulta qué tabla queremos consultar se nos muestra main, así que en muchísimos casos podríamos vivir pensando que en linux no existe más que una tabla de encaminamiento (main) y que linux nos ahorra leer las molestas entradas de bucle local y de broadcast. Por supuesto, estas reglas de aplicación pueden modificarse (y de hecho, se hará cuando se desee gestionar varias salidas a internet), pero para un encaminamiento normal no es necesario en absoluto.

Nosotros olvidaremos bajo los próximos dos epígrafes todo esto, y sólo lo retomaremos en el último, cuando sea necesario tenerlo presente.

Encaminamiento simple

Para ilustrarlo más simplemente, podemos antes podar nuestro esquema de red y suponer que el router 2 es el linux que queremos configurar:

Esquema de una red sencilla

Al configurar las dos interfaces de red:

# ip addr add 192.168.0.2/24 dev eth0
# ip link set eth0 up
# ip addr add 192.168.1.1/24 dev eth1
# ip link set eth1 up

Las entradas necesarias para las redes directamente conectadas se añaden automáticamente:

# ip route show
192.168.0.0/24 dev eth0  proto kernel  scope link  src 192.168.0.2
192.168.1.0/24 dev eth1  proto kernel  scope link  src 192.168.1.1

de modo que no hay que preocuparse por ellas. Sin embargo, aún debemos indicar cómo salir a internet y esto hay que hacerlo explícitamente:

# ip route add 0.0.0.0/0 via 192.168.0.1.

En este caso, que añadimos la salida predeterminada, puede también usar la palabra default:

# ip route add default via 192.168.0.1.

Si volviésemos a listar las entradas de la tabla de encaminamiento, obtendríamos lo siguiente:

# ip route show
default via 192.168.0.1 dev eth0
192.168.0.0/24 dev eth0  proto kernel  scope link  src 192.168.0.2
192.168.1.0/24 dev eth1  proto kernel  scope link  src 192.168.1.1

Aunque con esto habríamos completado la tarea de completar el encaminamiento, es muy común que, además, de encaminar debamos enmascarar el tráfico de salida. Si observamos el esquema propuesto y pensamos en las máquinas que componen la red 192.168.0.0/24 (incluido el propio router R1), llegaríamos a la conclusión de que, para que pudieran responder a cualquier comunicación procedente de la red interna 192.168.1.0/24, sería necesario que supieran que han de enviar los paquetes hacia 192.168.0.2 y no hacia 192.168.0.1, que sería lo natural pues es su puerta de enlace predeterminada. Así pues, habría que incluir una entrada en la tabla de encaminamiento de todas estas máquinas o al menos en el router R1 para indicar que la puerta de enlace para la red 192.168.1.0/24 es la interfaz eth0 el router R2. Esto en algunos casos, puede llegar a ser imposible, porque no tengamos permisos para obrar este cambio. Sin embargo, si nuestra intención es simplemente que las máquinas de la red interna se puedan comunicar con el exterior del router R2, basta con que enmascaremos los paquetes a su salida por la interfaz eth0 del router R2. Esto consiste en modificar la dirección de origen de los paquetes de manera que se sustituya la original por la de la interfaz de salida, en este caso, 192.168.0.2. De esta forma, las máquinas externas pensarán que se comunican con el router R2, al cual saben llegar porque está en su propia red, y mandarán a él sus respuestas. El router, por su parte, es capaz de distinguir las respuestas de este tráfico que enmascaró y saber que estos paquetes no son realmente para él sino para el emisor original, con que modificará automáticamente la dirección de destino y lo enviará por la red interna hacia su legítimo emisor. Esto tan largo de explicar se logra mediante un comando de iptables:

# iptables -t nat -A POSTROUTING -o eth0 -j SNAT --to-source 192.168.0.2

En el caso de que eth0 recibiera por dhcp su ip y esta fuera cambiante, entonces es más recomendable dejar que iptables calcule en cada momento la ip de la interfaz:

# iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

Retomemos ahora el esquema completo:

Esquema de una red completo

Si queremos que funcione, debemos indicarle a nuestra máquina que envíe los paquetes dirigidos a la red 192.168.2.0/24 en la interfaz con ip 192.168.1.2. Por tanto:

# ip route add 192.168.2.0/24 via 192.168.1.2

Y la red quedará completa:

# ip route show
default via 192.168.0.1 dev eth0
192.168.2.0/24 via 192.168.1.2 dev eth1
192.168.0.0/24 dev eth0  proto kernel  scope link  src 192.168.0.2
192.168.1.0/24 dev eth1  proto kernel  scope link  src 192.168.1.1

Si deseáramos eliminar una entrada basta con usar del:

# ip route del 192.168.2.0/24

Tradicionalmente para añadir interfaces de red, se ha usado el comando route. Por ejemplo, la última entrada podría haberse añadido así:

# route add -net 192.168.2.0 netmask 255.255.255.0 gateway 192.168.1.2

Si quisiéramos usar ifupdown para configurar el esquema sencillo, deberíamos escribir en el ficheroç interfaces lo siguiente:

allow-hotplug eth0
iface eth0 inet static
   address 192.168.0.2
   gateway 192.168.0.1

allow-hotplug eth1
iface eth1 inet static
   address 192.168.1.1

Y si la ocasión requiera enmascaramiento deberíamos indicarlo en la declaración de eth0:

allow-hotplug eth0
iface eth0 inet static
   address 192.168.0.2
   gateway 192.168.0.1
   up   iptables -t nat -A POSTROUTING -o $IFACE -j SNAT --to-source ${IF_ADDRESS}
   down iptables -t nat -D POSTROUTING -o $IFACE -j SNAT --to-source ${IF_ADDRESS}

Para el esquema más complejo que requiere la entrada adicional, deberíamos añadir opciones up y down a la declaración de eth1:

allow-hotplug eth1
iface eth1 inet static
   address 192.168.1.1
   up   ip route add 192.168.2.0/24 via 192.168.1.2
   down ip route del 192.168.2.0/24

Encaminamiento múltiple

El problema en este caso consiste en que se tienen varios caminos para acceder a internet, pero no se desea agregarlos, sino mantenerlos independientes, de modo que se escoja uno u otro camino atendiendo a distintos criterios. Por regla general, el criterio para determinar qué camino se escoge para llegar a un destino es la dirección de destino del paquete. En este caso, sin embargo, hay dos (o más) puertas de enlace para alcanzar el mismo destino, por lo que es necesario establecer criterios adicionales. Referido a esto existen dos términos informáticos:

Source-based routing
esto es, encaminamiento basado en el origen del paquete, lo cual significa que el camino se escoge dependiendo de cuál sea la red de origen del paquete.
Policy-based routing
esto es, enrutamiento basado en políticas, que es una generalización del anterior término puesto que la elección se hace atendiendo a cualquier criterio de red que deseemos establecer. Por ejemplo, podríamos desear que todo el tráfico de navegación saliera por una interfaz y que todo el tráfico de correo electrónico saliera por otra. En este caso, el criterio sería el puerto de destino (80 y 443 para el primer tipo de tráfico y 25 para el otro).

Abordar esto, exige hacer memoria de qué en linux existen varias tablas de encamiento y conocer cómo crear nuevas y manipular las reglas para usar unas u otras. Expliquémoslo resolviendo el siguiente supuesto:

Servidor con dos salidas a internet

cuyos datos de red expuestos en una tabla son estos:

IDRedInterfaz
NombreDirecciónMáquinaNombreDirección
1Red externa 1172.22.0.0/16Router 1eth0172.22.0.1
2Servidoreth0172.22.0.2
3Red externa 210.53.0.0/27Router 2eth010.53.0.1
4Servidoreth110.53.0.2
5Red interna 1192.168.0.0/24Servidoreth2192.168.0.1
6Red interna 2192.168.1.0/24Servidoreth3192.168.1.1

Nuestra intención es que la red interna 192.168.0.0/24 salga por eth0, mientras que la red 192.168.1.0/24 lo haga por eth1, para lo cual no vamos a seguir exactamente lo sugerido por la guía LARTC, que es un poco engorroso. La idea está tomada de Trent W. Buck, aunque tampoco se implementa exactamente su solución.

Si volvemos a echar un vistazo a las entradas de una tabla de encaminamiento, comprobaremos que el único criterio en esas entradas para decidir la puerta de enlace es la red de destino. Sin embargo, nosotros deseamos, aparte de este criterio, elegirla basándonos en el origen del paquete, puesto que el destino siempre es el mismo: 0.0.0.0/0. La solución se halla en que podemos definir varias tablas de encaminamiento y definir reglas para usar una u otra.

Partamos de que hemos configurado las cuatro interfaces y de que la dirección de loopback ya esté configurada:

# ip addr add 172.22.0.2/16 dev eth0
# ip link set eth0 up
# ip addr add 10.53.0.2/27 dev eth1
# ip link set eth1 up
# ip addr add 192.168.0.1/24 dev eth2
# ip link set eth3 up
# ip addr add 192.168.1.1/24 dev eth3
# ip link set eth3 up

En este punto nos encontraríamos con estas tablas:

# ip rule show
0: from all lookup local 
32766:   from all lookup main 
32767:   from all lookup default

de las cuales local está rellena y es la primera que se revisa y main contiene las entradas para las redes directamente conectadas. Si en este punto definiéramos una puerta de enlace para internet y la incluyéramos en la tabla main no podríamos especificar el origen del paquete, así que lo que hacemos es lo siguiente:

# ip route add throw default

que nos deja la tabla main así:

# ip route show
throw default 
10.53.0.0/27 dev eth1  proto kernel  scope link  src 10.53.0.2 
172.22.0.0/16 dev eth0  proto kernel  scope link  src 172.22.0.2 
192.168.0.0/24 dev eth2  proto kernel  scope link  src 192.168.0.1 
192.168.2.0/24 dev eth3  proto kernel  scope link  src 192.168.2.1

¿Qué significa este throw default? Básicamente significa que en esta tabla no se toma ninguna decisión de encaminamiento sobre cómo llegar a la red 0.0.0.0/0 o default (en la orden hemos usado la palabra default, pero podríamos haber expresado explícitamente la red), sino que se debe regresar a la política de rutas (la que muestra el comando ip rule) y probar con la siguiente tabla de encaminamiento apropiada. En realidad, este comportamiento de continuar con la siguiente tabla es el predeterminado, así que podríamos habernos ahorrado el comando. El caso es que tal y como está ahora mismo la política de rutas, no nos vale, sino que la tendremos que dejar de esta guisa:

0:      from all lookup local 
10:     from all lookup main 
249:    from 172.22.0.2 lookup ADSL 
250:    from 10.53.0.2 lookup TIC 
999:    from 192.168.0.0/24 lookup ADSL 
1000:   from 192.168.1.0/24 lookup TIC 
32767:  from all lookup default

Esta política de rutas supone que sucesivamente y hasta que se logre encaminar el paquete, se comprueba:

  1. La tabla local.
  2. La tabla main, donde se encuentran las entradas para las redes directamente conectadas.
  3. Las dos reglas que hay a continuación permiten responder siempre por la interfaz que se recibió un paquete, es decir, si se recibió un paquete por eth0, se responderá a él por eth0; y si por eth1, por eth1. Esto funciona así, porque si una aplicación de nuestra máquina origina, la ip de origen se establecerá dependiendo de por qué interfaz salga. Sin embargo, si lo que hace la aplicación es responder a un paquete, la ip de origen del paquete de respuesta, será la ip de destino que tuviera tal paquete (172.22.0.2 o 10.53.0.2); y, gracias a nuestras reglas 249 y 250, nos aseguraremos de que salga por la interfaz que entró.
  4. La tabla ADSL, pero sólo para el tráfico procedente de la red conectada a eth2. En esta tabla sólo incluiremos una entrada en que se indique que la puerta predeterminada es 172.22.0.1. La consecuencia es que todo el tráfico de tránsito que entre por eth2 saldrá por eth0.
  5. La tabla TIC es análoga a la tabla anterior, pero para la otra red interna: sólo conmprueban su entrada los paquetes procedentes de la red conectada a eth3 y sólo habrá una entrada que lleve los paquetes 10.53.0.1.
  6. La tabla default, por último, encamina todo lo que no haya sido encaminado hasta ahora. Por ejemplo, el tráfico que origina nuestra máquina.

Obsérvese que ADSL y TIC son dos tablas nuevas, cuyos nombres habrá que definir en el fichero adecuado:

# cat >> /etc/iproute2/rt_tables
100 ADSL
101 TIC

Sobre esto cabe una puntualización: lo que hemos hecho simplemente es darle un nombre a las tablas, de manera que la tabla 100 se llamará ADSL y así nos podremos referir a ella con un nombre legible fácilmente recordable. Sin embargo, podemos saltar este paso y referirnos siempre a las tablas con su número identificativo.

Ahora sí, podemos redefinimos las políticas tal como las hemos propuesto:

# ip rule add from all lookup main prio 10
# ip rule del prio 32766
# ip rule add from 192.168.0.0/24 lookup ADSL prio 999
# ip rule add from 172.22.0.2 lookup ADSL prio 249
# ip rule add from 192.168.1.0/24 lookup TIC prio 1000
# ip rule add from 10.53.0.2 lookup TIC prio 250

Por último, nuestra intención es que una tabla tenga como salida predeterminada la 172.22.0.1 y la otra la 10.53.0.1; y que la tabla default use, por ejemplo, 172.22.0.1:

# ip route add default via 172.22.0.1 table ADSL
# ip route add default via 10.53.0.1 table TIC
# ip route add default via 172.22.0.1 table default

Ahora bien, ¿qué debemos hacer si nuestra intención es hacer un encaminamiento basado en políticas? Por ejemplo, supongamos que proceda de donde proceda el tráfico, queremos que el tráfico ICMP siempre salga a través de eth1.

La forma de dar solución a esto es marcar el tráfico, puesto que la política de rutas permite, además de indicar la procedencia, hacer referencia a la marca del paquete. Así que empecemos por marcar el tráfico que nos interesa:

# iptables -t mangle -A PREROUTING -p icmp -j MARK --set-mark 0x1
# iptables -t mangle -A OUTPUT -p icmp -j MARK --set-mark 0x1

Y ahora añadamos una regla a la política de rutas que haga que este tráfico use la tabla TIC:

# ip rule add fwmark 0x1 lookup TIC prio 500

Obviamente, esta regla debe ser anterior a las que determinan la salida según la procedencia, pero posterior a las que nos permiten responder por la misma interfaz que hemos escuchado. Con esto debería estar todo completamente configurado, pero puede no ser así. Dependiendo de la configuración predeterminada para los parámetros del núcleo, podría ser que estuviera activada la defensa contra ataques de envenenamiento. Esta defensa consiste en comprobar que según las reglas de encaminamiento un paquete procedente de una interfaz debería haber llegado efectivamente por esa interfaz. En esta comprobación no se tienen en cuenta posibles marcas que pudiera tener el paquete y que modificaran la interfaz de comunicación, por lo que si usamos el marcado para elegir salida, el cálculo de comprobación puede dar otra intefaz distinta y el paquete ser rechazado. Para que no se realicen estas comprobaciones contra envenenamientos debemos cerciorarnos de que la opción /proc/sys/net/ipv4/conf/<interfaz>/rp_filter esté a 0. Podemos confirmar si es así y, si vemos que no, podemos comprobar el contenido de /etc/sysctl.conf para asegurarnos de que contenga las líneas:

net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.default.rp_filter = 0

La segunda de estas líneas provocaría que al crearse una interfaz su parámetro rp_filter estuviera a 0.

Si queremos gestionar esta situación con ifupdown, no tenemos más remedio que crear un script que lo permita:

Script multirouting para if-up-d y if-down.d

Debe colocarse en if-up.d e if-down.d:

# cd /etc/network
# mv /donde/se/encuentre/multirouting.txt if-up.d/multirouting
# ln -s ../if-up.d/multirouting if-down.d/

Con este script el caso anterior quedaría resuelto añadiendo estas líneas a /etc/network/interfaces:

auto lo
iface lo inet loopback
   multi_mark    0x1 TIC

allow-hotplug eth0
   address       172.22.0.2
   gateway       172.22.0.1
   multi_alias   ADSL

allow-hotplug eth1
   address       10.53.0.2/27
   multi_gateway 10.53.0.1
   multi_alias   TIC

allow-hotplug eth2
   address       192.168.0.1
   multi_default ADSL

allow-hotplug eth3
   address       192.168.1.1
   multi_default TIC

El script pretende resolver casos más complejos del expuesto aquí y que tienen que ver con el balanceo de carga, que se verá a continuación. Una explicación prolija de cómo usarlo se encuentra en su propio código, incluidos cinco ejemplos de uso. Baste aquí indicar que:

  • No es necesario nombrar en /etc/iproute2/rt_tables las tablas: el script lo hace por nosotros.
  • En la declaración de lo se ha declarado que el tráfico marcado con 0x1 salga por 10.53.0.1 para que afecte a todo el tráfico sea cual sea su procedencia. Esto se hace así por convención y conveniencia, ya que lo es una interfaz que no suele deshabilitarse nunca.
  • La opción multi_alias en cada interfaz de salida nos permite darle nombre a la tabla asociada a la puerta de enlace de esa interfaz.
  • La opción multi_default en cada interfaz interna indica por dónde se quiere sacar el tráfico de cada red interna.
  • Debido a que se ha usado gateway en eth0, su puerta de enlace se incluye en la tabla default, por lo que el tráfico que origina el servidor saldrá por 172.22.0.1.

Balanceo de carga

En este caso, en vez de usar una u otra interfaz de salida dependiendo de un determinado criterio, se pretende usar ambas interfaces de salida con objeto de aprovechar ambos anchos de banda conjuntamente. Si los anchos de banda fueran iguales, sería lógico que procurásemos salir el mismo número de veces por una interfaz que por otra; en cambio, si una tiene el doble que la otra, lo óptimo es salir el doble de veces por la una que por la otra.

Ha de aclararse que ambas interfaces (y ambas puertas de enlace) se encuentran en redes distintas y que, por tanto, la solución requiere instrumentos de capa de red. Si se pretendiera agregar interfaces en capa de enlace (lo cual significaría que ambas puertas de enlace se encontrarían en la misma red), entonces la solución sería distinta y habría que recurrir al módulo bonding del núcleo de linux para crear una única interfaz virtual.

La configuración es muy semejante a la anterior, así que en vez de referirla por completo, mostraremos cuáles son las diferencias. En primer lugar, si vemos nuestra política de rutas:

0:      from all lookup local 
10:     from all lookup main 
249:    from 172.22.0.2 lookup ADSL 
250:    from 10.53.0.2 lookup TIC 
999:    from 192.168.0.0/24 lookup ADSL 
1000:   from 192.168.1.0/24 lookup TIC
32767:  from all lookup default

Sobran las reglas que encaminan por una puerta de enlace determinada las redes internas, puesto que nuestro objetivo es balancear:

# ip rule del prio 999
# ip rule del prio 1000

Con esta configuración todas esas comunicaciones (y las que origina el propio servidor) acaban en la tabla default, que encamina según nuestra configuración anterior, o sea, siempre a través de eth0

# ip route show default
default via 172.22.0.1 dev eth0

Pues bien, es esta entrada la que hay que modificar para producir el balanceo:

# ip route replace default table default nexthop via 172.22.0.1 weight 2 nexthop via 10.53.0.1 weight 1

En el ejemplo se le ha dado el doble de peso a una puerta de enlace que a la otra, por lo que saldrá el doble de tráfico por una que por otra. La tabla debe quedar así:

# ip route show default
default 
        nexthop via 172.22.0.1  dev eth0 weight 2
        nexthop via 10.53.0.1  dev eth1 weight 1

Nótese que todo esto no es incompatible con que haya tráfico marcado o procedente de una red interna que siempre salga por una interfaz: todo es cuestión de incluir reglas en la política de rutas que hagan salir este tráfico a través de una tabla en particular.

Para gestionar este caso con ifupdown puede hacerse uso del script anterior que está preparado también para ello. La configuración quedaría así:

allow-hotplug eth0
   address       172.22.0.2
   gateway       172.22.0.1
   multi_table   default 2

allow-hotplug eth1
   address       10.53.0.2/27
   multi_gateway 10.53.0.1
   multi_table   default

allow-hotplug eth2
   address       192.168.0.1

allow-hotplug eth3
   address       192.168.1.1

De nuevo, es importante leer las instrucciones del script. En este caso:

  • Aunque gateway incluye en la tabla default la puerta de enlace 172.22.0.1 lo hace con peso 1, así que para indicar el peso nos vemos obligados a usar multi_table que hace participar la puerta de enlace en la tabla expresada (default en este caso).
  • En el caso de la otra puerta de enlace, nos vemos obligados a usar multi_table, porque multi_gateway no implica que se incluya en la tabla default. Al no indicar peso, se sobreentiende 1.
  • Las redes internas no tienen definida la opción multi_default, porque se quiere que usen la salida definida a través de la tabla default, que es la que se usa para aquel tráfico que no tiene especificada su salida.

Para la configuración de encaminamientos múltiples a través de ifupdown existe el paquete de debian ifupdown-multi: parece una buena alternativa a este script, aunque el autor no lo ha probado.

Proxy ARP con linux

Concepto

Ya se han visto dos técnicas para unir dos partes de una LAN:

  1. Un router para unirlas si se han dispuesto en redes diferentes.
  2. Un puente para unirlas si se han dispuesto dentro de una misma red.

En el primer caso los dispositivos de una parte que quieren comunicarse con la otra han de enviar sus paquetes a la ip de la interfaz del router que cae en su misma red. Esta ip es lo que se conoce como puerta de enlace.

En el segundo caso, la presencia del puente es totalmente transparente y los paquetes de una y otra parte lo atraviesan con total libertad. De hecho, ni siquiera es necesario que la interfaz bridge posea una dirección ip. A ojos de un cliente es indistingible un cliente se haya en el mismo segmento de red que en el otro.

Pero hay una tercera vía para conectar ambas partes y que es una solución intermedia entre las dos anteriores: el proxy ARP. Es intermedia en la medida en que ambas partes se encontrarán dentro de una misma red, lo cual lo asemeja a la segunda vía; y en que la máquina intermediaria dispondrá de dos direcciones ip (una para cada interfaz) que harán las veces de puerta de enlace (ya matizaremos esto), lo cual lo asemeja a la primera. En realidad, tiene más de la primera que de la segunda, ya que el tráfico es encaminado entre ambas interfaces y no conmutado, como en el caso de un bridge.

Esquema de un proxy ARP

Un proxy ARP manipula las peticiones ARP de los clientes, de modo que si le llega una procedente de un cliente que pregunta por la MAC de otro cliente del otro lado, responderá devolviéndole la dirección MAC de la interfaz que tiene en el lado de la petición. Si se observa la figura del ejemplo que lo ilustra, la tabla ARP del cliente con ip 192.168.0.100, tendría un aspecto como este:

$ ip n
192.168.0.1 dev eth0 lladdr de:ad:be:ef:00:01 STALE
192.168.0.15 dev eth0 lladdr de:ad:be:ef:00:0f STALE
192.168.0.30 dev eth0 lladdr de:ad:be:ef:00:1e STALE
.
.
.
192.168.0.200 dev eth0 lladdr de:ad:be:ef:00:01 STALE
192.168.0.201 dev eth0 lladdr de:ad:be:ef:00:01 STALE

O sea, de todas las máquinas de su segmento conocería la mac real, pero de las máquinas del segmento opuesto pensaría que la mac es la de la interfaz eth1 del servidor, puesto que éste, cuando recibe la petición ARP, la falsea y responde con su propia mac. De este modo, cuando el cliente pretenda comunicarse con el cliente del lado opuesto (192.168.0.200), el paquete será recibido por la interfaz eth1 del servidor, puesto que contendrá su mac, y éste lo enviará por la otra interfaz hasta su destino.

Usar esta estrategia para dividir la red tiene una serie de ventajas e inconvenientes:

  1. No crea una red nueva, en caso de que ya estuviera formada.
  2. El tráfico ARP es mayor que si se hubiera usando la opción de colocar un router, puesto que los clientes seguirá intentando averiguar las mac de los clientes del otro lado.
  3. Aparecen mac repetidas en las tablas ARP de los clientes, lo cual también es un síntoma de envenenamiento ARP.
  4. El proxy debe saber qué clientes tiene a cada lado para encaminar correctamente los paquetes. En el ejemplo, se ha supuesto que a un lado se han colocado las ips del rango 192.168.0.0/25 y al otro las del rango 192.168.0.128/25, lo cual reduce a dos las entradas en la tabla de encaminamiento. Si no hubiéramos sido tan organizados y las ips pudieran caer aleatoriamente a un lado u otro, entonces tendríamos que incluir una entrada por cada máquina.

Configuración

Debe colocarse una ip a cada interfaz del proxy:

# ip addr add 192.168.0.1/24 dev eth1
# ip addr add 192.168.0.129/24 dev eth2

e indicar qué máquinas caen a cada lado:

# ip route add 192.168.0.0/25 dev eth1
# ip route add 192.168.0.129/25 dev eth2

Por último, es necesario habilitar el ip_forward y el proxy_arp para ambas interfaces:

# sysctl -w net.ipv4.ip_forward=1
# sysctl -w net.ipv4.conf.eth1.proxy_arp=1
# sysctl -w net.ipv4.conf.eth2.proxy_arp=1

Con esto basta: ambas partes podrán comunicarse del modo descrito.

En este caso particular, se podríam haber hecho estas asignaciones a las interfaces:

# ip addr add 192.168.0.1/25 dev eth1
# ip addr add 192.168.0.129/25 dev eth2

y ni siquiera habría sido necesario añadir las dos rutas. Además, si dispusiera un servidor dhcp, sería posible entregar direcciones del rango adecuado por cada interfaz. De todos modos, si fuera posible hacer esta división, sería mejor dividir en dos subredes.

POR ESCRIBIR (aunque mejor en dhcp: cómo se configura el servidor dhcp). Ver la opción use-lease-addr-for-default-route true;

Control de tráfico (QoS)

Concepto

Si llegamos a implementar algunos de los usos cuyas soluciones se proponen en este documento y, además, instalamos algunos servicios (web, DNS, SSH, etc.) nos encontraremos con un servidor que sirve para encauzar clientes de redes internas hacia internet y que ofrece servicios al exterior. Esto puede llegar a suponer un gran consumo de ancho de banda, sobre todo a determinadas horas. Dadas las limitaciones de conexión a internet esto se puede traducir en un deficiente servicio, tanto a los usuarios internos que intentan acceder al exterior, como a los usuarios externos que intentan hacer uso de alguno de nuestros servicios desde sus casas.

La solución a este problema, aparte de contratar un mayor ancho de banda que a veces es la única posible, puede venir de la mano de lo que se llama control de tráfico y, en otras ocasiones, calidad de servicio. Si no configuramos nada al respecto, la salida de los paquetes por la interfaz que conduce hacia internet es una cola FIFO (en realidad, es un pelín más complicado como se discutirá más adelante), en la que el primer paquete que llega a la cola es el primero que sale. No hay distingos entre los distintos tipos de tráfico, así que puede darse el caso de que una conexión que consuma gran ancho de banda (p.e. un usuario externo que descarga un fichero medianamente grande que hemos colocado en el servidor web) impida que el administrador pueda usar con comodidad SSH para realizar alguna configuración importante en el servidor: éste notará tirones en la conexión y que durante algunos segundos es incapaz de escribir nada en la terminal, para que al cabo de este tiempo se escriban de sopetón todas las letras que intentó teclear.

El control de tráfico permite priorizar ciertas conexiones o reservarles un ancho mínimo, aun cuando otras personas estén haciendo un uso intensivo del ancho de bando.

Planificación de tráfico (qdisc)

Una planificación de tráfico o disciplina de cola (queue discipline en inglés) es el algoritmo que se fija para determinar de qué modo salen o entran los paquetes por una interfaz. Ya hemos dado un ejemplo de un algoritmo muy sencillo, el FIFO, que va poniendo en cola los paquetes y haciéndolos salir por la interfaz según el orden en le que llegaron.

Obviamente todas las interfaces tienen que tener definido una planificación, porque el kernel debe saber cómo hace salir los paquetes por ella. Cuando no se establece ninguno en particular, linux le asigna a la interfaz la qdisc PFIFO_FAST, que es una variante del FIFO:

# tc qdisc show dev eth0
qdisc pfifo_fast 0: root refcnt 2 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1

Esta es la planificación para la salida (root) que existe de forma predeterminada. Puede, por supuesto, ser cambiada y todo este apartado trata de cómo hacerse y con cuáles otras planificaciones hacerse.

Hay dos tipos de algoritmos: los algoritmos sin clases y los algoritmos con clases. Los segundos permiten clasificar los paquetes según distintos criterios y establecer distintas colas para los distintos tipos de tráfico que se establezcan.

Antes de comenzar, no obstante, es importante entender correctamente el alcance y la eficacia de una planificación. Entiéndase que el único tráfico que realmente podemos controlar es el tráfico de salida, ya que lo generaremos en el propio servidor o en otra máquina local que también podremos manipular. El de entrada, sin embargo, si procede de internet, sólo puede tratarse al llegar a la entrada de la interfaz, de manera que lo que haya pasado antes con el paquete nos es impòsible de controlar: ni podremos asegurar con que ToS nos llega el paquete ni cómo planificará su salida el router del ISP, etc. Lo único realmente eficaz que podremos hacer es descartar tráfico. Por ello, la planificación de entrada (ingress) es sumamente simple, aunque existan argucias para crear sobre ella planificaciones complejas. Se tratará el control de la entrada más adelante.

Planificaciones sin clases

Son las más sencillas. Revisaremos tres, la predeterminada PFIFO_FAST, la SFQ y la TBF, cuyos principios son muy útiles para entender la planificacione con clases HTB.

PFIFO_FAST

Es una variante algo más sofisticada del algoritmo FIFO. Su esquema es el siguiente:

Esquema del panificador PFIFO_FAST

Según llegan los paquetes a la interfaz el kernel comprueba el campo ToS de la cabecera IP y dependiendo del valor clasifica el paquete en tres bandas de mayor (0) a menor (2) prioridad. Las bandas funcionan de forma que hasta que una banda de mayor prioridad no se ha vaciado, no se comienza a vaciar la siguiente en prioridad. Cada banda se vacía según el principio FIFO.

Para conocer cómo se analizan los paquetes y clasificarlos es necesario profundizar un poco más en el byte ToS (según la definición antigua del RFC 791):

Esquema del byte ToS

Los tres primeros bits indican la prioridad del paquete, pero en la práctica no han sido usados. El último bit siempre vale 0 y los bits 3-6 son propiamente los que indican el tipo de servicio:

BitsValor decimalSignificado
00000Servicio normal
00011Mínimo coste (mmc)
00102Máxima fiabilidad (mr)
01004Máximo rendimiento (mt)
10008Mínimo retardo (md)

Como el séptimo bit siempre es 0, para calcular el valor del ToS, hay que doblar el valor decimal. Sabido esto podemos desglosar cómo calcula linux la prioridad y a qué banda asigna dicha prioridad:

ToSBitsSignificadoPrioridad en linuxBanda
0x00Servicio Normal0Best Effort1
0x21Mínimo coste1Filler2
0x42Máxima fiabilidad0Best Effort1
0x63mmc+mr0Best Effort1
0x84Máximo rendimiento2Bulk2
0xa5mmc+mt2Bulk2
0xc6mr+mt2Bulk2
0xe7mmc+mr+mt2Bulk2
0x108Mínimo retardo6Interactive0
0x129mmc+md6Interactive0
0x1410mr+md6Interactive0
0x1611mmc+mr+md6Interactive0
0x1812mt+md4Int. Bulk1
0x1a13mmc+mt+md4Int. Bulk1
0x1c14mr+mt+md4Int. Bulk1
0x1d15mmc+mr+mt+md4Int. Bulk1

Como vemos, a partir del ToS el kernel asigna una prioridad y esta prioridad determina la banda (0, 1 ó 2) que se asigna al paquete. La asignación de banda la determina el valor del campo priomap que en esta planificación es fijo: 1, 2, 2, 2, 1, 2, 0, 0 , 1, 1, 1, 1, 1, 1, 1, 1. Entiéndase:

Prioridad linux0123456789101112131415
Banda1222120011111111

... los paquetes con prioridad 0 van a la banda 1; los paquetes con prioridad 1, a la banda 2, etc. Las prioridades mayores a 6 se establecen mirando otros bits del byte, dado que aquí se ha discutido un uso antiguo que sólo utilizaba en la práctica cuatro bits. Más adelante se discutirá algo del uso más moderno.

Cada tipo de conexión tiene sus bits de ToS fijados atendiendo a su naturaleza. Algunas son estas:

ServicioBitsSignificadoToS & 0x1c
SSHInteractivo (ssh)1000Mínimo retardo16
No interactivo (scp, sftp)0100Máximo rendimiento8
FTPCanal de control1000Mínimo retardo16
Canal de datos0100Máximo rendimiento8
Telnet1000Mínimo retardo16
DNSPetición UDP1000Mínimo retardo16
Petición TCP0000Servicio normal0
Tranferencia de zona0100Máximo rendimiento8
ICMP0000Servicio normal0

La última columna es el resultado de aplicar la máscara 00011100 (0x1c en hexadecimal) al byte. Este resultado es útil, porque el RFC 2474 redefinió el byte por completo introduciendo el DSCP para los seis primeros bits y el RFC 3168 el ECN (Notificación de Congestión explícita) para los dos últimos. Esto hace que dependiendo de qué se use para codificar el tipo de servicio, el valor del byte pueda ser uno u otro:

Esquema del byte ToS 2

No obstante, los bits 3-5 coinciden y, si en alguna otra circunstancia tuviéramos que identificar tráfico, lo podríamos hacer mirando exclusivamente estos bits (máscara 0x1c).

Por otro lado el tamaño de las bandas viene determinado por el parámetro tx_queue_len:

$ cat /sys/class/net/eth0/tx_queue_len 
1000

que puede definirse para cada interfaz con el comando ip (véase man ip-set). Cuando se supera el tamaño de la cola, se comienzan a desechar paquetes, aunque el protocolo TCP debería ajustar la velocidad de envío a la velocidad del medio y que esto no ocurriera.

SFQ

A diferencia de la planificación anterior en esta no se prioriza ningún tipo de tráfico. La estrategia consiste en generar un hash de cada paquete a partir de su información de cabecera (por ejemplo, ips y puertos de origen y destino) y en función del valor enviar los paquetes a una de las 1024 cubetas disponibles, las cuales se van vaciando siguiendo el algoritmo round-robin, o sea, de la primera se extrae un paquete, de la segunda otro y así sucesivamente hasta llegar a la última, para luego volver a empezar.

Esquema de la planificación SFQ

Como pudiera ocurrir que unas cubetas se llenaran más rápido que otras, cada cierto tiempo se modifica el algoritmo de hash.

Los parámetros que pueden configurarse en esta planificación son:

  • divisor: es el número de cubetas (por defecto, 1024).
  • limit: es el número de paquetes que se almacenan en cada cubeta (por defecto, 128).
  • perturb: frecuencia en segundos en la que se renueva (por defecto, 10 segundos).
  • quantum: máximo número de bytes que salen de una cubeta en cada turno. Debe ser como mínimo el MTU, que para redes Ethernet suele ser 1500 bytes. De este modo, saldría un paquete por turno.

Esta planificación es adecuada para balancear tráfico de manera que ninguno resulte beneficiado. Obviamente, si se pretende definir una sóla planificación en la interfaz de salida, la planificación anterior es más apropiada, ya que favorece el tráfico de mayor prioridad. Esta, en cambio, puede ser útil en otros casos. Por ejemplo, imagínese que tenemos un router como el R2:

Esquema de un router con dos interfaces

que encamina todo el tráfico de la RED 2. Si no hemos tocado la planificación de su interfaz eth0, esta será una PFIFO_LAST, lo cual no está del todo mal, ya que prioriza según la prioridad del tráfico. Ahora bien, dentro de cada una de sus bandas (en que la prioridad es la misma), no se hace distingos en el tráfico, ya que los paquetes son evacuados mediante un algoritmo FIFO sin más. Así, pues, un cliente interno podría copar el tráfico. Sería más lógico que dentro de cada una de estas bandas se pudiera repartir equitativamente la salida según clientes y tráfico: así ninguno salía beneficiado. Este sería un caso en que SFQ es útil. Desgraciadamente, PFIFO_FAST no deja hacerlo, pero otra planificación como PRIO sí.

Aplicar esta planificación es muy sencillo:

# tc qdisc add dev eth0 root sfq

Pero ya se ha advertido que como planificación raíz sobre la interfaz no es muy adecuado. Si consultamos la planificación:

# tc qdisc show dev eth0
qdisc sfq 8001: root refcnt 2 limit 127p quantum 1514b depth 127 divisor 1024

Son reconocibles los parámetros que se han discutidos. Nótese también lo remarcado: 8001:. Este es un handle que identifica a la planificación y cuyo formato son dos números separados por dos puntos. El segundo, al no aparecer, se supone 0. Por convenio, a las planificaciones raiz se les suele asignar el handle 1: (o 1:0, si se prefiere). Por tanto, habría estado mejor hacer:

# tc qdisc add dev eth0 root handle 1: sfq

dentro. por supuesto, de la inconveniencia que supone usar esta planificación. Puede usarse cualquier handle, aunque el ffff: está reservado para la planificación de entrada (ingress).

TBF

Esta planificación es interesante porque permite introducir los conceptos que se encontrarán en HTB. A diferencia de los algoritmos anteriores permite controlar la velocidad a la que saldrán los paquetes, para lo cual se usa una argucia basada en tokens o fichas, si se prefiere. Para entender el mecanismo podemos usar un símil hidráulico. Supongamos que disponemos de un bidón en el que almacenamos agua que se llena por la parte superior con un determinado caudal constante, y se vacía por la parte inferior mediante un grifo, cuyo caudal puede ser mucho mayor que el de entrada:

Un bidón de agua

En estas condiciones es obvio que aunque abramos al máximo el grifo inferior, el caudal medio de desagüe siempre sera igual al caudal constante de entrada. Ahora bien, como disponemos de un bidón que almacena el agua, si cerramos el grifo inferior durante un rato, de manera que dejamos que el bidón se llene en parte o totalmente, cuando abramos el grifo, podremos desaguar al caudal máximo que nos permita el grifo inferior hasta que tengamos el bidón completamente vacío.

Volvamos ahora a nuestra planificación de tráfico, teniendo esta idea presente:

Esquema de la planificación TBF

Como en los algoritmos anteriores, vamos almacenando paquetes en una cola a espera de poder salir por la interfaz. La diferencia ahora es que se manipula la salida de estos paquetes de la cola introduciendo un mecanismo de control de la velocidad a la que salen. Para ello se dispone un bidon que en vez de llenarse de agua, se llena de fichas por valor de 1 byte. La velocidad de llenado es lo que se denomina rate en el esquema. Además hay una valocidad de vaciado máxima, llamada peakrate y una capacidad máxima del bidón denominada burst. Existe un bidón más pequeño en donde desaguan las fichas del bidón anterior. Este bidón, por lo general es del tamaño de la MTU por lo que sólo es capaz de albergar un paquete. Su capacidad de vaciado es teóricamente infinita, pero en la práctica, dada su capacidad, desagüa a la velocidad a la que se llena, esto es, peakrate. Sacar de la cola un paquete de 1500 bytes exige consumir 1500 fichas, así que si el flujo de paquetes es constante e impide que se llene el bidón sólo podremos sacar paquetes a la velocidad que fije rate. Si por el contrario, durante un determinado tiempo no hay paquetes que enviar, el bidón se irá llenando, de modo que, cuando los haya, podremos enviarlos a una velocidad máxima de peakrate hasta el máximo indicado por burst (la capacidad del bidón).

Este concepto de burst es útil para tráfico que vaya a tirones. Por ejemplo, la navegación de un usuario que pide una página, momento en el que hay tráfico, y luego se dedica un tiempo a ojearla, momento en el que no lo hay.

Como en la planificación anterior, se denomina limit a la cantidad de bytes en espera de ser enviados. Obsérvese que en condiciones ideales, si se envían paquetes a la velocidad fijada por rate, los paquetes esperarán para ser enviados un tiempo igual a la división limit/rate. Este tiempo medio de espera es lo que se denomina latencia y puede ser indicado al definir la planificación alternativamente al de limit.

Un ejemplo de definición puede ser este:

# tc qdisc add dev eth0 root tbf rate 1mbit burst 10k latency 50ms

Obsérvese que que no se ha definico la mtu, la cual se toma con la definida para la interfaz física (habitualmente en redes Ethernet 1500 bytes. Este es un valor adecuado si realmente los paquetes tienen ese tamaño, pero lo más probable es que al pasar por la planificación no sea así. La culpa la tiene una tecnología llamada LSO. Sin ella, se supone que en la capa de transporte el protocolo troceará los segmentos con un tamaño tal que al añadírsele las cabeceras ip y ethernet, los paquetes no alcancen el tamaño fijado por la mtu. En redes de alto rendimiento, para ahorrar ciclos de procesador, los datos se segmentan en paquetes muchos más grandes que al llegar a la interfaz son troceados por el hardware de la tarjeta para ajustarte al mtu. De hecho, si monitorizamos tráfico con tcpdump comprobaremos que los paquetes son bastante grandes. La consecuencia de que esté habilitada esta tecnología es que el mtu que tiene definida la planificación es demasiado pequeño y no nos van a funcionar. La solución es clara: o deshabilitamos esta tecnología con ethtoll o fijamos un mtu mucho mayor:

# tc qdisc add dev eth0 root tbf rate 1mbit burst 128k mtu 64k latency 50ms

Obviamente esta estrategia por si sola es muy pobre, pues no permite distinguir tipos de tráfico: lo útil es tener varias colas TBF en paralelo, de manera que a cada una vaya a parar un tipo de tráfico: así podremos establecer distintas ratios de envío según el tipo. De eso trata, precisamente, la planificación HTB.

Planificaciones con clases

PRIO

Esta planifición es conceptualmente muy parecida a la PFIFO_FAST (en la guía LARTC la califican de PFIFO_FAST con esteroides), pero con la particularidad de que permite configuración. Si se analiza la PFIFO_FAST con amplitud de miras, se podría considerar de que dispone de clases (cada una de las tres bandas puede considerarse una), pero que estas ni son configurables en número (siempre hay tres) ni son configurables en tipo de tráfico (a cada banda siempre va el mismo tipo de tráfico).

Pues bien, en esta planificación podemos indicar cuántas bandas queremos que haya y también que tipo de tráfico queremos que vaya a cada banda. Esto segundo puede hacerse de dos formas:

  1. Como en PFIFO_FAST, calculando la prioridad a partir del byte ToS y asignando una banda a cada prioridad mediante el campo priomap (recordemos que en PFIFO_FAST este campo existe pero tiene un valor que no es modificable).
  2. Mediante la creación de filtros que permiten asignar a las bandas cualquier tipo de tráfico a partir de la información que se pueda encontrar en las cabeceras de los paquetes.

Para el envío de paquetes se usa la misma técnica que en PFIFO_FAST: hasta que una banda de mayor prioridad no se vacíe de paquetes, no se envían paquetes de la siguiente banda. Por tanto, las clases (las bandas) no tienen asignado un ancho determinado de banda.

Para configurar esta planificación son necesarios fundamentalmente tres datos:

  1. El número de bandas, cuyo valor predeterminado es 3.
  2. El valor para priomap, cuyo valor predeterminado es el mismo que para PFIFO_FAST.
  3. La planificación para cada banda. La predeterminada es una planificación FIFO simple.

Por tanto, si en una interfaz se hace esta planificación sin especificar ningún parámetro, obtendremos una planificación PFIFO_FAST sin más. Veámoslo:

# tc qdisc show dev eth0
qdisc pfifo_fast 0: dev eth0 root refcnt 2 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1

Cómo aún no hemos establecido ninguna planificación sobre eth0 la que hay es una PFIFO_FAST. Establezcamos ahora una PRIO sin especificar nada:

# tc qdisc add dev eth0 root handle 1: prio
# tc qdisc show dev eth0
qdisc prio 1: dev eth0 root refcnt 2 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1

O sea, el mismo perro con distinto collar. Lo que sí es didáctico y, por tanto, particularmente útil es que con esta planificación podemos desglosar cada una de las bandas: la 1:1 (que hace las veces de 0), la 1:2 (1) y la 1:3 (2). Gracias a ellos podremos comprobar que la planificación PFIFO_FAST funciona como se explicó:

# tc -s class s dev eth0
class prio 1:1 parent 1: 
 Sent 3782 bytes 37 pkt (dropped 0, overlimits 0 requeues 0) 
 backlog 0b 0p requeues 0 
class prio 1:2 parent 1: 
 Sent 0 bytes 0 pkt (dropped 0, overlimits 0 requeues 0) 
 backlog 0b 0p requeues 0 
class prio 1:3 parent 1: 
 Sent 0 bytes 0 pkt (dropped 0, overlimits 0 requeues 0) 
 backlog 0b 0p requeues 0

Como puede verse, sólo la banda 0 (1:1) ha tenido tráfico. Si ahora confieso que estoy haciendo pruebas sobre una máquina virtual que manejo desde una sesión ssh, esto cobra sentido, ya que el tráfico ssh interactivo es tráfico de mínimo retardo y va a la banda 0, según ya se vio. Probemos a enviar un fichero con scp, que según lo visto, debería ir a la banda 2 (la de menor prioridad):

# scp /bin/ls usuario@192.168.1.5:/dev/null
usuario@192.168.1.5's password: 
ls                                              100%  116KB 115.5KB/s   00:00
# tc -s class s dev eth0
class prio 1:1 parent 1: 
 Sent 9654 bytes 85 pkt (dropped 0, overlimits 0 requeues 0) 
 backlog 0b 0p requeues 0 
class prio 1:2 parent 1: 
 Sent 3410 bytes 15 pkt (dropped 0, overlimits 0 requeues 0) 
 backlog 0b 0p requeues 0 
class prio 1:3 parent 1: 
 Sent 125122 bytes 95 pkt (dropped 0, overlimits 0 requeues 0) 
 backlog 0b 0p requeues 0

Cosa que efectivamente sucede. Como hemos querido emular la planificación PFIFO_FAST, no hemos llegado más allá en la configuración: nos hemos limitado a usar tres bandas, a usar el priomap predeterminado y no hemos definido ninguna planificación final para ninguna de las tres bandas.

Sin embargo, podríamos haber cambiado el número de bandas, la asignación del tráfico a cada banda y haber especificado planificaciones para ellas. Alterar el número de bandas es trivial: sólo hay que especificarlo al crear la planificación. Modificar la asignación de tráfico a las bandas puede hacerse de dos formas:

  1. La sencilla es, simplemente, modificar el valor de priomap al crear la planificación.
  2. La segunda consiste en modificar el valor de priomap de manera que todo el tráfico vaya siempre a la banda del tráfico predeterminado, y crear después filtros para mandar determinados tráficos a las restantes bandas. Por ejemplo, si creamos dos bandas (0 y 1) y la banda 1 es la predeterminado, el valor de priomap deberá ser 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1. Después habrá que definir filtros para que um determinado tráfico (el de SSH por ejemplo) vaya a la banda 0, o mejor dicho, la banda 1:1, que es como se llama realmente.

Por último, la asignación de una planificación final a cada banda es trivial. Por ejemplo, pasa asignar una SFQ a cada una de ellas:

# tc qdisc add dev eth0 parent 1:1 handle 10: sfq
# tc qdisc add dev eth0 parent 1:2 handle 20: sfq
# tc qdisc add dev eth0 parent 1:3 handle 30: sfq

En este artículo se propone una planificación PRIO para resolver un sistema en el que hay VoIP: se crean dos bandas: la de mayor prioridad para el tráfico VoIP y la de menor prioridad para el resto del tráfico. Para esta segunda banda se establece un planificación HTB, que se verá posteriormente, a la que irá todo el resto del tráfico. En nuestro caso de implementación, se ha usado también esta planificación para hacer que el tráfico de una subred penalizada tenga menor prioridad.

HTB

Esta planificación es una generalización de la TBF con clases, de manera que cada clase sigue una planificación TBF. Además introduce algunos conceptos nuevos para mejorar el rendimiento:

Esquema de la planificación HTB

En esta estrategia unos filtros iniciales permiten enviar el tráfico a una y otra clase (en el esquema se han considerado dos). Estas clases actuán según la planificación TBF con una modificación: una clase puede pedir pedir fichas a otra clase, si esta otra no las está utilizando, de manera que podrá enviar paquetes a una velocidad máxima dada por su ceil. En cambio, en competencia con el resto de las clases, enviaría paquetes a una velocidad dada por su rate, siempre y cuando se cumpla que la suma de las ratios de cada clase sea igual a la velocidad máxima a la que puede enviar paquetes la interfaz.

Los parámetros configurables de esta planificación son:

  • burst: es el tamaño del bidón, como en la planificación anterior. Puede definirse para cada clase.
  • rate: es la velocidad de llenado del bidón, como en la planificación anterior. Puede definirse para cada clase.
  • ceil: es la velociad máxima a la que se pueden enviar datos pidiendo prestado parte de su rate a las otras clases. Puede definirse para cada clase.
  • cburst: el tamaño del segundo bidón.
  • r2q: es la relación entre rate y el quantum tal y como se definió en la planificación SFQ, o dicho de otra forma, los quantum que se atesoran por segundo. Para valores muy pequeños (menores de 15 kbit) o muy grandes de rate es útil redefinirlo.

Obsérvese que al estar relacionadas r2q, rate y el quantum, sólo dos de ellas son variables independientes. Como r2q vale 10 de modo predeterminado, al crear la planificación HTB y definir en cada clase un rate, la variable calculada es quantum. Sin embargo, si debido a su rate el quantum resulta muy pequeño o muy grande para una determinada clase, se puede definir particularmente para ella un quantum.

Sobre la redistribución del ancho de banda sobrante de una de las clases entre las demás es importante tener en cuenta lo declarado aquí: los tokens se redistribuyen de forma proporcional al quantum de cada clase. Si no se ha redefinido ninguno en particular, resulta que el quantum de cada clase es proporcionales a su rate, por lo que en una redistribución de tokens sobrantes una clase con el doble de rate que otra, recibirá el doble de tokens extra. A menos que al definir las clases se definan prioridades (con prio) en ese caso, las clases con prioridad menor, recibirán tokens antes que las clases con prioridad mayor.

Cada clase, a su vez, puede dividirse en subclases o definirse para ella una planificación determinada (una SFQ por ejemplo).

En el apartado dedicado al caso práctico se tiene un ejemplo de cómo usar las planificaciones, las clases y los filtros, que se discutirán a continuación.

Planificación del tráfico entrante

Todo lo que se ha discutido aquí es de planificaciones para paquetes de dejan la interfaz (tráfico de salida), pero ¿qué ocurre con los paquetes que entran por la interfaz? Para la interfaz puede definirse también una planificación:

# tc qdisc add dev eth0 ingress

para la que no se puede definir ningún parametro. Lo que sí se pueden añadir a ella son filtros y estos filtros a su vez pueden tener definidos controles que pueden limitar tráfico como se hace en la planificación TBF. Por ejemplo:

# tc filter add dev eth0 parent ffff: protocol ip u32 match ip src 172.22.0.0/16 police rate 512kbit burst 128k mtu 64k drop flowid :1

Como puede observarse con police se han definido los parámetros propios de una planificación TBF. Como se ha añadido drop, esto significa que todos los paquetes que excedan este ratio serán desechados. El significado de esta línea es que se permitiría un tráfico máximo de entrada para paquetes procedentes de la red 172.22.0.0/16 de 512 Kbit/s.

Pueden añadirse varios filtros con distinta prioridad, de modo que se podrá limitar el ratio de entrada de distintos tráficos (aunque para entenderlo será necesario estudiar antes los filtros). También es posible definir planificaciones de los tipos ya estudiados recurriendo a la triquiñuela de de desviar el tráfico hacia una interfaz virtual ifb0 y tratar el tráfico de salida de esta interfaz. Sin embargo, todo esto es por lo general poco eficaz dado el nulo control que tenemos sobre el tráfico que recibimos. A la postre la mejor planificación de entrada es controlar adecuadamente el tráfico de salida.

Para establecer este último control han de tenerse en cuenta al menos tres aspectos:

  1. Los anchos de banda de subida y bajada que nos ofrece el ISP no son los efectivos de los que disfrutaremos a la salida y la entrada de la interfaz externa del servidor, puesto que la necesaria encapsulación que se haga para conectar con el router del proveedor hace perder algo.
  2. Conviene no saturar el enlace, para impedir que el router del ISP ponga en cola muchos paquetes y se dispare la latencia. Esto se traduce en desechar paquetes para que el tráfico de entrada no alcance el ancho de bajada.
  3. En los servicios TCP en que fundamentalmente un cliente obtiene datos de un servidor (HTTP es un buen ejemplo), por cada paquete de datos que el servidor envía al cliente, éste responde con un pequeño paquete que confirma la recepción. Por tanto, si disminuye el flujo de paquetes de confirmación, disminuitá el flujo de datos que envíe el servidor. Esto puede sernos una herramienta muy poderosa para gestionar el tráfico de entrada sin aplicar ninguna planificación directamente. Por ejemplo, si tenemos una serie de clientes internos que se conectan a servidores web externos, actuar sobre sus pauqtes de confirmación, tiene un efecto directo sobre la velocidad con la que bajarán los datos.

Bajo el próximo epígrafe se discutirán estos aspectos haciendo algunos números orientivos.

Cálculos orientativos

Pérdida por encapsulación

Si disponemos de una conexión ADSL o de fibra óptica es probable que se use ATM para transferir los datos y PPPoE. Estos protocolos consumen parte del ancho de banda en sus cabeceras lo que implica una merma en los anchos de banda que publicita el operador. Un cálculo estimativo puede hacerse a partir de la siguiente forma:

  • La cabecera PPPoE requiere 8 bytes. Como la MTU suele ser de 1500, tenemos una pérdida de 8 bytes cada 1500 (o sea, un 0,53%).
  • Cada celda ATM se compone de 53 bytes, de los cuales 48 son de datos. Esto, pues, supone una pérdida de 6 bytes cada 53. Si pasamos a 1500 bytes y redondeamos al alza, obtenemos un total de 174 bytes cada 1500.

En total, se pierden unos 153 bytes cada 1500, o lo que es lo mismo, un 10,2% de pérdidas.

Flujo de tráfico de confirmación

Calculemos ahora la relación que existe entre un flujo de datos de descarga y el flujo de datos de confirmación que existe en los protocolos que usan TCP en la capa de transporte. Los paquetes ACK de confirmación suelen tener un tamaño de 60 ó 70 bytes, mientras que los paquetes de datos, si la descarga es prolongada tendrán casi todos ellos un tamaño cercano al MTU, o sea, 1500 bytes. Como previamente hay una negociación en la que estos paquetes no son tan grandes y al final también podrá haber paquetes algo más pequeños, supongamos que el tamaño medio de estos paquetes es de 1300 bytes. En estas condiciones, para cada paquete de 1300 bytes de descarga, existe un paquete de confirmación de unos 65 bytes, lo cual significa que el tráfico de confirmación es aproximadamente un 5% del tráfico de descarga.

Este número, no obstante, es muy aproximado dependiendo del caso puede ser mayor o menor. Por ejemplo, si un único cliente descarga un largo fichero de internet la relación baja a aproximadamente el 3.5%.

Filtros

Los filtros permiten clasificar los paquetes, o dicho de otro modo, colocarlos en distintas colas. tc provee distintos métodos de filtrado (o clasificadores):

  1. El protocolo RSVP, pero que sólo es útil si se tiene el control de toda la red, por lo que no sirve para controlar el tráfico hacia internet.
  2. tcindex.
  3. route, que se basa en clasificar el tráfico basándose en las tablas de encaminamiento.
  4. fw, que se basa en observar la marca del paquete fijada por el cortafuegos.
  5. u32, que se basa en observar directamente el paquete y tomar decisiones en base a lo que se encuentre en él.
  6. ematch, el método más moderno y más verśatil, semejante al anterior, pero mucho más potente.

Dedicaremos este apartado fundamentalmente a explicar los dos últimos métodos, ya que exige el conocimiento de una sintaxis específica para la escritura de los filtros. No obstante, también se expondrán fw y route, que no tienen excesiva complicación.

Los filtros pueden adjuntarse tanto a una qdisc como a una clase, ahora bien, para que se apliquen es necesario que el paquete la visite. Es importante tener en cuenta esto, puesto que si adjuntamos un filtro a una clase que el paquete no visita nunca, entonces el filtro jamás se aplicará. A menos que haya muchos filtros y nuestro control sea muy complejo y lo recomiende, lo más sencillo es es adjuntar los filtros a la root qdisc, ya que todos los paquetes que pretendan salir por la interfaz llegan a ella.

Supongamos que tenemos la siguiente planificación para nuestra interfaz de salida eth0:

# tc qdisc add dev eth0 root handle 1: htb default 20
# tc class add dev eth0 parent 1:0 classid 1:1 htb rate 512kbit
# tc class add dev eth0 parent 1:1 classid 1:10 htb rate 100kbit ceil 200kbit prio 1
# tc class add dev eth0 parent 1:1 classid 1:20 htb rate 412kbit ceil 512kbit prio 2

Como consecuencia de ellas, todo el tráfico del que no se diga nada al respecto, irá a la clase 1:20. Nuestra intención es crear filtros que hagan que el tráfico que nosotros dispongamos, vaya a la clase 1:10. En este caso, un ejemplo de filtro tendría este aspecto:

# tc filter add dev eth0 parent 1:0 protocol ip prio 1 <filtro> flowid 1:10

Como vemos, se adjunta el filtro a la root qdisc de la interfaz eth0 con prioridad 1. Esta prioridad determina el orden en que se aplican los filtros, si se dispone varios: cuanto menor sea la prioridad, antes intentará aplicarse. La parte que se ha notado vagamente como filtro será la expresión del filtro, que ya veremos cómo se hace bajo los siguientes epígrafes. También hay que indicar, obviamente, hacia que clase se dirigirá todo paquete que cumpla con el filtro. Se ha indicado además que el protocolo es ip, pero pospondremos la discusión para el final del epígrafe.

Si queremos borrar el filtro basta con:

# tc filter del dev eth0 parent 1:0 prio 1

Y si queremos mostrar todos los filtros definidos sobre la interfaz:

# tc filter show dev eth0

o bien, para ver las estadísticas de paquetes que han acabado en cada clase:

# tc -s class show dev eth0

Finalmente, retomemos la discusión sobre el protocolo que se indica al añadir un filtro. Si observamos una cabecera Ethernet:

Esquema de una cabecera Ethernet II

veremos que en el último campo de dos bytes (EtherType) se indica el tipo de protocolo que encapsula la trama (ip, arp, etc). Al decir, por ejemplo, protocol ip, estamos diciéndole al comando que compruebe si ese campo tiene el valor 0x800, que es el valor para el protocolo IPv4. Si lo que pretendiéramos es filtrar tráfico arp entonces deberíamos indicar protocol arp. Lo habitual, por tanto, es que siempre se indique que el protocolo es el ip. Ahora bien, hay una excepción bastante común a esto: cuando se trabaja con tráfico etiquetado mediante el protocolo 802.1q. Cuando este es el caso, entre la MAC de origen y el campo EtherType, se añaden cuatro bytes:

Esquema de una cabecera Ethernet II con VLAN

Los dos primeros siempre son 0x8100, que indican que el protocolo de etiquetado es el 802.1q y de los otros dos los doce últimos bits son el identicador de la VLAN. Luego, ya sí, se encuentra el campo EtherType con valor 0x800, si lo que se encapsula es un paquete ip. Por esta razón, cuando el tráfico está etiquetado no puede indicarse protocol ip, puesto que donde debería haber 0x800 hay un 0x8100. En este caso es necesario indicar que el protocolo es el 8021q. Por ejemplo, para filtrar tráfico etiquetado como de la vlan 2:

# tc filter add dev eth0 parent 1:0 protocol 802.1q u32 match u16 0x0002 0x0fff at -4 flowid 1:100

Ya se entenderá convenientemente este filtro cuando se discuta sobre el tipo de filtro u32.

Filtro route

Este tipo de filtros se basan en las entradas de las tablas de encaminamiento. Se realizan así:

# tc filter add dev eth0 parent 1:0 protocol ip prio 1 route [to realm] [from realm] [fromif link] flowid 1:10

es decir, se incluye la palabra route y una o varias de estas tres condiciones.

La condición fromif es clara: el filtro se cumple cuando el paquete procede de la interfaz cuyo nombre se indica (el cual es el que aparece al hacer un ip link show). En cambio, las otras dos exigen antes la explicación de qué es eso de realm.

Tanto ip rule como ip route permiten, al añadir una regla o una entrada, incluir el reino (realm en inglés) al que queremos que pertenezca el origen o el destino. Por ejemplo:

# ip rule add from 192.168.1.0/24 lookup default realm 1
# ip route add 192.168.10.0/24 via 192.168.2.5 realm 10

Por supuesto, varias reglas y entradas pueden referirse a un mismo reino. Sabido esto, si escribimos el filtro:

# tc filter add dev eth0 parent 1:0 protocol ip prio 1 route to 10 flowid 1:10

Los paquetes que vayan dirigidos a la red 192.168.10.0/24 cumplirán con el filtro y acabarán en la clase 1:10. Si el filtro hubiera sido route from 10, entonces acabaría en dicha clase los paquetes que procedieran de tal red.

Como estos números no son muy significativos, es posible asociarles nombres escribiéndolos en el fichero /etc/iproute2/rt_realms.

Este tipo de filtro (del que se puede obtener una descripción detallada aquí), puede resultar útil si queremos controlar tráfico según origen o destino de los paquetes, pero es totalmente inútil si nuestro control lo queremos efectuar por tipo de tráfico.

Filtro fw

Consiste en filtrar atendiendo a la marca de paquete que haya podido asignarse con el cortafuegos (véase el documento dedicado a él). Permite, pues, toda la flexibilidad en la identificación del tráfico que proporciona la manipulación con iptables y sólo tiene el inconveniente de que estas marcas puedan entrar en conflicto con marcas que usemos para otros fines, como el encaminamiento.

Conociendo las herramientas de netfilter es muy sencillo de usar. Por ejemplo, si nuestra intención es clasificar el tráfico de salida de nuestro servidor web podemos hacer lo siguiente para marcarlo con un 1:

# iptables -t mangle -A OUTPUT -o eth0 -p tcp --sport 80 -j MARK --set mark 0x1

o alternativamente (que es más eficiente):

# iptables -t nat -A OUTPUT -o eth0 -p tcp --sport 80 -j CONNMARK --set mark 0x1
# iptables -t mangle -A POSTROUTING -m connmark --mark 0x1 -j CONNMARK --restore-mark

Ahora podríamos construir así el filtro:

# tc filter add dev eth0 parent 1:0 protocol ip prio 1 handle 1 fw flowid 1:10

Filtro u32

Este filtro se basa en inspeccionar directamente el paquete durante el proceso de control incluyendo en la misma sentencia tc filter las reglas de filtrado. Exige en algunos casos conocer bien el formato de las cabeceras IP y las de nivel superior como TCP, UDP o ICMP.

Este filtro tiene dos variantes:

  1. una que usa selectores generales en que las comparaciones se hacen en crudo sobre las cabeceras del paquete, lo cual significa que el filtro equivale básicamente a decir tómame los paquetes cuyos bytes tales tiene tal valor en hexadecimal. Para esto, obviamente, debemos conocer con exactitud cuál es el formato de las cabeceras.
  2. otra que usa selectores específicos, que permiten ser menos críptico al crear el filtro. Por ejemplo, tómame los paquetes cuya ip de origen sea tal.

Una explicación detallada de este tipo de filtro puede encontrarse aquí.

Selectores generales

En este caso la parte correspondiente al filtro tiene este aspecto:

u32 match [u32|u16|u8] PATRON MASCARA at [<despl>|<nexthdr+despl>]

Por ejemplo:

# tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32 match u8 6 0xff at 9 flowid 1:10

u8, u16 y u32 indican si nuestro patrón es un byte (8 bits), una palabra (16 bits) o una palabra doble (32 bits). el PATRÓN es el valor que pretendemos encontrar en los bits en los que nos fijamos, después de haberles aplicado la MÁSCARA. Por último at nos permite indicar el desplazamiento, esto es, a partir de qué byte desde el comienzo de la cabecera ip nos debemos fijar. Si al desplazamiento anteponemos la expresión nexthdr el comienzo no es el comienzo del paquete (esto es de la cabecera ip), sino el comienzo de la cabecera del protocolo de capa de transporte que, como se sabe, sigue inmediatamente a la cabecera ip. En realidad, esta última variante para indicar el desplazamiento tras el final de la cabecera ip exige alguna cosa más, así que se pospondrá su uso y discusión hasta que toque.

Para entender plenamente estas instrucciones podemos fijarnos en la expresión del filtro de ejemplo:

match u8 6 0xff at 9

Pretendemos comprobar si el valor del noveno (at 9) byte (u8) es 6 (en decimal, puesto que no se antecedido la cifra de 0x), después de que haberle aplicado la máscara 0xff. Esta máscara en binario consiste en todo unos, así que dejamos el noveno byte del paquete tiene que valor originariamente 6. Si miramos el formato de la cabecera IP (cuyo esuqema se toma de aquí:

Esquema de la cabecera IP

vemos que el noveno byte indica el protocolo de capa de transporte, cuyos identificadores se pueden consultar en /etc/protocols. El número 6 se corresponde con el protocolo TCP, así que nuestro filtro viene a significar: tomar los paquetes cuyo protocolo de capa de transporte sea TCP.

Otro ejemplo, puede ser el siguiente:

match u32 0xc0a8ff00 0xffffff00 at 16

Si observamos el esquema, veremos que en el byte 16, comienza la dirección ip de destino y tomamos una longitud de 32 bits, que es la longitud de estas direcciones. Si pasamos a formato CIDR el patrón y la máscara, resulta que obtenemos 192.168.255.0/24, así que la conclusión es que pretendemos seleccionar los paquetes cuya dirección de destino sea esta red.

Para indicar varias condiciones que deben cumplirse a la vez basta con yuxtaponer las expresiones:

match u8 6 0xff at 9 match u32 0xc0a8ff00 0xffffff00 at 16

En este caso, seleccionamos los paquetes TCP cuya red de destino sea la 192.168.255.0/24.

Selectores específicos

Los selectores específicos, simplemente, permiten referirnos de un modo más legible a todos estos campos de la cabecera IP que comprobamos con algo de pericia mediante los selectores generales. Al final de documento se desglosan todos los posibles, que son muchos. Un ejemplo es este:

match ip dst 192.168.255.0/24

que equivale al penúltimo filtro que se ilustró para los selectores específicos. En principio, nos permiten comprobar valores que se encuentren incluidos en la cabecera ip. Ahora bien, si consultamos el documento enlazado comprobaremos que hay selectores como este:

match ip dport 80 0xffff

que nos permiten indicar el puerto de destino. Esto, en realidad, es hacer trampas, porque el puerto de destino es un parámetro de los protocolos TCP o UDP y no del protocolo IP. Tal es así, que si echamos un vistazo a los filtros definimos, veremos que el filtro que acabamos de definir se apunta del siguiente modo:

match 00000050/0000ffff at 20

Esto es. se comprueban los cuatro primeros bytes a partir del byte 20, o sea, los custro bytes siguientes a la cabecera ip, siempre que esta tenga efectivamente 20 bytes de longitud y no se le añadan opciones después que hacen que varíe su tamaño. Como es lo habitual, es en este byte donde empieza la cabecera del protocolo de capa de transporte y, si consultamos un esquemos de la cabecera TCP o UDP, veremos que los dos primeros bytes se corresponden con el puerto de origen de la conexión y los dos siguientes con el de destino. Dada la máscara que hay en realidad estamos comparando el valor únicamente de estos dos últimos puertos, así que en condiciones normales (y suponiendo tráfico TCP o UDP), la expresión funciona: estamos comprobando el puerto de destino. Por supuesto, podemos yuxtaponer la expresión que asegura que el protocolo de la capa de transporte es TCP:

match ip dport 80 0xffff match ip protocol 6

Hay otro tipo de opciones que, directamente, afirman compruebar parámetros de las capas de transporte. Por ejemplo:

match tcp dst 80 0xffff

que aparente es equivalente a la anterior discutida. Sin embargo, si comprobamos cómo se apunta en la lista de filtros veremos lo siguiente:

match 00000050/0000ffff at nexthdr+0

o sea, que, en vez de dar por hecho que la cabecera tiene 20 bytes usa la expresión nexthdr+0 que significa al comienzo de la siguiente cabecera más un desplazamiento igual a 0. Dicho de otro forma, al comienzo de la cabecera del protocolo de transporte. Sin embargo, esto escrito tal cual no funciona, porque hay que indicarle a tc cómo puede calcular la longitud de la cabecera ip. De esto es lo que se tratará bajo un pŕoximo epígrafe. Antes, sin embargo, hay que introducir algunos conceptos más.

Tablas hashing

Cuando se crea un filtro de tipo u32, el núcleo de linux crea automáticamente una tabla hashing, que es simplemente una serie de buckets a cada uno de los cuales se les puede asignar uns lista de filtros. Esta tabla es la tabla 800 que sólo posee un bucket. Por este motivo si fijáramos un sólo filtro:

# tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32 match u8 6 0xff at 9 flowid 1:10

al consultar los filtros definidos se obtiene lo siguiente:

# tc filter show dev eth0
filter parent 1: protocol ip pref 1 u32 
filter parent 1: protocol ip pref 1 u32 fh 800: ht divisor 1 
filter parent 1: protocol ip pref 1 u32 fh 800::800 order 2048 key ht 800 bkt 0 flowid 1:10 
  match 00060000/00ff0000 at 8

La expresión fh 800: ht divisor 1 es la que indica que la tabla sólo tiene un bucket. Si observamos la siguiente regla comprobaremos que nuestro filtro está apuntado como 800::800 (o 800:0:800), lo cual quiere decir que se encuentra en la tabla 800, bucket 0 y su número es el 800. Si ahora añadiéramos otro filtro:

# tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32 match ip dport 80 0xffff flowid 1:10
# tc filter show dev eth0
[...]
filter parent 1: protocol ip u32 fh 800::801 order 2049 key ht 800 bkt 0 flowid 1:10 
  match 00000050/0000ffff at 20

Observaríamos que este se encuentra en la misma tabla y bucket y su número es el siguiente ya que la terna de números ha de ser única para cada regla. Esta terna se conoce como handle. Podemos especificar nosotros el número de filtro en vez de dejar que el núcleo lo ponga por nosotros (los valores válidos van de 1 a 0xffe):

# tc filter del dev eth0 prio 1 handle 800::801 u32
# tc filter add dev eth0 parent 1:0 protocol ip prio 1 handle ::1 u32 match ip dport 80 0xffff flowid 1:10
# tc filter show dev eth0
filter parent 1: protocol ip pref 1 u32 
filter parent 1: protocol ip pref 1 u32 fh 800: ht divisor 1 
filter parent 1: protocol ip pref 1 u32 fh 800::1 order 1 key ht 800 bkt 0 flowid 1:10 
  match 00000050/0000ffff at 20
filter parent 1: protocol ip pref 1 u32 fh 800::800 order 2048 key ht 800 bkt 0 flowid 1:10 
  match 00060000/00ff0000 at 8

Si nos conviene, también podemos crear nuestras propias tablas. Por ejemplo, limpiemos todo y creemos la 900 con un solo bucket:

# tc filter del eth0 prio 1
# tc filter add dev eth0 parent 1:0 protocol ip prio 1 handle 900: u32 divisor 1

En este punto deberíamos tener lo siguiente:

# tc filter show dev eth0
filter parent 1: protocol ip pref 1 u32 
filter parent 1: protocol ip pref 1 u32 fh 900: ht divisor 1 
filter parent 1: protocol ip pref 1 u32 fh 800: ht divisor 1

o sea, la tabla que se genera automáticamente y la que hemos creado nosotros. Ahora bien, las reglas que introduzcamos en esta tabla no se comprobarán automáticamente a menos que enlacemos con ellas:

# tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32 match ip protocol 6 0xff link 900:

Esto viene a significar que todo el tráfico TCP pase a ser comprobado por las reglas de la tabla 900. Creemos una regla:

# tc filter add dev eth1 parent 1:0 protocol ip prio 1 u32 ht 900: match ip sport 80 0xffff

Con lo que los filtros quedarán así:

# tc filter show dev eth0
filter parent 1: protocol ip pref 1 u32 
filter parent 1: protocol ip pref 1 u32 fh 900: ht divisor 1 
filter parent 1: protocol ip pref 1 u32 fh 900::800 order 2048 key ht 900 bkt 0 
  match 00500000/ffff0000 at 20
filter parent 1: protocol ip pref 1 u32 fh 800: ht divisor 1 
filter parent 1: protocol ip pref 1 u32 fh 800::800 order 2048 key ht 800 bkt 0 link 900: 
  match 00060000/00ff0000 at 8

Obsérvese que la regla que manda todo el tráfico TCP a ser comprobado con las reglas de la tabla 900, se encuentra en la 800 y, por tanto, se comprobará automáticamente.

Esto nos puede ayudar al cálculo de nexthdr.

Cálculo de nexthdr

La estrategia consiste en calcular la longitud de la cabecera ip al enlazar con la tabla para que se aplique en todas las reglas de la tabla enlazada. Lo primero es crear la tabla:

# tc filter add dev eth0 parent 1:0 protocol ip prio 1 handle 900: u32 divisor 1

Ahora hay que enlazar determinado cuál es tamaño de la cabecera. Si se observa el formato de la cabecera ip, se verá que este dato se almacena en el segundo nibble del primer byte. Sin embargo, este dato no se almacena en bytes (que es como se expresan los desplazamientos), sino en palabras largas, por lo que el resultado habrá que multiplicarlo por 4. Sabido todo esto el comando apropiado es el siguiente:

# tc filter add dev eth0 parent 1:0 protocol ip prio 1 h32 offset at 0 mask 0x0f00 shift 6 plus 0 eat match ip protocol 6 0xff link 900:

La razón de que el comando se escriba así es que offset at 0 nos toma la primera palabra, a la cual le aplicamos la máscara 0x0f00, lo cual convierte en ceros todos los dígitos que no sean el segundo nibble del primer bytes. Como nos sobran todos los ceros del segundo byte debemos hacer un desplazamiento de 8 hacia la derecha, pero como debemos luego multiplicar por 4, debemos desplazar dos a la izquierda. La consecuencia es que el desplazamiento final es de 6 (shift 6). A este resultado no hay que sumar nada (plus 0. eat es necesario para que este desplazamiento se aplique a todas las reglas de la tabla enlazada. Finalmente, ya podemos escribir la regla en la tabla 900:

# tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32 ht 900: match tcp src 80 0xffff flowid 1:10

que hará que se compruebe bien cuál es el puerto de origen del protocolo TCP, sea cual sea el tamaño de la cabecera ip.

Filtro basic (ematch)

Este filtro es sensiblemente más potente que el anterior: permite escribir operadores lógicos (not, or and) o de comparación al introducir las condiciones. Dispone, además, de varios módulos que nos permiten expresar nuestras condiciones de distinto modo. Repasaremos tres:

  1. u32, que consiste en expresar las condiciones con selectores generales tal y como se ha indicado ya.
  2. cmp, en que se pueden hacer comparaciones sobre las cabeceras de los paquetes. La ventaja sobre el anterior es que podemos indicar sobre qué cabecera..
  3. ipset, que permite hacer comprobaciones sobre los conjuntos definidos con ipset.

La sintaxis básica para usar este tipo de filtros es la siguiente:

# tc filter add dev eth0 parent 1:0 protocol ip prio 1 basic method "[not] modulo1(expresion) [or|and] [not] modulo2(expresion) ..." flowid 1:10

o sea, se indica ibasic method y a continuación se hace uso de uno o varios módulos tantas veces como se quiera uniéndoles mediante operadores lógicos. Obviamente el resultado de moduloX(expresion) será verdadero, si el paquete cumple, o falso, si no lo hace.

Módulo u32

No necesita mucha explicación. Las expresiones que se pueden usar son aquellas capaces de escribirse con los selectores generales. Por ejemplo, si hubiéramos querido indicar que filtrar por el puerto de origen usando selectores generales habríamos usado esta expresión:

match u16 80 0xffff at 20

Pues bien, para usar el módulo u32, hay que escribir lo mismo prescindiendo de la palabra match:

# tc filter add dev eth0 parent 1:0 protocol ip prio 1 basic match "u32(u16 80 0xffff at 20)" flowid 1:10

Ya debemos saber que esto no comprueba si el protocolo es TCP o UDP, para ello había que yuxtaponer la comprobación en el filtro u32. En este caso, habrá que usar el operador and:

# tc filter add dev eth0 parent 1:0 protocol ip prio 1 basic match "u32(u16 80 0xffff at 20) and u32(u8 6 0xff at 9)" flowid 1:10

Obviamente, no tiene mucho sentido usar este tipo de filtro, si vamos luego a usar este módulo exclusivamente.

Módulo cmp

Este módulo es semejante al u32 en la medida en que se basa en comparar bytes de las cabeceras, pero es bastante más versátil. Podemos obtener una ayuda de su sintaxis del siguiente modo:

# tc filter del dev eth0 basic match "cmp(help)"
cmp: invalid alignment
... ...
... cmp(>>help<<)...
Usage: cmp(ALIGN at OFFSET [ ATTRS ] { eq | lt | gt } VALUE)
where: ALIGN  := { u8 | u16 | u32 }
       ATTRS  := [ layer LAYER ] [ mask MASK ] [ trans ]
       LAYER  := { link | network | transport | 0..2 }

Example: cmp(u16 at 3 layer 2 mask 0xff00 gt 20)
Illegal "ematch"

Como puede observarse, las comparaciones no tienen por qué ser estrictamente iguales, si no tambié mayor o menor y se puede especificar la capa que se quiere comprobar. El ejemplo anterior resuelto con el módulo u32 tendría esta solución:

cmp(u16 at 0 layer transport eq 80) and cmp(u8 at 9 layer network eq 6)

Por supuesto se pueden mezclar ambos módulos en la solución:

cmp(u16 at 0 layer transport eq 80) and u32(u8 6 0xff at 9)
Módulo ipset

Permite comparar el paquete con conjuntos definidos mediante ipset. Supongamos que hemos definido este conjunto:

# ipset create puertos bitmap:port 1-65535
# ipset add puertos 80

En el que, como se ve se ha incluido el puerto 80. Para filtrar, los puertos de este conjunto habría, simplemente, que usar esta expresión:

ipset(puertos src)

El src es debido a que nos interesa clasificar por puerto de origen (para continuar con el ejemplo). Cuando los conjuntos están constituidos por parejas de valores (p.e. hash:net,net), entonces hay que separar src o dst con comas. Por ejemplo:

ipset(viajes src,dst)

Acciones y control sobre el filtro (action/police)

Para hacer aún más ricos los filtros, tc permite añadir acciones (action) y control (police) al comando en que se definen. Supongamos el siguiente filtro para el tráfico que sale por la interfaz eth0:

# tc filter add dev eth0 parent 1: protocol ip match ip dst 172.22.0.0/16 flowid 1:20

Es claro: mandaremos hacia la clase 1:20 todo el tráfico que vaya destinado a la red 172.22.0.2/16, con independencia de que este sea mucho o poco. En cambio si añadimos una police, podremos hacer que se tomen ciertas decisiones cuando el tráfico excede un determinado umbral. Por ejemplo:

# tc filter add dev eth0 parent 1: protocol ip match ip dst 172.22.0.0/16 action police rate 1mbit burst 128k mtu 64k drop flowid 1:20

El filtro es el mismo, pero se ha introducido un control que indica que si se sobrepasa la velocidad de 1 Mbit/s (aunque hay un burst de 100 kilobytes) el tráfico excedente será desechado (drop).

Se pueden hacer varias acciones, cuando el tráfico excede el umbral definido:

drop
Como ya se ha explicado con el ejemplo, simplemente, desecha paquetes al sobrepasar el umbral
pass
Deshabilita el control, es decir, actúa como si este no se hubiera escrito.
continue
Hace que, cuando se sobrepase el umbral, deje de ser clasificado por el filtro. Por tanto, si hay otros filtros de menor prioridad, podrán ser clasificados por estos.
reclassify
Re-clasifica el tráfico que excede el umbral, de suerte que le asigna menos prioridad.

Para ilustrar lo dicho vamos a suponer que definimos una planificación para la entrada de la interfaz:

# tc qdisc add dev eth0 ingress

Si definimos este filtro con prioridad 1:

# tc filter add dev eth0 parent ffff: protocol ip prio 1 u32 match u32 0 0 flowid :1 action police rate 1mbit burst 128k mtu 64k pass

el efecto es nulo: todo el tráfico (nuestro filtro es universal) tiene un control del ratio, pero al incluir pass este simplemente se ignorará. Por este motivo, si hacemos una prueba de velocidad con netcat (ver el próximo apartado) veremos que la limitación de 1 Mbit/s no es efectiva.

En cambio, su usamos si borramos el anterior filtro y usamos la acción drop:

# tc filter del dev eth0 parent ffff: prio 1
# tc filter add dev eth0 parent ffff: protocol ip prio 2 u32 match u32 0 0 flowid :1 action police rate 1mbit burst 128k mtu 64k drop

la limitación es plenamente efectiva. Asignar a este nuevo filtro la prioridad 2, no ha sido capricho, porque ahora añadiremos un nuevo filtro con prioridad 1 sin borrar este:

# tc filter add dev eth0 parent ffff: protocol ip prio 1 u32 match u32 0 0 flowid :1 action police rate 1mbit burst 128k mtu 64k continue

Con lo cual la cosa quedaría así:

# tc filter show dev eth0 parent ffff:
filter protocol ip pref 1 u32 
filter protocol ip pref 1 u32 fh 801: ht divisor 1 
filter protocol ip pref 1 u32 fh 801::800 order 2048 key ht 801 bkt 0 flowid :1 
  match 00000000/00000000 at 0
          action order 1:  police 0x11 rate 1Mbit burst 128Kb mtu 64Kb action continue overhead 0b 
ref 1 bind 1

filter protocol ip pref 2 u32 
filter protocol ip pref 2 u32 fh 800: ht divisor 1 
filter protocol ip pref 2 u32 fh 800::800 order 2048 key ht 800 bkt 0 flowid :1 
  match 00000000/00000000 at 0
          action order 1:  police 0x10 rate 1Mbit burst 128Kb mtu 64Kb action drop overhead 0b 
ref 1 bind 1

Es decir, el primer filtro limita el tráfico clasificado a 1 Mbit/s, pero este tráfico no es desechado, sino que con el se pasa a comprobar el segundo filtro (continue). Este segundo filtro clasifica otro tanto de tráfico y desecha el resto. La consecuencia es que, si probamos la velocidad de descarga, veremos que deascargas a 2 Mbit/s. No es que esto que acabamos de hacer sea muy útil, pero sirve para ilustrar cómo funciona continue. Pero no se vayan todavía, aún hay más, que prometía Super Ratón. Si se observa la salida de la consulta de los ciclos definidos, se verá que en cada filtro hay definida una acción que tiene un número de orden, lo cual puede hacernos llevar a pensar que para cada filtro pueden definirse varias acciones consecutivas. Y, efectivamente, es así, y basta usar pipe. Borremos los filtros y escribamos este otro:

# tc filter add dev eth0 parent ffff: protocol ip prio 1 u32 match u32 0 0 flowid :1\
action police rate 1mbit burst 128k mtu 64k\
action police rate 1mbit burst 128k mtu 64k drop

Si volvemos a consultar:

# tc filter show dev eth0 parent ffff:
filter protocol ip pref 1 u32 
filter protocol ip pref 1 u32 fh 800: ht divisor 1 
filter protocol ip pref 1 u32 fh 800::800 order 2048 key ht 800 bkt 0 flowid :1 
  match 00000000/00000000 at 0
        action order 1:  police 0x12 rate 1Mbit burst 128Kb mtu 64Kb action pipe overhead 0b 
ref 1 bind 1

        action order 2:  police 0x13 rate 1Mbit burst 128Kb mtu 64Kb action drop overhead 0b 
ref 1 bind 1

Obviamente, una prueba volverá a darnos una velocidad de descarga de 2 Mbit/s. En realidad, todas estas pruebas no pasan de ser meras comprobaciones porque no hacemos nada más hayá de acabar desechando, aunque troceemos el tráfico en bandas. En este caso, hay una primera banda de tráfico (el primer 1 Mbit/s), una segunda banda de igual anchura) y el resto del tráfico (por encima de los 2 Mbit/s, que se desecha. Sin embargo, a cada banda se le puede asociar una acción. Por ejemplo, podemos crear dos interfaces dummy, de manera que el primer Mbit/s lo enviemos a dummy0 y el resto a dummy1:

# tc filter add dev eth0 parent ffff: protocol ip prio 1 u32 match u32 0 0 flowid :1\
action mirred egress mirror dev dummy0\
action police rate 512kbit burst 128k mtu 64k pipe\
action mirred egress mirror dev dummy1

Si ahora, escuchamos tráfico con tcpdump en una y otra interfaz observaremos que ambas reciben tráfico.

Existen otras acciones interesantes:

mirred

Permite enviar tráfico hacia otra interfaz, bien clonándolo, bien desviándolo. Suponiendo que tengamos una una planificación para la entrada:

# tc qdisc add dev eth0 ingress

Lo siguiente duplicaría el tráfico hacia una interfaz dummy0:

# tc filter add dev eth0 parent ffff: protocol ip prio 1 u32 match u32 0 0 flowid :1 action mirred egress mirror dev dummy0

Esto significa que en ambas interfaces se observarían (con tcpdump, por ejempl) todos los paquetes que entran por eth0. Como la interfaz dummy desecha todos los paquetes que llegan a ella estos se perderían.

En cambio, podemos optar por redirigir los paquetes hacia la salida de una interfaz ifb:

# tc filter add dev eth0 parent ffff: protocol ip prio 1 u32 match u32 0 0 flowid :1 action mirred egress redirect dev ifb0

En este caso, no hay clonación, sino que todos los paquetes acaban en ifb0. Esta interfaz no desecha los paquetes y, además, al colocarlos en su salida, permite todas las planificaciones estudiadas.

xt

Permite aplicar acciones propias de iptables como si se hicieran en la tabla mangle de PREROUTING si la usamos a la entra de paquetes, o de POSTROUTING si la usamos a la salida:

# tc filter add dev eth0 parent ffff: protocolo ip prio 1 u32 match ip src 172.22.0.0/16 flowid :1 action xt -j MARK --set-mark 0x2

Comprobación

Tan importante como saber escribir los filtros, es poder comprobar luego que, efectivamente, funcionan. Para ello, podemos generar tráfico del tipo que clasifica el filtro en cuestión y comprobar que este actúa.

Lo primero (generar tráfico) depende mucho de la naturaleza del tráfico que hayamos querido filtrar. Si la naturaleza del tráfico era simplemente que fuera de un tipo determinado de servicio, muy probablemente nos habremos basado en el puerto característico de este servicio. Por ejemplo, supongamos que hemos pretendido clasificar tráfico SSH de salida, es decir, tráfico destinado al puerto 22. Si tenemos una máquina externa con un servidor de este tipo, no hay problema y bastará con conectarse, pero si no disponemos de ella en el momento, es fácil improvisar una conexión al puerto 22. Para ello, basta con ir a una máquina externa y poner a escuchar a netcat en el puerto 22:

# netcat -l -p 22 < /dev/zero

Y ahora desde la máquina en que hemos implementado el control de tráfico, podemos conectarnos y empezar a descargar. Supongamos que la ip de la máquina externa es 192.168.1.35:

$ netcat 192.168.1.35 80 | pv > /dev/null

El uso de pv es opcional, pero nos permite saber a qué velocidad estamos descargando y esta velocidad puede sernos útil.

Una vez que tengamos tráfico que nuestro filtro debería clasificar, podemos comprobar que efectivamente lo hace:

  1. Si habíamos definido una jerarquía de clases podemos mirar las estadísticas de estas clases para comprobar que se contabilizan paquetes en la clase por la que debería ir el tráfico:
    # tc -s class show dev eth0
  2. Si estas clases son de la planificación HTB, en la que se definen rate y ceil, podemos probar a ver si estas se cumplen. Para ello, nos será muy útil pv.
  3. Los dos métodos anteriores exigen que hayamos creado clases sobre la planificación de una interfaz. Existe un tercer método que nos ahora esto y que será el que se desarrolle a continuación.

El método consiste en crear una interfaz dummy:

# modprobe dummy numdummies=1
# ip link set dummy0 up

Y clonar el tráfico de salida de la interfaz real hacia esta:

# tc qdisc add dev eth0 root handle 1: prio
# tc filter add dev eth0 parent 1: protocol ip <filtro-a-probar> action mirred egress mirror dev dummy0

Por último, podemos escuchar si el tráfico que pretendemos clasificar con el filtro, llega a ella:

# tcpdump -ni dummy0

Obsérvese que, usando la disciplina PRIO sin parámetros, mantenemos la planificación PFIFO_FAST predeterminado. Sin embargo, ahora sí podemos definir el filtro que nos clona el tráfico hacia dummy0.

Para deshacer todo esto, basta con eliminar la planificación:

# tc qdisc del dev eth0 root

Por último, es importante indicar otra comprobación, aunque no relacionada con nuestro filtros, sino con la idoneidad de nuestra planificación. En el tráfico interactivo y de inicio de conexión es muy importante la latencia, es decir, el lapso de tiempo entre que se envía un paquete y se recibe una respuesta. Cuando se pretende calcular la latencia en general es muy fácil: es un dato que proporciona el comando ping:

# ping -c2 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=55 time=26.7 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=55 time=26.9 ms

--- 8.8.8.8 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 26.777/26.867/26.957/0.090 ms

Hay información de la latencia de cada envío y una estadística final. En este caso, la latencia media es algo menor a los 27 ms.

Sin embargo, esto mide esclusivamente la latencia del protocolo icmp que puede ser totalmente diferente a la de otro tráfico dependiendo de cómo hayamos hayamos hecho la clasificación del tráfico, que ancho le hayamos asignado a cada uno y qué planificaciones finales hayamos dispuesto. Para medir la latencia de cualquier otro tipo de tráfico podemos valores de tcpdump; que, cuando se usa con el parámtro -ttt, nos devuelve la diferencia que hay entre un paquete de ida y el correspondiente de respuesta. Para probarlo vamos a hacer uso del comando ping de nuevo y a comprobar qué latencias nos devuelve este comando y qué latencias observamos con tcpdump. Obviamente deben ser las mismas. Para ello pongamos a escuchar a tcpdump:

# tcpdump -ttt -ni eth0 icmp

y a la vez, en otra terminal, problemas a mandar un par de paquetes de ping. El resultado será este:

$ ping -c2 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=55 time=28.4 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=55 time=28.2 ms

--- 8.8.8.8 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 28.242/28.353/28.465/0.201 ms

mientras que en tcpdump veremos lo siguiente:

# tcpdump -ttt -ni br0 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on br0, link-type EN10MB (Ethernet), capture size 262144 bytes
00:00:00.000000 IP 192.168.1.5 > 8.8.8.8: ICMP echo request, id 15073, seq 1, length 64
00:00:00.028442 IP 8.8.8.8 > 192.168.1.5: ICMP echo reply, id 15073, seq 1, length 64
00:00:00.973287 IP 192.168.1.5 > 8.8.8.8: ICMP echo request, id 15073, seq 2, length 64
00:00:00.028218 IP 8.8.8.8 > 192.168.1.5: ICMP echo reply, id 15073, seq 2, length 64

Obsérvese que entre el primer paquete y el segundo hay una diferencia de 28.44 ms (medida que coincide con la que obtuvimos con el comando ping y entre el tercero y el cuarto 28.22 ms.

Caso de implementación

Supongamos que disponemos un servidor (fundamentalmente SSH, FTP, DNS, VPN y WEB) que, además, sirve de router para algunas redes internas de usuarios, una de las cuales queremos además que tenga limitado el ancho de banda de acceso a internet, por ejemplo, porque es una wifi sin demasido control y puede saturarnos la red.

Conocido el problema, lo primero es analizar el tráfico y clasificarlo según queramos asegurar un ancho de banda y una prioridad a cada tipo de tráfico:

  1. El tráfico DNS para la resolución de nombres conviene que sea sea fluido.
  2. En todas las conexiones TCP el receptor envía al emisor paquetes ACK para confirmar que va recibiendo los paquetes que éste le envía (véase aquí). Es importante para que la bajada sea fluida que estos paquetes ACK suban con prontitud. Por tanto, para mejorar el servicio a los clientes de la red interna, debemos reservar ancho para estos paquetes.
  3. Como SSH y VPN nos sirve para administración remota, también conviene que podamos acceder a él con agilidad.
  4. También conviene que reservemos un ancho de banda para que el servidor web sirva páginas.
  5. Por último, podríamos decidir limitar el ancho para una determinada parte de los clientes, caracterizada porque se corresponde con una subred determinada (p.e. 192.168.255.64/26).

Control del tráfico saliente

Supongamos que:

  1. Nuestra interfaz de salida es eth0 de 1Gbit/s.
  2. Disponemos de un ancho de banda de 512Kbit/s para subida y 6Mbit/s para bajada, aunque debemos disminuir estos valores para tener en cuenta las pérdidas al salir a la WAN. Disminuyamos en un 10% estos anchos, con lo que obtendremos R=460 Kbit/s y D=5.4 Mbit/s.
  3. Entre el router (172.22.0.1) y eth0 (172.22.0.2) hay una red 172.22.0.0/16, a alguno de cuyos ordenadores se puede querer acceder sin las limitaciones del acceso a internet.
  4. La interfaz que comunica con los clientes que queremos limitar es eth1 y a estos clientes se les ha dado una interfaz en el rango 192.168.255.64/26.

Vamos a ensayar dos planificaciones: una que no tiene en cuenta la existencia de la subred penalizada; y otra que sí la tiene. Sea una o la otra es importante hacer notar que como consecuencia de que la mayor parte del tráfico es TCP, la velocidad a la que permitamos salir paquetes de nuestra máquina, afectará a la velocidad con que lleguen paquetes de vuelta a ella, ya que dicho protocolo acomodo la velocidad de envío a la de recepción.

Sin penalización

Podemos diseñar este control del tráfico de salida:

Esquema del control del tráfico de salida

La justificación de esta política es la siguiente:

  1. Hacemos una primera distinción entre el tráfico destinado a la red local externa (clase 1:20) e internet (1:10). El tráfico hacia internet nunca podrá superar la velocidad de subida contratada (R), así que se reserva para esta última clase ese rate y no se define un ceil mayor. La otra clase en cambio, sí podrá llegar a ocupar todo el en ancho disponible.
  2. Dentro del tráfico hacia internet se distingue:
    1. Los paquetes ACK que los clientes internos envían a los servidores externos para confirmar que reciben paquetes. Recordemos que este tráfico representa alrededor del 5% del tráfico de respuesta, o sea, que supondría unos 5 Mbit/s en la bajada.
    2. El tráfico de resolución de nombres, tanto el de consulta al exterior, como las respuestas de nuestro servidor.
    3. El tráfico de control interactivo de los servidores SSH, FTP y VPN.
    4. Las respuestas del servidor HTTP.
    5. El resto del tráfico.

Para crear las clases HTB:

# tc qdisc add dev eth0 root handle 1: htb default 50
# tc class add dev eth0 parent 1:0 classid  1:1 htb rate 1gbit

# tc class add dev eth0 parent 1:1 classid 1:100 htb rate 460kbit
# tc class add dev eth0 parent 1:1 classid 1:200 htb rate 999i540kbit ceil 1gbit

# tc class add dev eth0 parent 1:100 classid 1:10 htb rate 230kbit ceil 360kbit burst 32k prio 5
# tc class add dev eth0 parent 1:100 classid 1:20 htb rate  43kbit ceil 230kbit burst 32k prio 3
# tc class add dev eth0 parent 1:100 classid 1:30 htb rate  43kbit ceil 460kbit burst 64k prio 1
# tc class add dev eth0 parent 1:100 classid 1:40 htb rate  86kbit ceil 460kbit burst 64k prio 4
# tc class add dev eth0 parent 1:100 classid 1:50 htb rate  58kbit ceil 460kbit burst 32k prio 2

No obstante, lo anterior es interesante que discutamos el ancho de banda que reservaremos para el tráfico ACK de confirmación. El caso que nos ocupa es el de servidor cuya conexión utilizarán bastantes clientes internos como salida a la web. Por tanto, será un tráfico importante que afectará al rendimiento global de la conexión, por lo que el cálculo de su ancho de banda no debe hacerse muy a la ligera, no al menos tan a la ligera como se ha definido aquí. Una estrategia es definir el ancho de subida que deseamos que ocupen el resto de tráficos y obtener el rate para este tráfico como la diferencia entre el ancho total de subida y la suma de los rates de los restantes tráficos. Ahora bien, este ancho de banda para los paquetes ACK de confirmación suponen el 5% del tráfico de bajada correspondiente, por lo que haciendo una simple regla de tres, podríamos obtener este último. Si este tráfico de bajada relacionado nos consume todo el ancho de banda disponible, no habremos hecho una buena planificación, puesto que el resto de tráficos necesitan ancho de bajada.

La conclusión a todas estas disquisiciones es que para fijar el ancho reservado para el tráfico ACK debemos hacer dos números y quedarnos con el más pequeño:

  1. Calcular el ancho de subida disponible después de sustraer al ancho total de subida el ancho de subida que consumen los restantes tráficos.
  2. Calcular el ancho de bajada deseable para los clientes internos como el ancho total de bajada menos los anchos de bajada que suponen los restantes tráficos. En nuestro caso particular, podemos suponer que estos tráficos generan un tráfico de salida (subida) igual al de entrada (bajada), salvo el del servidor HTTP que generará como tráfico de entrada un 5% del de salida. Obtenido este ancho de bajada, podemos obtener el ancho de subida calculando el 5% (o quizás mejor un 3,5%). Además podemos ser algo prudentes y disminuir el número obtenido por un coeficiente corrector (4/5, 5/6, etc).

Este procedimiento de cálculo del tráfico ACK está desarrollado en el módulo m1 del script de configuración, pero lo dejaremos expresado también aquí:

Tenemos un ancho total de bajada de 5400 Kbit/s (suponiendo unas pérdidas del 10% como consecuencia del paso de la WAN a la LAN), pero para que no se colapse el enlace vamos a aprovechar sólo el 95% (véase la limitación de la entrada, lo cual significa 5130 Kbit/s. En máxima competencia podemos suponer que el tráfico DNS ocupa unos 43 Kbit/s, el tráfico interactivo otros 43 Kbit/s, el tráfico sin clasificar otros 58 Kbit/s y el tráfico HTTP hacia nuestro servidor web el 5% de 86 Kbit/s. Por tanto, nos queda un ancho libre para la navegación web de 4982 Kbit/s. Este ancho de bajada supondrá un 3,5% (o un 5%) en la subida, lo que supone unos 175 Kbit/s. Finalmente podemos aplicar a este número un coeficiente reductor de 5/6 y obtendremos un rate de 145 Kbit/s. En el gráfico y en la implementación hemos concedido al tráfico ACK 230 Kbit/s, lo cual es a todas luces excesivo. Lo mejor es tomar como referencia el primer número que es el más pequeño y hacer pruebas en torno a él para ajustar el rate definitivo para esta clase de tráfico. Obviamente la diferencia entre 230 y 145 (o el número deefinitivo que finalmente asignemos), no debemos desaprovecharla, sino repartirtlo entre los otros tipos de tráfico. Esto a su vez provocaría que el ancho de banda en la bajada se modificara y tuviéramos que rehacer el cálculo, pero como en nuestro caso particular el cambio no sería muy significativo (puesto que la bajada la ocupa casi todo el tráfico generado por los clientes), el número no variaría mucho.

Para las disciplinas finales, por su parte, podem os hacer esto:

# tc qdisc add dev eth0 parent 1:200 handle 6000: prio
# tc qdisc add dev eth0 parent 1:10 handle 1000: prio
# tc qdisc add dev eth0 parent 1:20 handle 2000: bfifo limit 17200b
# tc qdisc add dev eth0 parent 1:30 handle 3000: pfifo limit 32
# tc qdisc add dev eth0 parent 1:40 handle 4000: pfifo limit 32 
# tc qdisc add dev eth0 parent 1:50 handle 5000: sfq perturb 4 limit 32

Obsérvese la definición de la planificación final para los paquetes del tráfico DNS. Se ha usado bfifo, en vez de pfifo que es exactamente la misma planificación, excepto por el hecho de que el limit se define en bytes y no en número de paquetes, que por cierto, en este tipo de tráfico son extraordinariamente pequeños (en torno a los 50 bytes. El modo de calcular limit ha sido en establecer una latencia máxima. Por ejemplo, si no queremos que ningún paquete espere más de 400 ms en cola, entonces debemos hacer la cola de 17200 bytes a lo sumo.

Ahora toca crear los filtros. Usaremos el filtro basic para casi todos ellos:

# tc filter add dev eth0 parent 1:0 protocol ip u32 match ip dst 172.22.0.0/16 flowid 1:200

# tc filter add dev eth0 parent 1:0 protocol ip basic match "u32(u8 6 0xff at 9) and \
                                                             (cmp(u8 at 13 layer transport eq 0x10) or \
                                                              cmp(u8 at 13 layer transport eq 0x2)) and \
                                                             cmp(u16 at 2 layer network lt 100)"\
                                             flowid 1:10
# tc filter add dev eth0 parent 1:0 protocol ip basic match "u32(u8 17 0xff at 9) and \
                                                             (cmp(u16 at 2 layer transport eq 53) or\
                                                              cmp(u16 at 0 layer transport eq 53))" \
                                             flowid 1:20
# tc filter add dev eth0 parent 1:0 protocol ip basic match "(u32(u8 6 0xff at 9) and\
                                                              cmp(u8 at 1 layer network mask 0x1c eq 0x10) and\
                                                             (cmp(u16 at 0 layer transport eq 22) or
                                                              cmp(u16 at 0 layer transport eq 21))) or \
                                                             (u32(u8 17 0xff at 9) and \
                                                              cmp(u16 at 0 layer transport eq 1194))" \
                                             flowid 1:30
# tc filter add dev eth0 parent 1:0 protocol ip basic match "u32(u8 6 0xff at 9) and\
                                                             (cmp(u16 at 0 layer transport eq 80) or\
                                                              cmp(u16 at 0 layer transport eq 443))" \
                                             flowid 1:40

Obsérvese el primer filtro que es un poco más complicado que el resto: se buscan paquetes TCP, que tengan el marcador ACK (0x10) (o el 0x02, que es el marcador SYN) y, además, cuya longitud sea menor de 100 bytes. Esta última condición tiene una explicación. En el caso de un ordenador doméstico los paquetes ACK de salida serán básicamente los que envía el software cliente (un navegador, por ejemplo) al servidor (web, en ese caso) para decirle que va recibiendo la información que le ha pedido. Estos paquetes, como son paquetes de confirmación, son pequeños (sobre los 60 ó 70 bytes). Por otro lado, los paquetes que un servidor envía a su cliente con la información que este le pide, también llevan únicamente el marcador ACK y miden sobre 1500 bytes; pero en el caso de un ordenador doméstico sin servicios al exterior siempre son de entrada y no de salida. Por tanto, nos podemos ahorrar comprobar la longitud. Ahora bien, en nuestro caso hemos dispuesto varios servidores que envían información al cliente externo con paquetes ACK grandes. Estos no queremos de ninguna manera que vayan por la clase 1:20, porque esta está destinada exclusivamente a los paquetes oye-esto-va-bien. Nótese que en esta clase hemos incluido también los paquetes que inician conexión.

Tembién requiere un poco de atención el filtro que identifica al tráfico ssh y ftp interactivo. Además del puerto de salida, se comprueba que el byte ToS (aplicando una máscara 0x1c) valga 16 (o 0x10 en hexadecimal). Recuerde la explicación a ello.

En cuanto a las planificaciones finales, se ha usado PRIO sin parámetros adicionales (es decir, una PFIFO_FAST) para las clases en las que hay tráfico de distinta prioridad, SFQ en aquellas en las que haya diversos tráficos y se quiera equilibrar el reparto del ancho entre ellos, y PFIFO en las restantes. Obsérvese que las planificiones 2000:, 3000: y 4000: son algo discutibles. Sin embargo, en el caso particular que se trata nuestra intención es montar un servidor DNS con lo que presumiblemente los clientes internos harán sus consultan a él. Por tanto, todo el tráfico DNS de consulta lo originará nuestro servidor e irá dirigido al forwarder: no habrá por tanto distintos tráficos. Las consultas externas a nuestro servidor sí se se podrán recibir desde muchos clientes distintos, pero al no tener ser el nuestro un servidor de gran tráfico, no serán consultas muy comunes y raramente simultáneas. Sobre las otras dos planificaciones en disputa se puede hacer idéntico razonamiento: rara vez se darán conexiones simultáneas. Tan sólo el servidor web podría tener algo de trabajo simultáneo y poder justificar una planificación SFQ.

Con penalización

En este caso el diseño es un poco más complejo:

Esquema del control del tráfico de salida

Los principios son los mismos que en el caso anterior, pero con el fin de penalizar clientes se define una planificación PRIO con dos bandas para la clase 1:10; de manera que a la de menor prioridad vaya el tráfico procedente de los clientes penalizados y a el resto a la otra. Este último tráfico, posteriormente, se clasifica según los mismos criterios que en el primer diseño. Opcionalmente se puede dejar que un poco de tráfico de la subred penalizada, vaya a la clase banda prioritaria y compita en igualdad con el resto del tráfico.

Téngase en cuenta, si se utiliza este diseño, que a estas alturas del flujo de paquetes por el núcleo ya ha actuado iptables (véase el flujo de paquetes a través del núcleo), de modo que si se hace un enmascaramiento (cosa bastante habitual) la ip de origen de los paquetes ya habrá sido sustituida por la de la interfaz eth0. Así pues, en este punto es imposible distinguir qué paquetes proceden de la subred penalizada y cuáles no. Para soslayar este problema, hay dos soluciones:

  1. Marcar los paquetes con iptables antes de enmascararlos para poder identificarlos.
  2. Dotar a la interfaz eth0 de dos direcciones de red: con la principal (p.e. 172.22.0.2/16 se enmascarará todo el tráfico, excepto el proviniente de la subred limitada que se enmascarará con la ip secundaria (p.e. 172.22.255.2). Esto de proporcionar dos direcciones distintas a una misma dirección ip es lo que se denomina ip aliasing.

Ambas soluciones nos permiten diseñar la salida, pero sólo la segunda tambíen la entrada, en la que nos encontraremos con una situación simétrica: controlamos los paquetes antes de se traten con iptables y, por tanto, de que se deshaga el enmascaramiento. Así, pues, no es posible comprobar si la dirección de destino pertenece a la subred penalizada, porque ésta será aún la de la interfaz eth0. Sin embargo, si hemos hecho ip aliasing, sí podremos basarnos en el destino para distinguir paquetes: aquellos dirigidos a la ip adicional van dirigidos a la subred.

Así pues, haremos ip aliasing y enmascararemos con la segunda dirección la subred de marras, como paso previo:

# ip addr add 172.22.255.2/24 dev eth0
# iptables -t nat -I POSTROUTING -o eth0 -s 192.168.255.64/27 -j SNAT --to-source 172.22.255.2

Comencemos por las clases y disciplinas intermedias, que se complican algo:

# tc qdisc add dev eth0 root handle 1: htb default 100
# tc class add dev eth0 parent 1:0 classid 1:1 htb rate 1gbit

# tc class add dev eth0 parent 1:1 classid 1:100 htb rate 460kbit
# tc class add dev eth0 parent 1:1 classid 1:200 htb rate 999i540kbit ceil 1gbit

# tc qdisc add dev eth0 parent 1:100 handle 100: prio bands 2 priomap 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

# tc qdisc add dev eth0 parent 100:1 handle 1001: htb default 50
# tc class add dev eth0 parent 1001: classid 1001:100 htb rate 512kbit

# tc class add dev eth0 parent 1001:100 classid 1001:10 htb rate 230kbit ceil 400kbit burst 32k prio 5
# tc class add dev eth0 parent 1001:100 classid 1001:20 htb rate  43kbit ceil 230kbit burst 32k prio 3
# tc class add dev eth0 parent 1001:100 classid 1001:30 htb rate  43kbit ceil 460kbit burst 64k prio 1
# tc class add dev eth0 parent 1001:100 classid 1001:40 htb rate  86kbit ceil 460kbit burst 64k prio 4
# tc class add dev eth0 parent 1001:100 classid 1001:50 htb rate  58kbit ceil 460kbit burst 32k prio 2

Las planificaciones finales quedan del siguiente modo:

# tc qdisc add dev eth0 parent 1001:10 handle 1000: prio
# tc qdisc add dev eth0 parent 1001:20 handle 2000: bfifo limit 17200b
# tc qdisc add dev eth0 parent 1001:30 handle 3000: pfifo limit 32
# tc qdisc add dev eth0 parent 1001:40 handle 4000: pfifo limit 32
# tc qdisc add dev eth0 parent 1001:50 handle 5000: sfq perturb 4 limit 32
# tc qdisc add dev eth0 parent 100:2   handle 1002: prio
# tc qdisc add dev eth0 parent 1:200   handle 6000: prio

Quedan, por último, los filtros. En realidad, son los mismos que para el caso anterior (salvando la diferencia en los handle, excepto el nuevo que aparece para separar el tráfico en la planificiación 100:. Callemos este último, por ahora, y desglosemos el resto:

# tc filter add dev eth0 parent 1:0 protocol ip u32 match ip dst 172.22.0.0/16 flowid 1:20

# tc filter add dev eth0 parent 1001: protocol ip basic match "u32(u8 6 0xff at 9) and \
                                                               (cmp(u8 at 13 layer transport eq 0x10) or \
                                                                cmp(u8 at 13 layer transport eq 0x2)) and \
                                                               cmp(u16 at 2 layer network lt 100)"\
                                               flowid 1001:10
# tc filter add dev eth0 parent 1001: protocol ip basic match "u32(u8 17 0xff at 9) and \
                                                               (cmp(u16 at 2 layer transport eq 53) or\
                                                                cmp(u16 at 0 layer transport eq 53))" \
                                               flowid 1001:20
# tc filter add dev eth0 parent 1001: protocol ip basic match "(u32(u8 6 0xff at 9) and \
                                                                cmp(u8 at 1 layer network mask 0x1c eq 0x10) and \
                                                               (cmp(u16 at 0 layer transport eq 22) or \
                                                                cmp(u16 at 0 layer transport eq 21))) or \
                                                               (u32(u8 17 0xff at 9) and \
                                                                cmp(u16 at 0 layer transport eq 1194))" \
                                               flowid 1001:30
# tc filter add dev eth0 parent 1001: protocol ip basic match "u32(u8 6 0xff at 9) and\
                                                               (cmp(u16 at 0 layer transport eq 80) or\
                                                                cmp(u16 at 0 layer transport eq 443))" \
                                               flowid 1001:40

Obviamente, son los mismos exceptuando el hecho que los filtros que dividen el tráfico a internet se aplican sobre la planificación 1001:. Sin embargo, queda el filtro que segrega el tráfico de la subred penalizada hacia la banda 100:2. Puede ser este:

# tc filter add dev eth0 parent 100: protocol ip prio 1 u32 match ip src 172.22.255.2/32 flowid 100:1 \
                                     action police rate 32kbit burst 64k mtu 64k continue
# tc filter add dev eth0 parent 100: protocol ip prio 2 u32 match ip src 172.22.255.2/32 flowid 100:2

Con los filtros se pretende que, en situación de competencia, la red penalizada nunca ocupe más de un 10% del ancho de bajada. Por este motivo (y asumiendo que prácticamente todo el tráfico de subida de estos clientes es de confirmación) se permite que 32 Kbit/s se traten como tráfico no penalizado. El número se obtiene de aplicar el 5% al 10% del tráfico de bajada y redondear hacia arriba un poco. Si no se quiere tener estos miramientos, se puede eliminar el primer filtro.

Control del tráfico entrante

El control de tráfico más eficiente que puede hacerse para gestionar la entrada, es haber configurado eficientemente la salida; y este es el que se sugiere. Existen, no obstante, mecanismos para realizar planificaciones a la entrada semejantes a la de la salida. Se indicará cómo hacerlos más adelante.

Limitación de la entrada

Lo recomendable a la entrada es, simplemente, limitar la bajada para impedir que se sature el enlace y se disparen las latencias. Para ello podemos hacer lo siguiente:

# tc qdisc add dev eth0 handle ffff: ingress
# tc filter add dev eth0 parent ffff: protocol ip prio 1 u32 match ip src 172.22.0.0/16 flowid :1
# tc filter add dev eth0 parent ffff: protocol ip prio 2 u32 match u32 0 0 \
                                      action police rate 5100kbit burst 64k mtu 64k drop \
                                      flowid :1

aunque habrá que ajustar rate (siempre deberá estar por debajo 5400) y hacer pruebas para comprobar cómo se comporta la latencia. Por ejemplo, podemos poner a descargar un fichero que ocupe todo el ancho de banda y efectuar algunas pruebas de ping.

Planificación de la entrada

Hay varias soluciones para lograr crear planificaciones del tráfico de entrada:

  1. Si limitamos el tráfico a la salida en la interfaz interna eth1 que conecta con la subred penalizada, soslayamos el problema de vérnoslas con la planificación de entrada y, aunque hayamos hecho enmascaramiento, los paquetes ya dispondrán de su ip de destino definitiva, por tanto será muy fácil establecer la limitación. Sin embargo, es una solución muy deficiente si eth1 no es la única interfaz interna, puesto que por ella no pasará todo el tráfico que entró por eth0. Si, además, disponemos varios servicios en nuestra máquina, ni siquiera sería solución recomendable si fuera la única interfaz interna. En estas condiciones, lo único que podríamos hacer es limitar el ancho de banda para la subred penalizada (con un filtro, por ejemplo), pero esto precisamente también lo podríamos hacer con la planificación a la entrada de eth0. Así pues, desechamos esta estrategia
  2. Si hacemos el ip aliasing como ya propusimos, podemos usar una interfaz virtual ifb para redirigir a su salida el tráfico que entra por eth0. De este modo, hemos convertido la entrada en una salida y podremos aplicar planificaciones de salida.
  3. Usar una interfaz virtual imq a cuya salida se redirige el tráfico, cuando es tratado por netfilter. Tiene la ventaja sobre la interfaz cirtual ifb que para entonces ya se ha deshecho el enmascaramiento (no se necesita, por tanto, ip aliasing) y que se puede identificar y marcar el tráfico con iptables. En cambio, exige parchear y recompilar el kernel, así que desecharemos esta vía.

Desarrollemos la segunda de las soluciones. Hecho el ip aliasing, debemos crear una interfaz virtual ifb lo cual exige cargar el módulo:

# modprobe ifb numifbs=1
# ip link set ifb0 up

A continuación mandamos todo el tráfico entrante procedente de internet (o sea, el que no provenga de 172.22.0.0/16) hacia esta interfaz:

# tc qdisc add dev eth0 handle ffff: ingress
# tc filter add dev eth0 parent ffff: protocol ip basic match "not u32(u32 0xac160000 0xffff0000 at 12)"
                                      action mirred egress redirect dev ifb0

Hecho esto, volvemos a encontrarnos ante un caso de control sobre el tráfico de salida y podemos aplicar todo lo aprendido para gestionar el tráfico de internet. Podemos, por ejemplo, definir una planificación PRIO igual a la 100: que ideamos anteriormente en la que el tráfico menos prioritario es el destino a la subred penalizada:

Planificación para la entrada

Excepcionalmente hemos dado el handle 100: a la planificación root y no el 1: con el propósito de que los identificadores de planificaciones y clases sean lo más parecidos al diseño de la planificación de salida. Para no ser muy prolijos, si en vez de crear una planificación HTB para la banda 100:1, creamos una planificación PRIO que emula una PRIO_FAST, los comandos quedan así:

# tc qdisc add dev ifb0 root handle 100: prio bands 2 priomap 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

# tc qdisc add dev ifb0 parent 100:1 1001: prio
# tc qdisc add dev ifb0 parent 100:2 1002: prio

Y el filtro para desviar el tráfico penalizado hacia 1:2, es igual al ya definido en la entrada, salvo por el hecho de que ahora la ip es la de destino y no la de origen:

# tc filter add dev ifb0 parent 100: protocol ip prio 1 u32 match ip dst 172.22.255.2/32 flowid 100:1 \
                                   action police rate 51kbit burst 64k mtu 64k continue
# tc filter add dev ifb0 parent 100: protocol ip prio 2 u32 match ip dst 172.22.255.2/32 flowid 100:2

Con esto, podría valer. Ahora bien, desarrollar el diseño dibujado en el gráfico exige unos cuantos comandos más.

Script de configuración

Para poner todo junto y automatizado se ha escrito el siguiente script:

Script de configuración del control de tráfico

Para ver cómo funciona no hay más que pedir la prolija ayuda:

# qos.sh -h

En principio, los controles de salida y entrada que implementa son muy simples: los que se han discutido, pero sin distinguir en el tráfico no penalizado, de manera que en vez de generarse las clases 1:10, 1:20, etc. hay una planificación PRIO idéntica a la PFIFO_FAST.

Es posible escribir módulos que implementen cualquier otra política más complicada. El módulo m1 define exactamente las discutidas en el tema y puede servir de modelo para escribir otras:

Módulo para el script que implementa el control de tráfico discutido

El módulo habrá que dejarlo en el directorio que indique la variable MODULESDIR del script. Como ejemplo, para implementar los control de salida y entrada expuestos en el tema, basta con lo siguiente:

# qos.sh -o eth0 -u 512 -d 6000 -L 10 -2 172.22.255.2/16 -l 192.168.255.64/26 start

El script no se encarga en absoluto de cargar el módulo ifb para que haya disponibles interfaces ifb: esto deberá hacerse a mano o integrarse en algún script de arranque.

Apéndice: Vademécum sobre iproute2

Modificación del nombre de una interfaz
# ip link set dummy0 name eth10
Consulta del estado de las interfaces

Todas:

$ ip link show

Una en particular (p.e. eth0):

$ ip link show eth0
Consulta de la configuración de las interfaces

Todas:

$ ip addr show

Una en particular (p.e. eth0):

$ ip addr show eth0
Activación de una interfaz

Activación:

# ip link set eth0 up

Desactivación:

# ip link set eth0 down
Configuración de una interfaz
# ip add add 192.168.0.2/24 dev eth0
Consulta de la tabla ARP

Todas las entradas:

$ ip neigh show

Una entrada en particular:

$ ip neigh show 192.168.0.1
Adición de entradas ARP

Adición:

# ip neigh add 192.168.1.7 lladdr 00:11:22:33:44:55 dev eth0

Eliminación:

ip neigh del 192.168.1.7 dev eth0
Modificación de la dirección MAC

Antes es necesario desactivar la interfaz.

# ip link set eth10 addr 00:11:22:33:44:55
Consulta de rutas

Consulta de rutas:

# ip route show

Consulta de rutas de cualquier tabla (p.e. ADSL):

# ip route show table ADSL
Adición de rutas

Adición de la puerta de enlace predeterminada:

# ip route add default via 192.168.10.1

Eliminación de la puerta de enlace:

# ip route del default

Eliminación de la entrada para una red particular:

# ip route del 10.53.0.0/27
Creación de puentes

Creación de una interfaz bridge (p.e. br0):

# ip link add br0 type bridge

Eliminación de una interfaz:

# ip link del br0

Adición de un puerto (p.e. eth0):

# ip link set eth0 master br0

Remoción de un puerto (p.e. eth0):

# ip link set eth0 nomaster
Creación de subinterfaces VLAN:

Creación de subinterfaz vlan:

# ip link add link eth0 name eth0.100 type vlan id 100

Eliminación de la subinterfaz:

# ip link del eth0.100

Bibliografía

  1. Chapter 5. Network setup: Capítulo 5 del manual oficial de debian, que trata sobre la configuración de red.
  2. iproute2 cheat sheet: Recopilación de comandos que pueden hacerse con iproute2.
  3. Comparison of BRTCL and BRIDGE commands.
  4. Virtual switching technologies and Linux bridge: Interesante exposición (transparencias) sobre switching con linux.
  5. Linux Advanced Routing & Traffic Control Howto: El manual de referencia sobre encaminamiento avanzado y control de tráfico con linux. GULIC produjo una traducción de este documento del que se ha procurado conservar una copia.
  6. Dual Uplink BCP
  7. Traffic Control, Shaping and QoS: ilustrativo artículo en inglés sobre control de tráfico y calidad de servicio.
  8. Traffic Control HOWTO: Manual sobre control de tráfico con linux.
  9. Notes on the Linux Traffic Control Engine: Estas notas contienen información muy detallada sobre algunos aspectos de tc (p.e. sobre el filtro u32).
  10. Traffic Control: Algunos apuntes muy en borrador sobre tc. Está desglosada la forma de pedir ayuda a tc sobre las distintas planificaciones y filtros.
  11. Understanding TCP Sequence abd Acknowledgment Numbers: Interesante documento que destripa una comunicación TCP.