← Documentación

Manual 07 · RADIUS en sitio (Fase 2)

RADIUS es el camino de grado operador: tiempo, ancho de banda, corte en caliente y contabilidad real del consumo, con cualquier fabricante que hable el protocolo. Es también la respuesta cuando un firmware no expone portal externo (caso típico de Ruijie).

Por qué no vive en la nube. RADIUS habla UDP (1812 autenticación, 1813 contabilidad, 3799 corte). Railway —y la mayoría de las plataformas de aplicaciones— sólo exponen TCP/HTTP. El servidor RADIUS va en el sitio del ISP, junto al NAS, y habla con zonaw.ipsmart.app por HTTPS. No es una limitación del diseño: es cómo funciona el protocolo.


1. Arquitectura

  Teléfono ──── AP/NAS ──── FreeRADIUS (en sitio) ──HTTPS──► zonaw.ipsmart.app
                  │              │                              │
            UDP 1812/1813        │  agente de corte ◄──HTTPS────┘
                  ▲              │        (órdenes CoA pendientes)
                  └──── UDP 3799 ┘
  • El NAS pregunta «¿este usuario entra?» → FreeRADIUS consulta GET /api/radius/authorize de IPSmart Z, que responde con los atributos de esa sesión.
  • El NAS reporta consumo → FreeRADIUS lo reenvía a POST /api/radius/authorize, y el panel muestra bytes reales.
  • El corte anticipado lo pide IPSmart Z dejando una orden; el agente en sitio la recoge de GET /api/radius/ordenes y lanza el paquete CoA/Disconnect contra el NAS.

2. Levantar el servidor

En una máquina del sitio (un mini PC, una VM, un contenedor en el servidor del ISP) con acceso a la red de gestión del NAS:

mkdir -p /opt/zonaw-radius && cd /opt/zonaw-radius

docker-compose.yml:

services:
  radius:
    image: freeradius/freeradius-server:3.2-alpine
    container_name: zonaw-radius
    restart: unless-stopped
    network_mode: host          # RADIUS necesita ver las IPs reales de los NAS
    volumes:
      - ./config:/etc/raddb
    environment:
      - ZONAW_URL=https://zonaw.ipsmart.app
      - ZONAW_CLAVE_TAREAS=${ZONAW_CLAVE_TAREAS}

  agente:
    image: node:22-alpine
    container_name: zonaw-agente
    restart: unless-stopped
    network_mode: host
    working_dir: /app
    volumes:
      - ./agente:/app
    command: node agente.js
    environment:
      - ZONAW_URL=https://zonaw.ipsmart.app
      - ZONAW_CLAVE_TAREAS=${ZONAW_CLAVE_TAREAS}
      - NAS_HOST=10.60.0.1
      - NAS_SECRETO=${NAS_SECRETO}

ZONAW_CLAVE_TAREAS es el valor de Z_CLAVE_TAREAS del servicio de IPSmart Z. Sin él, los puntos de mantenimiento responden 401.


3. Clientes: quién puede preguntarle al RADIUS

config/clients.conf:

client nas-plaza {
    ipaddr          = 10.60.0.1
    secret          = PON-AQUI-UN-SECRETO-LARGO
    shortname       = plaza-bolivar
    nas_type        = other
    require_message_authenticator = yes
}
  • Un secreto distinto por NAS. Reusar el mismo en toda la red significa que comprometer un AP compromete la autenticación de todas las zonas.
  • require_message_authenticator = yes protege contra el ataque BlastRADIUS (CVE‑2024‑3596). No lo quites.

4. Consultar a IPSmart Z desde FreeRADIUS

config/mods-available/rest:

rest {
    connect_uri = "${ZONAW_URL}"

    authorize {
        uri         = "${..connect_uri}/api/radius/authorize?username=%{User-Name}&mac=%{Calling-Station-Id}"
        method      = 'get'
        tls         = ${..tls}
        header      = "X-Clave-Tareas: ${ZONAW_CLAVE_TAREAS}"
    }

    accounting {
        uri         = "${..connect_uri}/api/radius/authorize"
        method      = 'post'
        body        = 'json'
        header      = "X-Clave-Tareas: ${ZONAW_CLAVE_TAREAS}"
    }

    pool {
        start = 2
        min   = 1
        max   = 10
        idle_timeout = 60
    }
}

Y en config/sites-enabled/default, dentro de las secciones correspondientes:

authorize {
    preprocess
    rest
    pap
}

accounting {
    detail
    rest
}
ln -s ../mods-available/rest config/mods-enabled/rest
docker compose up -d
docker logs -f zonaw-radius

5. Qué responde IPSmart Z

{
  "Session-Timeout": 1800,
  "Idle-Timeout": 600,
  "Acct-Interim-Interval": 300,
  "Mikrotik-Rate-Limit": "1024k/4096k",
  "WISPr-Bandwidth-Max-Up": 1024000,
  "WISPr-Bandwidth-Max-Down": 4096000,
  "Class": "e3b0c442-98fc-4c14-9afb-f4c8996fb924"
}
Atributo Para qué
Session-Timeout Los segundos que le quedan. Se recalcula en cada consulta, no es un valor fijo: un cliente que se reconecta no reinicia su reloj
Idle-Timeout Cierra la sesión del que se fue sin desconectarse
Acct-Interim-Interval Cada cuánto reporta consumo el NAS (300 s es un buen equilibrio)
Mikrotik-Rate-Limit Límite en equipos MikroTik. subida/bajada, en ese orden
WISPr-Bandwidth-Max-* Lo mismo, en el atributo estándar que entienden los demás fabricantes. En bits por segundo
Class El id de la sesión de IPSmart Z. El NAS lo devuelve en la contabilidad, y es lo que permite cruzar el consumo con la persona

6. El agente de corte

agente/agente.js — pregunta cada 5 segundos si hay que desconectar a alguien y lanza el paquete CoA:

// Pregunta por órdenes, manda el Disconnect-Request al NAS (UDP 3799) y
// confirma. Es lo único que no se puede hacer desde la nube.
const CADA_MS = 5000

async function pasada() {
  const r = await fetch(`${process.env.ZONAW_URL}/api/radius/ordenes`, {
    headers: { 'X-Clave-Tareas': process.env.ZONAW_CLAVE_TAREAS },
  })
  const { ordenes } = await r.json()
  const cumplidas = []

  for (const orden of ordenes) {
    try {
      await desconectar(orden.mac)          // radclient / paquete CoA
      cumplidas.push(orden.id)
    } catch (e) {
      console.error('No se pudo desconectar', orden.mac, e.message)
    }
  }

  if (cumplidas.length) {
    await fetch(`${process.env.ZONAW_URL}/api/radius/ordenes`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-Clave-Tareas': process.env.ZONAW_CLAVE_TAREAS },
      body: JSON.stringify({ ids: cumplidas }),
    })
  }
}
setInterval(pasada, CADA_MS)

El GET de órdenes también sirve de latido: si el agente deja de preguntar, el panel de IPSmart Z marca ese RADIUS como caído a los 10 minutos. Un agente muerto sin aviso es una zona que no corta a nadie.

Desconexión a mano, para probar:

echo "User-Name=z-1a2b3c4d,Framed-IP-Address=10.60.1.34" | \
  radclient -x 10.60.0.1:3799 disconnect PON-AQUI-UN-SECRETO-LARGO

Respuesta esperada: Disconnect-ACK. Si responde Disconnect-NAK, el NAS no encontró la sesión (revisa que el identificador coincida); si no responde nada, el NAS no tiene habilitado CoA.


7. Habilitar CoA en el NAS

MikroTik:

/radius incoming set accept=yes port=3799
/radius add service=hotspot address=10.60.0.5 secret="..." timeout=3s
/ip hotspot profile set [find name=hsprof1] use-radius=yes radius-interim-update=5m

UniFi: Settings → Profiles → RADIUS con el servidor y el secreto; la red de invitados en modo RADIUS MAC auth o 802.1X según el diseño.

Omada: Settings → Authentication → RADIUS Profile.

Ruijie: Authentication → RADIUS Server, y comprobar que la contabilidad esté activada — sin ella no hay consumo ni corte por inactividad.


8. Comprobaciones

# ¿Autentica?
echo "User-Name=z-1a2b3c4d,User-Password=xxxxxxxxxxxx" | \
  radtest -x z-1a2b3c4d xxxxxxxxxxxx 127.0.0.1 0 testing123

# ¿Está llegando la contabilidad?
docker exec zonaw-radius tail -f /var/log/radius/radacct/*/detail-*

# ¿El agente sigue vivo? (en el panel: ficha de la zona → equipo)
curl -s -H "X-Clave-Tareas: $ZONAW_CLAVE_TAREAS" https://zonaw.ipsmart.app/api/radius/ordenes

9. Cuando algo no funciona

Síntoma Causa Arreglo
Access-Reject siempre El módulo rest no está en la sección authorize, o la clave de tareas es incorrecta Sección 4
Autentica pero sin límite de banda El NAS no entiende el atributo enviado Usar el Mikrotik-Rate-Limit o el WISPr-* según fabricante
El consumo no aparece en el panel El NAS no devuelve Class, o la contabilidad está apagada Sección 5 y 7
El corte no llega nunca El agente está caído, o el NAS no acepta CoA Sección 6 y 7
Funciona y de pronto todos son rechazados El certificado de zonaw.ipsmart.app cambió y el contenedor no lo confía docker compose restart radius tras actualizar los certificados raíz
Latencia alta en la autenticación El sitio tiene mal enlace a internet y cada consulta va a la nube Subir el pool de conexiones, y evaluar caché local de sesiones

10. Lista de comprobación

  • El servidor RADIUS está en el sitio, no en la nube.
  • Un secreto distinto por NAS, y require_message_authenticator = yes.
  • Módulo rest apuntando a zonaw.ipsmart.app con la clave de tareas.
  • Session-Timeout recalculado (probar reconectando: no debe reiniciarse el reloj).
  • Contabilidad llegando: el panel muestra bytes reales.
  • Agente de corte corriendo, con latido visible en el panel.
  • radclient … disconnect probado a mano y respondiendo Disconnect-ACK.
  • Prueba de corte real cronometrada, con un teléfono.