CoA implementado
This commit is contained in:
@@ -31,6 +31,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "1812:1812/udp"
|
- "1812:1812/udp"
|
||||||
- "1813:1813/udp"
|
- "1813:1813/udp"
|
||||||
|
- "3799:3799/udp"
|
||||||
environment:
|
environment:
|
||||||
- RADIUS_CLIENTS_CIDR=${RADIUS_CLIENTS_CIDR:-0.0.0.0/0}
|
- RADIUS_CLIENTS_CIDR=${RADIUS_CLIENTS_CIDR:-0.0.0.0/0}
|
||||||
- RADIUS_SHARED_SECRET=${RADIUS_SHARED_SECRET:-testing123}
|
- RADIUS_SHARED_SECRET=${RADIUS_SHARED_SECRET:-testing123}
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ server default {
|
|||||||
port = 1813
|
port = 1813
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Listen for incoming CoA/Disconnect-Request (for testing / tooling)
|
||||||
|
listen {
|
||||||
|
type = coa
|
||||||
|
ipaddr = *
|
||||||
|
port = 3799
|
||||||
|
}
|
||||||
|
|
||||||
authorize {
|
authorize {
|
||||||
# Si es EAP (WPA-Enterprise)
|
# Si es EAP (WPA-Enterprise)
|
||||||
if (&EAP-Message) {
|
if (&EAP-Message) {
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ const RADIUS_SECRET = process.env.RADIUS_SECRET || process.env.RADIUS_SHARED_SEC
|
|||||||
const requests = [];
|
const requests = [];
|
||||||
const sseClients = new Set();
|
const sseClients = new Set();
|
||||||
let radiusReloading = false;
|
let radiusReloading = false;
|
||||||
|
// Active sessions indexed by Acct-Session-Id
|
||||||
|
const activeSessions = new Map();
|
||||||
|
|
||||||
function pushRequest(rec) {
|
function pushRequest(rec) {
|
||||||
requests.push(rec);
|
requests.push(rec);
|
||||||
@@ -53,6 +55,7 @@ const PGDATABASE = process.env.PGDATABASE || 'radius';
|
|||||||
const PGUSER = process.env.PGUSER || 'radius';
|
const PGUSER = process.env.PGUSER || 'radius';
|
||||||
const PGPASSWORD = process.env.PGPASSWORD || 'radius';
|
const PGPASSWORD = process.env.PGPASSWORD || 'radius';
|
||||||
const pool = new Pool({ host: PGHOST, port: PGPORT, database: PGDATABASE, user: PGUSER, password: PGPASSWORD });
|
const pool = new Pool({ host: PGHOST, port: PGPORT, database: PGDATABASE, user: PGUSER, password: PGPASSWORD });
|
||||||
|
const SESSION_TIMEOUT = parseInt(process.env.SESSION_TIMEOUT || process.env.SESSION_TIMEOUT_SECONDS || '0', 10) || 0;
|
||||||
|
|
||||||
// SQL helpers: users in radcheck/radreply
|
// SQL helpers: users in radcheck/radreply
|
||||||
async function readUsersFromDb() {
|
async function readUsersFromDb() {
|
||||||
@@ -107,8 +110,11 @@ async function upsertUserToDb(user) {
|
|||||||
['WISPr-Bandwidth-Max-Down', String(MAX_DOWN)],
|
['WISPr-Bandwidth-Max-Down', String(MAX_DOWN)],
|
||||||
['WISPr-Bandwidth-Max-Up', String(MAX_UP)],
|
['WISPr-Bandwidth-Max-Up', String(MAX_UP)],
|
||||||
];
|
];
|
||||||
|
if (SESSION_TIMEOUT > 0) {
|
||||||
|
attrs.push(['Session-Timeout', String(SESSION_TIMEOUT)]);
|
||||||
|
}
|
||||||
await client.query(
|
await client.query(
|
||||||
"DELETE FROM radreply WHERE username = $1 AND attribute IN ('Tunnel-Type','Tunnel-Medium-Type','Tunnel-Private-Group-Id','WISPr-Bandwidth-Max-Down','WISPr-Bandwidth-Max-Up')",
|
"DELETE FROM radreply WHERE username = $1 AND attribute IN ('Tunnel-Type','Tunnel-Medium-Type','Tunnel-Private-Group-Id','WISPr-Bandwidth-Max-Down','WISPr-Bandwidth-Max-Up','Session-Timeout')",
|
||||||
[username]
|
[username]
|
||||||
);
|
);
|
||||||
for (const [attr, val] of attrs) {
|
for (const [attr, val] of attrs) {
|
||||||
@@ -195,11 +201,34 @@ app.post('/authorize', (req, res) => {
|
|||||||
app.post('/accounting', (req, res) => {
|
app.post('/accounting', (req, res) => {
|
||||||
console.log('--- RADIUS Accounting ---');
|
console.log('--- RADIUS Accounting ---');
|
||||||
console.log(JSON.stringify(req.body, null, 2));
|
console.log(JSON.stringify(req.body, null, 2));
|
||||||
|
const attrs = normalizeAttributes(req.body);
|
||||||
|
try {
|
||||||
|
const st = String(attrs['Acct-Status-Type'] || attrs['Acct-Status-Type*0'] || '').toUpperCase();
|
||||||
|
const sessionId = String(attrs['Acct-Session-Id'] || '');
|
||||||
|
const username = String(attrs['User-Name'] || '');
|
||||||
|
if (sessionId && username) {
|
||||||
|
if (st === 'START' || st === 'ALIVE' || st === 'INTERIM-UPDATE' || st === 'INTERIM') {
|
||||||
|
activeSessions.set(sessionId, {
|
||||||
|
username,
|
||||||
|
sessionId,
|
||||||
|
nasIp: attrs['NAS-IP-Address'] || '',
|
||||||
|
nasId: attrs['NAS-Identifier'] || '',
|
||||||
|
callingStationId: attrs['Calling-Station-Id'] || '',
|
||||||
|
calledStationId: attrs['Called-Station-Id'] || '',
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
} else if (st === 'STOP') {
|
||||||
|
activeSessions.delete(sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('accounting session update error:', e);
|
||||||
|
}
|
||||||
pushRequest({
|
pushRequest({
|
||||||
id: Date.now() + ':' + Math.random().toString(16).slice(2),
|
id: Date.now() + ':' + Math.random().toString(16).slice(2),
|
||||||
ts: new Date().toISOString(),
|
ts: new Date().toISOString(),
|
||||||
type: 'accounting',
|
type: 'accounting',
|
||||||
attrs: normalizeAttributes(req.body),
|
attrs,
|
||||||
});
|
});
|
||||||
return res.status(200).json({});
|
return res.status(200).json({});
|
||||||
});
|
});
|
||||||
@@ -250,6 +279,9 @@ app.patch('/api/users/:username', async (req, res) => {
|
|||||||
disabled: disabled !== undefined ? !!disabled : current.disabled,
|
disabled: disabled !== undefined ? !!disabled : current.disabled,
|
||||||
};
|
};
|
||||||
await upsertUserToDb(next);
|
await upsertUserToDb(next);
|
||||||
|
if (disabled === true) {
|
||||||
|
disconnectUserSessions(uname).catch(err => console.error('CoA disconnect error:', err));
|
||||||
|
}
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -369,6 +401,90 @@ async function sendRadiusSelfTest() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send RADIUS Disconnect-Request (CoA) to NAS (UDP 3799)
|
||||||
|
async function sendDisconnectRequest({ nasIp, username, sessionId, callingStationId, nasId }) {
|
||||||
|
if (!nasIp) throw new Error('NAS IP required for Disconnect-Request');
|
||||||
|
const packet = radius.encode({
|
||||||
|
code: 'Disconnect-Request',
|
||||||
|
secret: RADIUS_SECRET,
|
||||||
|
attributes: {
|
||||||
|
'User-Name': username,
|
||||||
|
'Acct-Session-Id': sessionId,
|
||||||
|
'Calling-Station-Id': callingStationId || undefined,
|
||||||
|
'NAS-IP-Address': nasIp,
|
||||||
|
'NAS-Identifier': nasId || undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const client = dgram.createSocket('udp4');
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
client.close();
|
||||||
|
reject(new Error('CoA timeout'));
|
||||||
|
}, 3000);
|
||||||
|
client.on('message', (msg) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
client.close();
|
||||||
|
try {
|
||||||
|
const res = radius.decode({ packet: msg, secret: RADIUS_SECRET });
|
||||||
|
resolve({ code: res.code });
|
||||||
|
} catch (e) {
|
||||||
|
resolve({ code: 'unknown' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
client.send(packet, 0, packet.length, 3799, nasIp, (err) => {
|
||||||
|
if (err) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
client.close();
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disconnectUserSessions(username) {
|
||||||
|
const targets = [];
|
||||||
|
for (const sess of activeSessions.values()) {
|
||||||
|
if (sess.username === username && sess.nasIp) targets.push(sess);
|
||||||
|
}
|
||||||
|
if (targets.length === 0) return;
|
||||||
|
for (const sess of targets) {
|
||||||
|
try {
|
||||||
|
const result = await sendDisconnectRequest({
|
||||||
|
nasIp: sess.nasIp,
|
||||||
|
username: sess.username,
|
||||||
|
sessionId: sess.sessionId,
|
||||||
|
callingStationId: sess.callingStationId,
|
||||||
|
nasId: sess.nasId,
|
||||||
|
});
|
||||||
|
pushRequest({
|
||||||
|
id: Date.now() + ':' + Math.random().toString(16).slice(2),
|
||||||
|
ts: new Date().toISOString(),
|
||||||
|
type: 'coa-disconnect',
|
||||||
|
attrs: {
|
||||||
|
'User-Name': sess.username,
|
||||||
|
'NAS-IP-Address': sess.nasIp,
|
||||||
|
'Acct-Session-Id': sess.sessionId,
|
||||||
|
'Calling-Station-Id': sess.callingStationId,
|
||||||
|
'result': result.code,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
pushRequest({
|
||||||
|
id: Date.now() + ':' + Math.random().toString(16).slice(2),
|
||||||
|
ts: new Date().toISOString(),
|
||||||
|
type: 'coa-disconnect',
|
||||||
|
attrs: {
|
||||||
|
'User-Name': sess.username,
|
||||||
|
'NAS-IP-Address': sess.nasIp,
|
||||||
|
'Acct-Session-Id': sess.sessionId,
|
||||||
|
'Calling-Station-Id': sess.callingStationId,
|
||||||
|
'error': String(e?.message || e),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
app.post('/test/radius', async (req, res) => {
|
app.post('/test/radius', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const result = await sendRadiusSelfTest();
|
const result = await sendRadiusSelfTest();
|
||||||
|
|||||||
Reference in New Issue
Block a user