tc (traffic control) es la herramienta del paquete iproute2 que configura cómo el kernel Linux encola y envía los paquetes de cada interfaz. Con ella puedes limitar la velocidad de subida de un servidor, reservar ancho de banda para un servicio, dar prioridad a SSH frente a descargas masivas o limitar también el tráfico de entrada. En este tutorial medirás el ancho de banda actual, aplicarás un límite sencillo con TBF, crearás un reparto por clases con HTB, limitarás la bajada con IFB y harás todo persistente con systemd en Ubuntu 24.04.

Requisitos previos

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath. tc funciona igual en Debian 12 y Rocky Linux 9.
  • Un usuario no root con privilegios sudo.
  • Acceso a la consola del servidor: un error con tc en la interfaz principal puede cortar tu sesión SSH.
  • Una segunda máquina con iperf3 para medir (otro servidor o tu equipo), con el puerto TCP 5201 accesible.

Conceptos que usarás:

TérminoQué es
qdiscDisciplina de cola: el algoritmo que decide cómo y cuándo salen los paquetes de una interfaz.
classSubdivisión de una qdisc con clases (como HTB), cada una con su propio ancho de banda.
filterRegla que decide a qué clase va cada paquete.
handleIdentificador mayor:menor, por ejemplo 1: para la qdisc raíz y 1:10 para una clase.

Un punto clave: tc solo puede dar forma al tráfico que sale de una interfaz (egress). El tráfico de entrada ya ha llegado cuando el kernel lo ve, así que para limitar la bajada hay que redirigirlo a una interfaz virtual (paso 5).

Paso 1: Identificar la interfaz y medir la línea base

Averigua qué interfaz usa el servidor para salir a Internet:

ip route get 1.1.1.1
1.1.1.1 via 203.0.113.1 dev eth0 src 203.0.113.10 uid 1000

La interfaz es la que aparece tras dev, aquí eth0. En tu servidor puede llamarse ens18, enp1s0 u otro nombre; sustitúyelo en todos los comandos.

Consulta la qdisc actual:

tc qdisc show dev eth0
qdisc fq_codel 0: root refcnt 2 limit 10240p flows 1024 quantum 1514 target 5ms interval 100ms memory_limit 32Mb ecn drop_batch 64

fq_codel es la qdisc por defecto en Ubuntu. Es la que volverá a aplicarse cuando borres tu configuración.

Instala iperf3 en el servidor y en la máquina de pruebas:

sudo apt update
sudo apt install iperf3

Si el instalador pregunta si quieres arrancar iperf3 como servicio, responde No. En la máquina de pruebas, deja iperf3 escuchando:

iperf3 -s

Desde el servidor, mide la subida (el servidor envía) durante 10 segundos:

iperf3 -c test_host_ip -t 10
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-10.00  sec  1.09 GBytes   938 Mbits/sec   12            sender
[  5]   0.00-10.00  sec  1.09 GBytes   935 Mbits/sec                  receiver

Y la bajada (el servidor recibe) con -R:

iperf3 -c test_host_ip -t 10 -R

Apunta ambos valores para comparar después.

Paso 2: Limitar la subida con TBF

TBF (Token Bucket Filter) es la forma más sencilla de poner un techo a toda la interfaz. Aplica un límite de 100 Mbit/s a la salida:

sudo tc qdisc replace dev eth0 root tbf rate 100mbit burst 128kb latency 50ms
  • rate 100mbit: velocidad máxima sostenida. En tc, mbit son megabits por segundo; mbps serían megabytes, un error común.
  • burst 128kb: cuántos bytes pueden salir de golpe. Si es demasiado pequeño, no se alcanza el rate configurado.
  • latency 50ms: cuánto puede esperar un paquete en la cola antes de descartarse.
  • replace sustituye la qdisc raíz actual, o la crea si no existe.

Comprueba que está activa:

tc -s qdisc show dev eth0
qdisc tbf 8001: root refcnt 2 rate 100Mbit burst 128Kb lat 50ms
 Sent 185216 bytes 1207 pkt (dropped 0, overlimits 0 requeues 0)
 backlog 0b 0p requeues 0

Repite la medición de subida:

iperf3 -c test_host_ip -t 10
[  5]   0.00-10.00  sec   114 MBytes  95.6 Mbits/sec   35            sender

El resultado queda algo por debajo de 100 Mbit/s porque tc cuenta también las cabeceras. Para quitar el límite y volver a la qdisc por defecto:

sudo tc qdisc del dev eth0 root

Paso 3: Repartir el ancho de banda con HTB

TBF limita todo por igual. Si quieres que SSH siempre responda aunque haya una copia masiva en curso, o reservar una parte para el servidor web, necesitas HTB (Hierarchical Token Bucket), que divide el ancho de banda en clases con un mínimo garantizado (rate) y un máximo que pueden tomar prestado (ceil).

El reparto de ejemplo sobre 100 Mbit/s:

ClaseTráficoGarantizadoMáximoPrioridad
1:10SSH5 Mbit/s100 Mbit/s0 (más alta)
1:20Web (80, 443)60 Mbit/s100 Mbit/s1
1:30Resto35 Mbit/s100 Mbit/s2

Crea la qdisc raíz HTB, indicando que el tráfico no clasificado vaya a la clase 1:30:

sudo tc qdisc replace dev eth0 root handle 1: htb default 30

Crea una clase padre con el total y las tres clases hijas:

sudo tc class add dev eth0 parent 1: classid 1:1 htb rate 100mbit ceil 100mbit
sudo tc class add dev eth0 parent 1:1 classid 1:10 htb rate 5mbit ceil 100mbit prio 0
sudo tc class add dev eth0 parent 1:1 classid 1:20 htb rate 60mbit ceil 100mbit prio 1
sudo tc class add dev eth0 parent 1:1 classid 1:30 htb rate 35mbit ceil 100mbit prio 2

Añade una qdisc fq_codel dentro de cada clase. Así cada clase reparte su parte de forma justa entre conexiones y mantiene baja la latencia:

sudo tc qdisc add dev eth0 parent 1:10 fq_codel
sudo tc qdisc add dev eth0 parent 1:20 fq_codel
sudo tc qdisc add dev eth0 parent 1:30 fq_codel

Por último, los filtros que envían cada tipo de tráfico a su clase. Como se trata de tráfico de salida, se filtra por puerto de origen: son las respuestas del servidor SSH y del servidor web:

sudo tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip sport 22 0xffff flowid 1:10
sudo tc filter add dev eth0 parent 1: protocol ip prio 2 u32 match ip sport 80 0xffff flowid 1:20
sudo tc filter add dev eth0 parent 1: protocol ip prio 2 u32 match ip sport 443 0xffff flowid 1:20

Estos filtros u32 solo clasifican IPv4. El tráfico IPv6 irá a la clase por defecto 1:30, lo cual funciona pero sin prioridad.

Comprueba las clases y cuánto tráfico pasa por cada una:

tc -s class show dev eth0
class htb 1:10 parent 1:1 leaf 8002: prio 0 rate 5Mbit ceil 100Mbit burst 1600b cburst 1600b
 Sent 48213 bytes 402 pkt (dropped 0, overlimits 0 requeues 0)
class htb 1:20 parent 1:1 leaf 8003: prio 1 rate 60Mbit ceil 100Mbit burst 1590b cburst 1600b
 Sent 0 bytes 0 pkt (dropped 0, overlimits 0 requeues 0)
class htb 1:30 parent 1:1 leaf 8004: prio 2 rate 35Mbit ceil 100Mbit burst 1593b cburst 1600b
 Sent 912845 bytes 6120 pkt (dropped 0, overlimits 0 requeues 0)

Los contadores Sent de 1:10 suben mientras usas SSH, lo que confirma que el filtro funciona. Una prueba con iperf3 (puerto 5201) caerá en 1:30 y podrá usar hasta 100 Mbit/s mientras las otras clases estén libres.

Paso 4: Limitar el tráfico de una IP de destino

Otra necesidad habitual es limitar solo lo que el servidor envía a un destino concreto, por ejemplo una réplica de copias de seguridad en 198.51.100.50, para que no sature el enlace. Sobre la configuración HTB anterior, añade una clase limitada a 20 Mbit/s sin posibilidad de tomar prestado:

sudo tc class add dev eth0 parent 1:1 classid 1:40 htb rate 20mbit ceil 20mbit prio 3
sudo tc qdisc add dev eth0 parent 1:40 fq_codel
sudo tc filter add dev eth0 parent 1: protocol ip prio 3 u32 match ip dst 198.51.100.50/32 flowid 1:40

Verifica que el tráfico hacia esa IP pasa por 1:40:

tc -s class show dev eth0 classid 1:40

Paso 5: Limitar la bajada con IFB

Para limitar el tráfico de entrada se redirige a una interfaz virtual IFB (Intermediate Functional Block). Allí el tráfico se trata como salida y se le puede aplicar cualquier qdisc.

Carga el módulo y levanta la interfaz ifb0:

sudo modprobe ifb numifbs=1
sudo ip link set dev ifb0 up

Añade una qdisc de entrada a eth0 y redirige todo lo que llega a ifb0:

sudo tc qdisc add dev eth0 handle ffff: ingress
sudo tc filter add dev eth0 parent ffff: matchall action mirred egress redirect dev ifb0

Limita ifb0 a 50 Mbit/s:

sudo tc qdisc replace dev ifb0 root tbf rate 50mbit burst 128kb latency 50ms

Mide la bajada desde el servidor:

iperf3 -c test_host_ip -t 10 -R
[  5]   0.00-10.00  sec  56.9 MBytes  47.7 Mbits/sec                  receiver

Para deshacer la limitación de bajada:

sudo tc qdisc del dev eth0 ingress
sudo tc qdisc del dev ifb0 root

Paso 6 (opcional): Simular una red lenta con netem

netem añade retardo, variación y pérdida de paquetes artificiales. Es útil para probar cómo se comporta una aplicación con una conexión mala. Aplícalo a ifb0 o a una interfaz de pruebas mejor que a la principal, porque afectará también a tu sesión SSH. Por ejemplo, 100 ms de retardo con 20 ms de variación y un 1 % de pérdida:

sudo tc qdisc replace dev ifb0 root netem delay 100ms 20ms loss 1%

Con la redirección del paso 5 activa, ping -c 5 1.1.1.1 mostrará unos 100 ms más de latencia. Quítalo con:

sudo tc qdisc del dev ifb0 root

Paso 7: Hacer la configuración persistente

Las reglas de tc se pierden al reiniciar. Guarda la configuración en un script y ejecútalo con un servicio de systemd. Crea el script:

sudo nano /usr/local/sbin/tc-shaping
#!/usr/bin/env bash
set -euo pipefail

DEV="eth0"
TOTAL="100mbit"
DOWN="50mbit"

stop() {
    tc qdisc del dev "$DEV" root 2>/dev/null || true
    tc qdisc del dev "$DEV" ingress 2>/dev/null || true
    if ip link show ifb0 >/dev/null 2>&1; then
        tc qdisc del dev ifb0 root 2>/dev/null || true
    fi
}

start() {
    stop

    # Subida: HTB con prioridad para SSH y web
    tc qdisc add dev "$DEV" root handle 1: htb default 30
    tc class add dev "$DEV" parent 1: classid 1:1 htb rate "$TOTAL" ceil "$TOTAL"
    tc class add dev "$DEV" parent 1:1 classid 1:10 htb rate 5mbit ceil "$TOTAL" prio 0
    tc class add dev "$DEV" parent 1:1 classid 1:20 htb rate 60mbit ceil "$TOTAL" prio 1
    tc class add dev "$DEV" parent 1:1 classid 1:30 htb rate 35mbit ceil "$TOTAL" prio 2
    for class in 10 20 30; do
        tc qdisc add dev "$DEV" parent "1:$class" fq_codel
    done
    tc filter add dev "$DEV" parent 1: protocol ip prio 1 u32 match ip sport 22 0xffff flowid 1:10
    tc filter add dev "$DEV" parent 1: protocol ip prio 2 u32 match ip sport 80 0xffff flowid 1:20
    tc filter add dev "$DEV" parent 1: protocol ip prio 2 u32 match ip sport 443 0xffff flowid 1:20

    # Bajada: redirección a ifb0 con TBF
    modprobe ifb numifbs=1
    ip link set dev ifb0 up
    tc qdisc add dev "$DEV" handle ffff: ingress
    tc filter add dev "$DEV" parent ffff: matchall action mirred egress redirect dev ifb0
    tc qdisc add dev ifb0 root tbf rate "$DOWN" burst 128kb latency 50ms
}

case "${1:-}" in
    start) start ;;
    stop) stop ;;
    *) echo "Uso: $0 {start|stop}" >&2; exit 1 ;;
esac

Ajusta DEV, TOTAL y DOWN a tu caso y hazlo ejecutable:

sudo chmod 755 /usr/local/sbin/tc-shaping

Crea la unidad de systemd:

sudo nano /etc/systemd/system/tc-shaping.service
[Unit]
Description=Limitación de ancho de banda con tc
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/tc-shaping start
ExecStop=/usr/local/sbin/tc-shaping stop

[Install]
WantedBy=multi-user.target

Activa el servicio y comprueba que aplica las reglas:

sudo systemctl daemon-reload
sudo systemctl enable --now tc-shaping.service
systemctl status tc-shaping.service --no-pager
● tc-shaping.service - Limitación de ancho de banda con tc
     Loaded: loaded (/etc/systemd/system/tc-shaping.service; enabled; preset: enabled)
     Active: active (exited) since Thu 2026-09-24 12:20:41 UTC; 3s ago
tc qdisc show dev eth0
qdisc htb 1: root refcnt 2 r2q 10 default 0x30 direct_packets_stat 0 direct_qlen 1000
qdisc fq_codel 8005: parent 1:10 limit 10240p flows 1024 quantum 1514 target 5ms interval 100ms memory_limit 32Mb ecn drop_batch 64
...
qdisc ingress ffff: parent ffff:fff1 ----------------

Para desactivar la limitación temporalmente usa sudo systemctl stop tc-shaping, y para que no se aplique en el arranque, sudo systemctl disable tc-shaping.

Solución de problemas

Error: Exclusivity flag on, cannot modify.: ya existe una qdisc en esa posición. Usa tc qdisc replace en lugar de add, o bórrala antes con tc qdisc del dev eth0 root.

RTNETLINK answers: File exists al añadir una clase o filtro: ese classid ya existe. Ejecuta sudo /usr/local/sbin/tc-shaping stop para empezar de cero.

modprobe: FATAL: Module ifb not found: el kernel instalado no incluye el módulo en el paquete base. Instálalo con sudo apt install linux-modules-extra-$(uname -r) y repite el paso 5.

No se alcanza la velocidad configurada: el burst es demasiado pequeño para ese rate. Súbelo (por ejemplo a 256kb para varios cientos de Mbit/s) y vuelve a medir.

Todo el tráfico cae en la clase por defecto: revisa los filtros con tc filter show dev eth0 y recuerda que en salida se filtra por puerto de origen para servicios que el servidor ofrece, y por puerto de destino para conexiones que el servidor inicia.

Conclusión

Has medido el ancho de banda con iperf3, has aplicado un límite global con TBF, has repartido la subida por clases con HTB dando prioridad a SSH, has limitado la bajada mediante IFB y has hecho la configuración persistente con un servicio de systemd. tc -s class show te permite comprobar en cualquier momento cuánto tráfico pasa por cada clase.

Como siguientes pasos puedes:

  • Marcar paquetes con nftables (meta mark set) y clasificarlos en tc con filtros fw, lo que permite reglas más ricas y también para IPv6.
  • Probar la qdisc cake, que combina limitación y reparto justo en una sola línea.
  • Registrar la evolución del tráfico por clase con tu sistema de monitorización para ajustar los valores rate y ceil.