diff --git a/.env b/.env new file mode 100644 index 0000000..995c28b --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +RADIUS_CLIENTS_CIDR=192.168.87.0/24 +RADIUS_SHARED_SECRET=tamosbien + diff --git a/freeradius/clients.conf b/freeradius/clients.conf index 2bd1469..be71df1 100644 --- a/freeradius/clients.conf +++ b/freeradius/clients.conf @@ -1,7 +1,12 @@ client unifi { - ipaddr = %{env:RADIUS_CLIENTS_CIDR} - secret = %{env:RADIUS_SHARED_SECRET} + ipaddr = 192.168.87.0/24 + secret = tamosbien require_message_authenticator = no nastype = other } +# Cliente local para pruebas con radclient dentro del contenedor +client localhost { + ipaddr = 127.0.0.1 + secret = tamosbien +} diff --git a/freeradius/mods-available/rest b/freeradius/mods-available/rest index 5034646..cfc739c 100644 --- a/freeradius/mods-available/rest +++ b/freeradius/mods-available/rest @@ -5,7 +5,7 @@ rest { # Authorize: llama al API Node authorize { - uri = "%{env:REST_ENDPOINT:-http://node:3000}/authorize" + uri = "http://node:3000/authorize" method = "post" body = "json" # send_all = yes -> envía todos los atributos del paquete @@ -14,9 +14,8 @@ rest { # Accounting: opcional accounting { - uri = "%{env:REST_ENDPOINT:-http://node:3000}/accounting" + uri = "http://node:3000/accounting" method = "post" body = "json" } } - diff --git a/freeradius/sites-enabled/default b/freeradius/sites-enabled/default index 7fcbcbb..c7d46dd 100644 --- a/freeradius/sites-enabled/default +++ b/freeradius/sites-enabled/default @@ -14,12 +14,9 @@ server default { authorize { # Llama a la API REST para decidir y añadir atributos rest - - # Si la API no estableció Auth-Type, aceptamos por defecto (demo) - if (!&control:Auth-Type) { - update control { - Auth-Type := Accept - } + # Para laboratorio: aceptar todo siempre (MAC-Auth / pruebas) + update control { + Auth-Type := Accept } } @@ -40,4 +37,3 @@ server default { # rest } } - diff --git a/node-api/index.js b/node-api/index.js index df42ce5..bf4efd9 100644 --- a/node-api/index.js +++ b/node-api/index.js @@ -1,13 +1,35 @@ import express from 'express'; import morgan from 'morgan'; +import path from 'path'; +import { fileURLToPath } from 'url'; const app = express(); app.use(express.json()); app.use(morgan('dev')); +// Static files for dashboard +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +app.use(express.static(path.join(__dirname, 'public'))); + const VLAN_ID = process.env.VLAN_ID || '2'; const MAX_UP = process.env.MAX_UP || '10000000'; // bits per second const MAX_DOWN = process.env.MAX_DOWN || '10000000'; // bits per second +const MAX_REQUESTS = parseInt(process.env.MAX_REQUESTS || '200', 10); + +// In-memory request store + SSE clients +const requests = []; +const sseClients = new Set(); + +function pushRequest(rec) { + requests.push(rec); + while (requests.length > MAX_REQUESTS) requests.shift(); + // Broadcast via SSE + const payload = `data: ${JSON.stringify(rec)}\n\n`; + for (const res of sseClients) { + try { res.write(payload); } catch { /* ignore */ } + } +} // Helper: standard Accept with VLAN + bandwidth function buildAcceptPayload(extra = {}) { @@ -27,30 +49,75 @@ function buildAcceptPayload(extra = {}) { }; } +// Normalize attributes from FreeRADIUS rlm_rest JSON +function normalizeAttributes(body = {}) { + // Newer rlm_rest may send attributes at top-level as { Attr: { type, value: [..] } } + // or under body.attributes / body.request as plain map. + const src = body.attributes || body.request || body; + const out = {}; + for (const [k, v] of Object.entries(src || {})) { + if (v && typeof v === 'object' && Array.isArray(v.value)) out[k] = v.value[0]; + else out[k] = v; + } + return out; +} + // Authorize endpoint: FreeRADIUS rlm_rest calls this in authorize {} app.post('/authorize', (req, res) => { console.log('--- RADIUS Authorize Request ---'); console.log(JSON.stringify(req.body, null, 2)); - // Por ahora aprobamos todas las solicitudes válidas (si traen User-Name) - const attrs = (req.body && (req.body.attributes || req.body.request)) || {}; - if (!attrs['User-Name'] && !attrs['User-Name*0']) { - // Responder vacío -> no cambia nada; o devolver 204 - return res.status(200).json({}); - } - - return res.status(200).json(buildAcceptPayload()); + const attrs = normalizeAttributes(req.body); + const reply = buildAcceptPayload(); + pushRequest({ + id: Date.now() + ':' + Math.random().toString(16).slice(2), + ts: new Date().toISOString(), + type: 'authorize', + attrs, + decision: 'accept', + vlan: VLAN_ID, + bandwidth: { up: MAX_UP, down: MAX_DOWN }, + }); + return res.status(200).json(reply); }); // Accounting endpoint (opcional) app.post('/accounting', (req, res) => { console.log('--- RADIUS Accounting ---'); console.log(JSON.stringify(req.body, null, 2)); + pushRequest({ + id: Date.now() + ':' + Math.random().toString(16).slice(2), + ts: new Date().toISOString(), + type: 'accounting', + attrs: normalizeAttributes(req.body), + }); return res.status(200).json({}); }); +// API: recent requests +app.get('/api/requests', (req, res) => { + res.json({ items: requests.slice(-MAX_REQUESTS) }); +}); + +// SSE stream for live updates +app.get('/events', (req, res) => { + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.flushHeaders?.(); + // send a hello event + res.write(`event: hello\n`); + res.write(`data: {"ok":true}\n\n`); + sseClients.add(res); + req.on('close', () => sseClients.delete(res)); +}); + +// Root: serve dashboard +app.get('/', (req, res) => { + res.sendFile(path.join(__dirname, 'public', 'index.html')); +}); + const port = process.env.PORT || 3000; app.listen(port, () => { console.log(`Node RADIUS REST API listening on :${port}`); }); - diff --git a/node-api/public/index.html b/node-api/public/index.html new file mode 100644 index 0000000..86f3482 --- /dev/null +++ b/node-api/public/index.html @@ -0,0 +1,92 @@ + + + + + + RADIUS Dashboard + + + +
+

RADIUS Dashboard

+
+
+
+ Conectando… + 0 eventos + VLAN 2 • 10/10 Mbps +
+
+ + +
+
+
+ + + + +