commit 6ef48911efe33bffda9dc8f3914ba008608b8e0a Author: Codex Bot Date: Wed Sep 24 14:12:26 2025 -0600 Initial stack: FreeRADIUS + Node API + docker-compose diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8416c5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,38 @@ +version: "3.9" + +services: + node: + build: ./node-api + ports: + - "3000:3000" + environment: + - VLAN_ID=2 + - MAX_UP=10000000 + - MAX_DOWN=10000000 + networks: + - radius_net + + freeradius: + image: freeradius/freeradius-server:3.2.2 + depends_on: + - node + ports: + - "1812:1812/udp" + - "1813:1813/udp" + environment: + - REST_ENDPOINT=http://node:3000 + - RADIUS_CLIENTS_CIDR=${RADIUS_CLIENTS_CIDR:-0.0.0.0/0} + - RADIUS_SHARED_SECRET=${RADIUS_SHARED_SECRET:-testing123} + volumes: + - ./freeradius/mods-available/rest:/etc/freeradius/mods-available/rest:ro + - ./freeradius/mods-available/rest:/etc/freeradius/mods-enabled/rest:ro + - ./freeradius/sites-enabled/default:/etc/freeradius/sites-enabled/default:ro + - ./freeradius/clients.conf:/etc/freeradius/clients.conf:ro + command: ["-X"] + networks: + - radius_net + +networks: + radius_net: + driver: bridge + diff --git a/freeradius/clients.conf b/freeradius/clients.conf new file mode 100644 index 0000000..2bd1469 --- /dev/null +++ b/freeradius/clients.conf @@ -0,0 +1,7 @@ +client unifi { + ipaddr = %{env:RADIUS_CLIENTS_CIDR} + secret = %{env:RADIUS_SHARED_SECRET} + require_message_authenticator = no + nastype = other +} + diff --git a/freeradius/mods-available/rest b/freeradius/mods-available/rest new file mode 100644 index 0000000..5034646 --- /dev/null +++ b/freeradius/mods-available/rest @@ -0,0 +1,22 @@ +rest { + # Timeouts + connect_timeout = 4 + read_timeout = 8 + + # Authorize: llama al API Node + authorize { + uri = "%{env:REST_ENDPOINT:-http://node:3000}/authorize" + method = "post" + body = "json" + # send_all = yes -> envía todos los atributos del paquete + # por defecto rlm_rest ya serializa atributos en JSON + } + + # Accounting: opcional + accounting { + uri = "%{env:REST_ENDPOINT:-http://node:3000}/accounting" + method = "post" + body = "json" + } +} + diff --git a/freeradius/sites-enabled/default b/freeradius/sites-enabled/default new file mode 100644 index 0000000..7fcbcbb --- /dev/null +++ b/freeradius/sites-enabled/default @@ -0,0 +1,43 @@ +server default { + listen { + type = auth + ipaddr = * + port = 1812 + } + + listen { + type = acct + ipaddr = * + port = 1813 + } + + 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 + } + } + } + + authenticate { + # Aceptar todo cuando control:Auth-Type := Accept + Auth-Type Accept { + ok + } + } + + accounting { + rest + ok + } + + post-auth { + # Aquí podríamos volver a llamar a REST para atributos dinámicos + # rest + } +} + diff --git a/node-api/Dockerfile b/node-api/Dockerfile new file mode 100644 index 0000000..b077b84 --- /dev/null +++ b/node-api/Dockerfile @@ -0,0 +1,11 @@ +FROM node:20-alpine +WORKDIR /app + +COPY package*.json ./ +RUN npm ci --only=production || npm i --only=production + +COPY . . + +EXPOSE 3000 +CMD ["node", "index.js"] + diff --git a/node-api/index.js b/node-api/index.js new file mode 100644 index 0000000..df42ce5 --- /dev/null +++ b/node-api/index.js @@ -0,0 +1,56 @@ +import express from 'express'; +import morgan from 'morgan'; + +const app = express(); +app.use(express.json()); +app.use(morgan('dev')); + +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 + +// Helper: standard Accept with VLAN + bandwidth +function buildAcceptPayload(extra = {}) { + return { + control: { + 'Auth-Type': 'Accept', + ...extra.control, + }, + reply: { + 'Tunnel-Type': 'VLAN', + 'Tunnel-Medium-Type': 'IEEE-802', + 'Tunnel-Private-Group-Id': String(VLAN_ID), + 'WISPr-Bandwidth-Max-Down': String(MAX_DOWN), + 'WISPr-Bandwidth-Max-Up': String(MAX_UP), + ...extra.reply, + }, + }; +} + +// 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()); +}); + +// Accounting endpoint (opcional) +app.post('/accounting', (req, res) => { + console.log('--- RADIUS Accounting ---'); + console.log(JSON.stringify(req.body, null, 2)); + return res.status(200).json({}); +}); + +const port = process.env.PORT || 3000; +app.listen(port, () => { + console.log(`Node RADIUS REST API listening on :${port}`); +}); + diff --git a/node-api/package.json b/node-api/package.json new file mode 100644 index 0000000..ee0ac53 --- /dev/null +++ b/node-api/package.json @@ -0,0 +1,12 @@ +{ + "name": "radius-rest-api", + "version": "0.1.0", + "private": true, + "main": "index.js", + "type": "module", + "dependencies": { + "express": "^4.19.2", + "morgan": "^1.10.0" + } +} +