diff --git a/docker-compose.yml b/docker-compose.yml index 3ff0e99..e157fc8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,11 @@ services: - RADIUS_HOST=freeradius - RADIUS_AUTH_PORT=1812 - RADIUS_SECRET=${RADIUS_SHARED_SECRET:-testing123} + volumes: + - ./node-api/index.js:/app/index.js:ro + - ./node-api/public:/app/public:ro + - /var/run/docker.sock:/var/run/docker.sock + - ./freeradius/mods-config/files/authorize:/shared/authorize networks: - radius_net @@ -29,8 +34,11 @@ services: volumes: - ./freeradius/mods-available/rest:/etc/freeradius/mods-available/rest:ro - ./freeradius/mods-available/rest:/etc/freeradius/mods-enabled/rest:ro + - ./freeradius/mods-available/rest_inner:/etc/freeradius/mods-available/rest_inner:ro + - ./freeradius/mods-available/rest_inner:/etc/freeradius/mods-enabled/rest_inner:ro + - ./freeradius/mods-enabled/eap:/etc/freeradius/mods-enabled/eap:ro - ./freeradius/sites-enabled/default:/etc/freeradius/sites-enabled/default:ro - # inner-tunnel: usar el archivo por defecto del contenedor + - ./freeradius/sites-enabled/inner-tunnel:/etc/freeradius/sites-enabled/inner-tunnel:ro - ./freeradius/mods-config/files/authorize:/etc/freeradius/mods-config/files/authorize:ro - ./freeradius/clients.conf:/etc/freeradius/clients.conf:ro command: ["-X"] diff --git a/freeradius/mods-available/rest b/freeradius/mods-available/rest index 248221e..7617ed5 100644 --- a/freeradius/mods-available/rest +++ b/freeradius/mods-available/rest @@ -25,4 +25,11 @@ rest { method = "post" body = "json" } + + # Post-auth: obtener atributos de respuesta (VLAN, etc.) + post-auth { + uri = "http://node:3000/post-auth" + method = "post" + body = "json" + } } diff --git a/freeradius/mods-available/rest_inner b/freeradius/mods-available/rest_inner new file mode 100644 index 0000000..520bc77 --- /dev/null +++ b/freeradius/mods-available/rest_inner @@ -0,0 +1,11 @@ +rest rest_inner { + connect_timeout = 4 + read_timeout = 8 + + authorize { + uri = "http://node:3000/authorize-inner" + method = "post" + body = "json" + } +} + diff --git a/freeradius/mods-config/files/authorize b/freeradius/mods-config/files/authorize index 8d59f9d..979e40a 100644 --- a/freeradius/mods-config/files/authorize +++ b/freeradius/mods-config/files/authorize @@ -1,3 +1,15 @@ +# Managed by Node dashboard; do not edit manually user1 Cleartext-Password := "contra1" -user2 Cleartext-Password := "contra2" + Tunnel-Type = VLAN, + Tunnel-Medium-Type = IEEE-802, + Tunnel-Private-Group-Id = "2" +user2 Cleartext-Password := "contra2" + Tunnel-Type = VLAN, + Tunnel-Medium-Type = IEEE-802, + Tunnel-Private-Group-Id = "3" + +prueba2 Cleartext-Password := "contra2" + Tunnel-Type = VLAN, + Tunnel-Medium-Type = IEEE-802, + Tunnel-Private-Group-Id = "2" diff --git a/freeradius/mods-enabled/eap b/freeradius/mods-enabled/eap index 3eb3d47..541c1dc 100644 --- a/freeradius/mods-enabled/eap +++ b/freeradius/mods-enabled/eap @@ -1,2 +1,30 @@ -$INCLUDE /etc/freeradius/mods-available/eap +eap { + default_eap_type = peap + tls-config tls-common { + private_key_password = whatever + private_key_file = ${certdir}/server.pem + certificate_file = ${certdir}/server.pem + ca_file = ${cadir}/ca.pem + dh_file = ${certdir}/dh + random_file = /dev/urandom + fragment_size = 1024 + include_length = yes + auto_chain = yes + } + + tls { + tls = tls-common + } + + peap { + tls = tls-common + default_eap_type = mschapv2 + copy_request_to_tunnel = yes + use_tunneled_reply = yes + virtual_server = "inner-tunnel" + } + + mschapv2 { + } +} diff --git a/freeradius/sites-enabled/default b/freeradius/sites-enabled/default index 1a1ae54..ad6a8e7 100644 --- a/freeradius/sites-enabled/default +++ b/freeradius/sites-enabled/default @@ -40,28 +40,7 @@ server default { } post-auth { - # Asignación de VLAN dinámica por usuario - if (&User-Name == "user1") { - update reply { - Tunnel-Type := VLAN - Tunnel-Medium-Type := IEEE-802 - Tunnel-Private-Group-Id := "2" - } - } - elsif (&User-Name == "user2") { - update reply { - Tunnel-Type := VLAN - Tunnel-Medium-Type := IEEE-802 - Tunnel-Private-Group-Id := "5" - } - } - else { - # Fallback opcional: comentar si no quieres valor por defecto - update reply { - Tunnel-Type := VLAN - Tunnel-Medium-Type := IEEE-802 - Tunnel-Private-Group-Id := "2" - } - } + # Obtener atributos de VLAN/otros desde el API + rest.post-auth } } diff --git a/freeradius/sites-enabled/inner-tunnel b/freeradius/sites-enabled/inner-tunnel index d261615..57377d7 100644 --- a/freeradius/sites-enabled/inner-tunnel +++ b/freeradius/sites-enabled/inner-tunnel @@ -6,16 +6,16 @@ server inner-tunnel { } authorize { - # Primero obtenemos credenciales del usuario desde el API - rest.authorize_inner_tunnel - # Luego dejamos que EAP procese (PEAP/MSCHAPv2) + # Obtener credenciales del usuario desde el API (debe devolver Cleartext-Password) + rest_inner + # Fallback/local: también consultar backend 'files' (user1/user2) + files + # Procesar EAP (PEAP) y MS-CHAPv2 eap - # mschap puede establecer Auth-Type si procede mschap } authenticate { - # Autenticación EAP (PEAP/MSCHAPv2) eap Auth-Type MS-CHAP { mschap @@ -23,7 +23,6 @@ server inner-tunnel { } post-auth { - # Aquí podríamos añadir lógica adicional de auditoría si se desea - # No agregamos atributos de reply aquí; se añadirán en el outer post-auth + # Nada aquí; el outer post-auth añadirá VLAN } } diff --git a/freeradius/startup.sh b/freeradius/startup.sh new file mode 100755 index 0000000..a15e481 --- /dev/null +++ b/freeradius/startup.sh @@ -0,0 +1,29 @@ +#!/bin/sh +set -eu + +AUTH_FILE="/etc/freeradius/mods-config/files/authorize" + +# Start FreeRADIUS in foreground +# Start in debug/foreground mode for logs +freeradius -X & +PID=$! + +prev_mtime="" + +poll_reload() { + while true; do + if [ -f "$AUTH_FILE" ]; then + mtime=$(stat -c %Y "$AUTH_FILE" 2>/dev/null || stat -f %m "$AUTH_FILE" 2>/dev/null || echo "") + if [ "${mtime}" != "${prev_mtime}" ] && [ -n "$mtime" ]; then + # File changed: send HUP to reload users + kill -HUP "$PID" 2>/dev/null || true + prev_mtime="$mtime" + fi + fi + sleep 2 + done +} + +poll_reload & + +wait "$PID" diff --git a/node-api/index.js b/node-api/index.js index ec49268..deecefb 100644 --- a/node-api/index.js +++ b/node-api/index.js @@ -1,7 +1,9 @@ import express from 'express'; import morgan from 'morgan'; +import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; +import http from 'http'; import dgram from 'dgram'; import radius from 'radius'; @@ -21,10 +23,18 @@ const MAX_REQUESTS = parseInt(process.env.MAX_REQUESTS || '200', 10); const RADIUS_HOST = process.env.RADIUS_HOST || 'freeradius'; const RADIUS_AUTH_PORT = parseInt(process.env.RADIUS_AUTH_PORT || '1812', 10); const RADIUS_SECRET = process.env.RADIUS_SECRET || process.env.RADIUS_SHARED_SECRET || 'tamosbien'; +const DOCKER_SOCK = process.env.DOCKER_SOCK || '/var/run/docker.sock'; +const FREERADIUS_CONTAINER = process.env.FREERADIUS_CONTAINER || 'radiusnucleo-freeradius-1'; // In-memory request store + SSE clients const requests = []; +// In-memory users: username -> { password, vlan } +const users = new Map([ + ['user1', { password: 'contra1', vlan: '2' }], + ['user2', { password: 'contra2', vlan: '3' }], +]); const sseClients = new Set(); +let radiusReloading = false; function pushRequest(rec) { requests.push(rec); @@ -36,6 +46,60 @@ function pushRequest(rec) { } } +function broadcastStatus(payload) { + const ev = `event: status\n` + `data: ${JSON.stringify(payload)}\n\n`; + for (const res of sseClients) { try { res.write(ev); } catch {} + } +} + +const AUTH_FILE = process.env.AUTH_FILE || '/shared/authorize'; + +async function persistUsersToFreeradius() { + try { + const header = '# Managed by Node dashboard; do not edit manually\n'; + const blocks = []; + for (const [username, { password, vlan }] of users.entries()) { + const v = String(vlan || VLAN_ID); + blocks.push(`${username} Cleartext-Password := "${password}" + Tunnel-Type = VLAN, + Tunnel-Medium-Type = IEEE-802, + Tunnel-Private-Group-Id = "${v}"\n`); + } + await fs.writeFile(AUTH_FILE, header + blocks.join('\n')); + // Trigger FreeRADIUS reload via Docker API (HUP) + triggerRadiusReload().catch(() => {}); + } catch (e) { + console.error('Failed to persist users to FreeRADIUS files:', e); + } +} + +async function triggerRadiusReload() { + try { + radiusReloading = true; + broadcastStatus({ radius_reloading: true }); + // Call Docker Engine API over unix socket: POST /containers/{id}/kill?signal=HUP + await new Promise((resolve, reject) => { + const req = http.request({ + method: 'POST', + socketPath: DOCKER_SOCK, + path: `/v1.41/containers/${encodeURIComponent(FREERADIUS_CONTAINER)}/kill?signal=HUP`, + }, (res) => { + res.resume(); + res.on('end', resolve); + }); + req.on('error', reject); + req.end(); + }); + } catch (e) { + console.error('Failed to HUP FreeRADIUS:', e?.message || e); + } finally { + setTimeout(() => { + radiusReloading = false; + broadcastStatus({ radius_reloading: false }); + }, 1500); + } +} + // Helper: standard Accept with VLAN + bandwidth function buildAcceptPayload(extra = {}) { return { @@ -105,12 +169,9 @@ app.post('/authorize-inner', (req, res) => { console.log(JSON.stringify(req.body, null, 2)); const attrs = normalizeAttributes(req.body); - const users = { - 'user1': 'contra1', - 'user2': 'contra2', - }; const username = (attrs['User-Name'] || '').toString(); - const password = users[username]; + const entry = users.get(username); + const password = entry?.password; if (!password) { pushRequest({ @@ -133,12 +194,43 @@ app.post('/authorize-inner', (req, res) => { }); return res.status(200).json({ - control: { - 'Cleartext-Password': password, - }, + control: [ + { name: 'Cleartext-Password', value: String(password) } + ] }); }); +// Post-auth: return reply attributes like VLAN based on user mapping +app.post('/post-auth', (req, res) => { + const attrs = normalizeAttributes(req.body); + const username = (attrs['User-Name'] || '').toString(); + const vlan = users.get(username)?.vlan || VLAN_ID; + return res.status(200).json({ + reply: [ + { name: 'Tunnel-Type', value: 'VLAN' }, + { name: 'Tunnel-Medium-Type', value: 'IEEE-802' }, + { name: 'Tunnel-Private-Group-Id', value: String(vlan) }, + { name: 'WISPr-Bandwidth-Max-Down', value: String(MAX_DOWN) }, + { name: 'WISPr-Bandwidth-Max-Up', value: String(MAX_UP) } + ] + }); +}); + +// Users API +app.get('/api/users', (req, res) => { + const items = Array.from(users.entries()).map(([username, { password, vlan }]) => ({ username, password, vlan })); + res.json({ items }); +}); + +app.post('/api/users', (req, res) => { + const { username, password, vlan } = req.body || {}; + if (!username || !password) return res.status(400).json({ ok: false, error: 'username and password required' }); + const vlanStr = vlan ? String(vlan) : VLAN_ID; + users.set(String(username), { password: String(password), vlan: vlanStr }); + persistUsersToFreeradius().then(() => console.log('Users synced to FreeRADIUS files')); + res.json({ ok: true }); +}); + // API: recent requests app.get('/api/requests', (req, res) => { res.json({ items: requests.slice(-MAX_REQUESTS) }); @@ -196,6 +288,9 @@ app.get('/events', (req, res) => { // send a hello event res.write(`event: hello\n`); res.write(`data: {"ok":true}\n\n`); + // send initial status + res.write(`event: status\n`); + res.write(`data: ${JSON.stringify({ radius_reloading: radiusReloading })}\n\n`); sseClients.add(res); req.on('close', () => sseClients.delete(res)); }); diff --git a/node-api/public/index.html b/node-api/public/index.html index eeb1dc1..32027da 100644 --- a/node-api/public/index.html +++ b/node-api/public/index.html @@ -29,6 +29,7 @@ Conectando… 0 eventos VLAN 2 • 10/10 Mbps +
+