Router con QoS hecho con una Raspberry Pi y un modem USB


A continuación se presenta un ejemplo de configuración en el que se usa una Raspberry Pi para crear un router 3G/4G. Además de presentar la configuración se incluye una configuración avanzada para dotarlo de QoS y limitar el tráfico de los dispositivos conectados.

Instalación básica

En el proceso mostrado se parte de una imagen básica arm64 descargada desde https://downloads.raspberrypi.org/. Recomiendo usar la imagen de Debian mínima (lite) en una Raspberry Pi 4, aunque en una Pi 3 todo funciona de forma similar.

Para no necesitar teclado y pantalla se pueden realizar las siguientes modificaciones en la tarjeta SD antes de ponerla en la Raspberry Pi. Se monta la partición boot y se hacen los siguientes cambios:

  1. Para conexión directa con un cable de red, editar cmdline.txt y añadir después de rootwait ip=192.168.99.99 y en el otro extremo una IP de la misma red.
  2. Para activar el servidor SSH: crear un fichero ssh en /boot con touch ssh.

Recuerda que la partición boot es la partición VFAT de la imagen descargada.

Conexión via modem 3G/4G

En las pruebas que he realizado en los últimos años me han funcionado prácticamente todos los modem USB que he comprado. Tanto de marcas conocidas como algunos chinos. Con algunos de ellos puedes tener problemas de desconexión pero suele ser porque se sobrecalienta y se apaga, asegúrate que esté todo bien ventilado.

La configuración propuesta para el modem USB opere es la siguiente:

  1. Permitir extraer o poner el modem con la Raspberry Pi funcionando.
  2. Cambiar el gestor de red por defecto a NetworkManager.

Tras el primer arranque con la imagen Debian lite hay que entrar y cambiar el gestor de red por NetworkManager mediante:

apt install network-manager
apt purge openresolv dhcpcd5
sytemctl enable NetworkManager

Tras un reinicio la red estará funcionando con NetworkManager y se puede comprobar ejecutando nmcli. En este momento se conecta el modem USB y se comprueba si lo ha reconocido mediante

$ ip a

4: wwan0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 5a:e5:3c:15:8a:00 brd ff:ff:ff:ff:ff:ff

En este caso aparece la interfaz wwan0 y mediante el modem-manager mmcli se puede comprobar el model:

$ mmcli -L

/org/freedesktop/ModemManager1/Modem/0 [QUALCOMM INCORPORATED] 0

Es importante el número de modem, que es 0, para configurar el APN. En el ejemplo mostrado se usa una tarjeta de simyo, a la que previamente se le ha desactivado el PIN. En caso de otro operador, solo hay que introducir los datos correctos:

$ nmcli c add con-name "simyo" type gsm ifname "*" apn "orangeworld"
$ nmcli c mod simyo gsm.username ""
$ nmcli c mod simyo gsm.password ""

En el caso de simyo no hay usuario y contraseña. Aunque existe un asistente en modo texto para network-manager: nmtui-edit esta configuración no se puede hacer con este asistente. En cambio, sí se puede ver con nmtui-edit la conexión y activarla con el comando nmtui-connect.

Llegado a este punto se va a configurar para que la conexión se reconecte siempre de forma ininterrumpida. Por defecto el número de reintentos de conexión es 3, se puede ver con nmcli, siendo la referencia de estos parámetros estos: nm-settings-nmcli

$ nmcli conn show simyo

connection.id:                    simyo
connection.uuid:                  d1228d30-aad4-4966-b606-8f2270f7d3ff
connection.stable-id:             --
connection.type:                  gsm
connection.interface-name:        --
connection.autoconnect:           sí
connection.autoconnect-priority:  0
connection.autoconnect-retries:  -1 (default)
connection.multi-connect:         0 (default)
connection.auth-retries:         -1
connection.timestamp:            1576525195
connection.read-only:            no
connection.permissions:          --
connection.zone:                 --
connection.master:               --
connection.slave-type:           --
connection.autoconnect-slaves:   -1 (default)
connection.secondaries:          --
connection.gateway-ping-timeout:  0
connection.metered:              desconocido
connection.lldp:                 default
connection.mdns:                 -1 (default)
connection.llmnr:                -1 (default)

El problema está con la opción connection.auth-retries: -1 , donde -1 significa 3 veces, hay que ponerlo a 0 para que sea infinito. También es necesario cambiar autoconnect-retries -1 ya que -1 significa 4 veces y también hay que ponerla a cero:

nmcli c modify simyo connection.autoconnect-retries 0
nmcli c modify simyo connection.auth-retries 0

Para finalizar hay que activar la conexión y ponerla en modo automático mediante:

nmcli c mod simyo connection.autoconnect yes
nmcli c up simyo

Crear el punto de acceso y compartir Internet

Sobre esto hay multitud de información en la red, habitualmente usando: hostapd, dnsmasq e iptables. Aquí se va a realizar de diferente modo:

  • El punto de acceso se creará con NetworkManager
  • El firewall con nftables
  • El servidor de nombre y dhcp sí se realizará con dnsmasq

El punto de acceso Wifi se configura mediante nmtui-edit añadiendo una nueva conexión de tipo WiFi con las opciones:

Configuración AP

En la configuración de la captura anterior hay que asegurarse de:

  • Indicar el dispositivo wlan0
  • Establecer el modo en "Punto de acceso"
  • Establecer la configuración IPv4 en manual y asignar únicamente una IP

Con esto se activa sólo el punto de acceso pero queda por configurar el sistema operativo para que reenvíe paquetes

Activando el reenvío (forwarding)

Esto es lo que siempre se olvida activar, editar fichero /etc/sysctl.conf y descomentar la línea:

net.ipv4.ip_forward=1

para activarlo y comprobar que está correcto ejecutar el comando indicado y comprobar la salida

$ sysctl -p

net.ipv4.ip_forward = 1

Ahora dnsmasq (dhcp server + dns cache)

dnsmasq es un servidor DHCP y DNS caché muy usado, existiendo muchas páginas con información para su configuración. Recomiendo una configuración básica siguiendo las instrucciones dnsmasq - Debian

Para instalarlo:

apt install dnsmasq

Para configurarlo se crea un fichero nuevo /etc/dnsmasq.d/router.conf. Se va a crear un pool de 20 Ips y se utilizarán los DNS de Google. Es importante que la IP coincida con la IP estática asignada a wlan0 cuando se configuró NetworkManager. El contenido de /etc/dnsmasq.d/router.conf inicialmente puede ser el siguiente

interface=wlan0
listen-address=192.168.20.1
bind-interfaces
server=8.8.8.8
domain-needed
bogus-priv
dhcp-range=192.168.20.100,192.168.20.120,24h

Para activar el servicio en el arranque y probar si funciona correctamente se debe iniciar el servicio y comprobar si no hay errores mediante:

systemctl enable dnsmasq
systemctl restart dnsmasq
systemctl status dnsmasq

Debido a un bug el inicio puede fallar, es recomendable cambiar el servicio con systemctl edit dnsmasq.service y añadir:

[Unit]
After=network-online.target
Wants=network-online.target

Después es recomendable recargar la configuración de systemd con: systemctl daemon-reload

Firewall y NAT con NFTables

Para compartir Internet se usará nftables ya que iptables está obsoleto y es buen momento para ir aprendiendo cosas nuevas.

apt purge iptables
apt install nftables

Probablemente en la distribución ya está instalado, pero no está activo el en el arranque del sistema por ello hay que ejecutar:

systemctl enable nftables

La activación del servicio en el paso anterior hace que el sistema tras iniciar la red cargue siempre el fichero /etc/nftables.conf que es donde está el firewall del sistema.

Para activar NAT hay que editar el fichero /etc/nftables.conf y añadir al final del mismo:

table ip nat {
        chain prerouting {
                type nat hook prerouting priority 0; policy accept;
        }

        chain postrouting {
                type nat hook postrouting priority 100; policy accept;
                masquerade random,persistent
        }
}

Para cargar esta configuración se puede ejecutar directamente /etc/nftables.conf o reiniciar el servicio que es lo mismo:

systemctl restart nftables

A partir de este momento si todo ha ido bien funcionará el nuevo punto de acceso compartiendo Internet.

Añadiendo la calidad de servicio / control de tráfico

Añadiendo QoS a este sistema persigue conseguir los siguientes objetivos:

  1. Evitar agotar la tarifa de datos móvil del modem
  2. Evitar que uno de los dispositivos conectados al router se quede con todo el ancho de banda y se queden los demás sin servicio.

Para conseguir lo anterior, la solución aquí presentada hace lo siguiente:

  1. Se establece un límite al ancho de banda global (entrada/salida) de la conexión externa del modem.
  2. Se reparte de manera equitativa el ancho de banda externo configurado en el punto anterior entre todos los dispositivos conectados al router, y a cada dispositivo se le garantiza al menos un mínimo de ancho de banda.

Para que todo opere correctamente la configuración anterior debe ser coherente de forma que:

  1. El límite exterior debe ser inferior al ancho de banda de la conexión externa. Esto es complicado de saber, pues el modem puede alternar en 3G/4G. La mejor opción es medirla o ir probando sabiendo cual es la máxima.
  2. Hay que asegurarse que la suma de los anchos de banda garantizados no exceda el límite del punto anterior.

Si deseas profundizar en este tema las referencias son:

Configuración propuesta

El procedimiento propuesto consiste en cargar un script QoS cuando se active la conexión del modem. Para ello, en NetworkManager cuando ocurre un evento se ejecutan los scripts del directorio /etc/NetworkManager/dispatcher.d/. Así, hay que añadir nuevo script que compruebe la interfaz que se está disparando es wwan0 y añadir el control de tráfico.

En este primer ejemplo simple, sólo se va a limitar el tráfico entrante y saliente a un determinado caudal. No se hará reparto equitativo entre las diferentes IPs conectadas al punto de acceso. Si tienes muchos equipos consumiendo datos y deseas que todos tengan siempre algo de ancho de banda debes realizar la configuración avanzada contada en la próxima sección.

En este ejemplo básico se creará el fichero /etc/NetworkManager/dispatcher.d/modem-qos.sh con permiso de ejecución: chmod +x /etc/NetworkManager/dispatcher.d/modem-qos.sh. El contenido sería el mostrado pero se deben establecer los límites adecuados en las variables I_LIMIT=4Mbit y O_LIMIT=4Mbit

#!/usr/bin/env bash

interface=$1
event=$2

INET=wwan0   # Salida del modem
WIFI=wlan0   # Wifi en modo AP

I_LIMIT=4Mbit  ## límite en la entrada aplicada en wifi
O_LIMIT=2Mbit  ## límite en salida aplicada en wwan0

function start {
    set -x
    # Tráfico entrante
    tc qdisc del dev $WIFI root 2> /dev/null
    tc qdisc add dev $WIFI root handle 1: htb default 30
    tc class add dev $WIFI parent 1: classid 1:1 htb rate $I_LIMIT burst 5K

    ## Tráfico saliente
    tc qdisc del dev $INET root 2> /dev/null
    tc qdisc add dev $INET root handle 2: htb default 30
    tc class add dev $INET parent 2: classid 2:1 htb rate $O_LIMIT burst 5K
    set +x
}

function stop {
    # Tráfico entrante
    tc qdisc del dev $WIFI root 2> /dev/null

    ## Tráfico saliente
    tc qdisc del dev $INET root 2> /dev/null
}

if [[ $interface = "wwan0" ]] && [[ $event = "up" ]]
then
  start
fi

if [[ $interface = "wwan0" ]] && [[ $event = "down" ]]
then
  stop
fi

Para comprobar su la configuración es correcta basta con quitar y poner el modem USB. Si se ha disparado correctamente el script anterior se podrá ver en el fichero syslog y además se puede comprobar mediante los comandos tc que muestran las estadísticas:

tc -s -d qdisc show dev wlan0
tc -s -d class show dev wlan0

tc -s -d qdisc show dev wwan0
tc -s -d class show dev wwan0

Versión avanzada de calidad de servicio / control de tráfico

En esta segunda versión se asigna límites a cada una de las IPs conectadas. Estás deben de coincidir con las configuradas anteriormente con dnsmasq. Además estoy dando prioridad a las conexiones SSH, a los paquetes DNS y permitiendo la red local a máxima velocidad para asegurarme que todo funciona mejor.

En este ejemplo los parámetros a establecer son los siguientes:

  • IP_LIMIT: Mínimo ancho de banda de entrada garantizado a cada IP
  • I_LIMIT: Máximo ancho de banda de entrada alcanzable por IP
  • O_LIMIT: Límite global del ancho de banda de salida

Para que la configuración opere correctamente hay que considerar que si se permiten 10 dispositivos conectados simultáneamente entonces la siguiente restricción debe cumplirse: 10 × IP_LIMIT ≤ O_LIMIT

#!/usr/bin/env bash

interface=$1
event=$2

# Trafic shaper
INET=wwan0
WIFI=wlan0

IP_LIMIT=500kbit # Mínimo ancho de banda de entrada garantizada a cada IP (rate)
I_LIMIT=5Mbit    # Máximo ancho de banda de entrada alcanzable por IP (ceil)
O_LIMIT=2Mbit    # Límite global del ancho de banda de salida

function start {

    # Tráfico entrante
    tc qdisc del dev $WIFI root 2> /dev/null
    tc qdisc add dev $WIFI root handle 1: htb default 30
    tc class add dev $WIFI parent 1: classid 1:1 htb rate $I_LIMIT burst 5K
    tc class add dev $WIFI parent 1: classid 1:2 htb rate 100Mbit # Red local

    tc class add dev $WIFI parent 1:1 classid 1:10 htb rate 150kbit ceil $I_LIMIT prio 1
    tc class add dev $WIFI parent 1:1 classid 1:30 htb rate 100kbit ceil $I_LIMIT prio 2

    # Filtro para red local
    tc filter add dev $WIFI \
        protocol ip parent 1: prio 1 u32 \
        match ip src 192.168.20.0/24 \
        match ip dst 192.168.20.0/24 \
        flowid 1:2


    # Filtro para tráfico SSH
    tc filter add dev $WIFI \
        protocol ip parent 1: prio 1 u32 \
        match ip sport 22 0xffff \
        flowid 1:10

    # Filtro para tráfico SSH
    tc filter add dev $WIFI \
        protocol ip parent 1: prio 1 u32 \
        match ip dport 22 0xffff \
        flowid 1:10

    # Filtro para tráfico DNS
    tc filter add dev $WIFI protocol ip parent 1: prio 1 u32 \
        match ip sport 53 0xffff flowid 1:10

    for ip in {100..120} ; do
        tc class add dev $WIFI parent 1:1 classid 1:$ip htb rate $IP_LIMIT ceil $I_LIMIT prio 2
        tc filter add dev $WIFI parent 1:0 protocol ip prio 1 u32 \
            match ip dst 192.168.20.$ip flowid 1:$ip
    done


    ## Tráfico saliente
    tc qdisc del dev $INET root 2> /dev/null
    tc qdisc add dev $INET root handle 2: htb default 30
    tc class add dev $INET parent 2: classid 2:1 htb rate $O_LIMIT burst 5K

    tc class add dev $INET parent 2:1 classid 2:10 htb rate 500kbit ceil $O_LIMIT prio 1
    tc class add dev $INET parent 2:1 classid 2:30 htb rate 200kbit ceil $O_LIMIT prio 2

    # Filtro para tráfico DNS saliente
    tc filter add dev $INET protocol ip parent 2: prio 1 u32 \
        match ip dport 53 0xffff flowid 2:10
}

function stop {
    # Tráfico entrante
    tc qdisc del dev $WIFI root 2> /dev/null

    ## Tráfico saliente
    tc qdisc del dev $INET root 2> /dev/null
}


if [[ $interface = "wwan0" ]] && [[ $event = "up" ]]
then
  start
fi

if [[ $interface = "wwan0" ]] && [[ $event = "down" ]]
then
  stop
fi
#raspberrypi