feat(smartradius): Implement full RADIUS server and client with RFC 2865/2866 compliance, including packet handling, authenticators, attributes, secrets manager, client APIs, and comprehensive tests and documentation
This commit is contained in:
649
ts_server/classes.radiusserver.ts
Normal file
649
ts_server/classes.radiusserver.ts
Normal file
@@ -0,0 +1,649 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import type {
|
||||
IRadiusServerOptions,
|
||||
IRadiusServerStats,
|
||||
IRadiusPacket,
|
||||
IAuthenticationRequest,
|
||||
IAuthenticationResponse,
|
||||
IAccountingRequest,
|
||||
IAccountingResponse,
|
||||
TAuthenticationHandler,
|
||||
TAccountingHandler,
|
||||
} from './interfaces.js';
|
||||
import {
|
||||
ERadiusCode,
|
||||
ERadiusAttributeType,
|
||||
EAcctStatusType,
|
||||
ENasPortType,
|
||||
EServiceType,
|
||||
} from './interfaces.js';
|
||||
import { RadiusPacket } from './classes.radiuspacket.js';
|
||||
import { RadiusAttributes } from './classes.radiusattributes.js';
|
||||
import { RadiusAuthenticator } from './classes.radiusauthenticator.js';
|
||||
import { RadiusSecrets } from './classes.radiussecrets.js';
|
||||
|
||||
/**
|
||||
* RADIUS Server implementation
|
||||
* Implements RFC 2865 (Authentication) and RFC 2866 (Accounting)
|
||||
*/
|
||||
export class RadiusServer {
|
||||
private authSocket?: plugins.dgram.Socket;
|
||||
private acctSocket?: plugins.dgram.Socket;
|
||||
private readonly secrets: RadiusSecrets;
|
||||
private authenticationHandler?: TAuthenticationHandler;
|
||||
private accountingHandler?: TAccountingHandler;
|
||||
|
||||
private readonly options: Required<
|
||||
Pick<IRadiusServerOptions, 'authPort' | 'acctPort' | 'bindAddress' | 'duplicateDetectionWindow' | 'maxPacketSize'>
|
||||
>;
|
||||
|
||||
private readonly stats: IRadiusServerStats = {
|
||||
authRequests: 0,
|
||||
authAccepts: 0,
|
||||
authRejects: 0,
|
||||
authChallenges: 0,
|
||||
authInvalidPackets: 0,
|
||||
authUnknownClients: 0,
|
||||
acctRequests: 0,
|
||||
acctResponses: 0,
|
||||
acctInvalidPackets: 0,
|
||||
acctUnknownClients: 0,
|
||||
};
|
||||
|
||||
// Duplicate detection cache: key = clientIp:clientPort:identifier
|
||||
private readonly recentRequests: Map<string, { timestamp: number; response?: Buffer }> = new Map();
|
||||
private duplicateCleanupInterval?: ReturnType<typeof setInterval>;
|
||||
|
||||
constructor(options: IRadiusServerOptions = {}) {
|
||||
this.options = {
|
||||
authPort: options.authPort ?? 1812,
|
||||
acctPort: options.acctPort ?? 1813,
|
||||
bindAddress: options.bindAddress ?? '0.0.0.0',
|
||||
duplicateDetectionWindow: options.duplicateDetectionWindow ?? 10000, // 10 seconds
|
||||
maxPacketSize: options.maxPacketSize ?? RadiusPacket.MAX_PACKET_SIZE,
|
||||
};
|
||||
|
||||
this.secrets = new RadiusSecrets({
|
||||
defaultSecret: options.defaultSecret,
|
||||
resolver: options.secretResolver,
|
||||
});
|
||||
|
||||
if (options.authenticationHandler) {
|
||||
this.authenticationHandler = options.authenticationHandler;
|
||||
}
|
||||
if (options.accountingHandler) {
|
||||
this.accountingHandler = options.accountingHandler;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the RADIUS server
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
await Promise.all([
|
||||
this.startAuthServer(),
|
||||
this.startAcctServer(),
|
||||
]);
|
||||
|
||||
// Start duplicate detection cleanup
|
||||
this.duplicateCleanupInterval = setInterval(() => {
|
||||
this.cleanupDuplicateCache();
|
||||
}, this.options.duplicateDetectionWindow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the RADIUS server
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
if (this.duplicateCleanupInterval) {
|
||||
clearInterval(this.duplicateCleanupInterval);
|
||||
this.duplicateCleanupInterval = undefined;
|
||||
}
|
||||
|
||||
const stopPromises: Promise<void>[] = [];
|
||||
|
||||
if (this.authSocket) {
|
||||
stopPromises.push(new Promise<void>((resolve) => {
|
||||
this.authSocket!.close(() => {
|
||||
this.authSocket = undefined;
|
||||
resolve();
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
if (this.acctSocket) {
|
||||
stopPromises.push(new Promise<void>((resolve) => {
|
||||
this.acctSocket!.close(() => {
|
||||
this.acctSocket = undefined;
|
||||
resolve();
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
await Promise.all(stopPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the authentication handler
|
||||
*/
|
||||
public setAuthenticationHandler(handler: TAuthenticationHandler): void {
|
||||
this.authenticationHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the accounting handler
|
||||
*/
|
||||
public setAccountingHandler(handler: TAccountingHandler): void {
|
||||
this.accountingHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set secret for a client
|
||||
*/
|
||||
public setClientSecret(clientIp: string, secret: string): void {
|
||||
this.secrets.setClientSecret(clientIp, secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get server statistics
|
||||
*/
|
||||
public getStats(): IRadiusServerStats {
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset server statistics
|
||||
*/
|
||||
public resetStats(): void {
|
||||
this.stats.authRequests = 0;
|
||||
this.stats.authAccepts = 0;
|
||||
this.stats.authRejects = 0;
|
||||
this.stats.authChallenges = 0;
|
||||
this.stats.authInvalidPackets = 0;
|
||||
this.stats.authUnknownClients = 0;
|
||||
this.stats.acctRequests = 0;
|
||||
this.stats.acctResponses = 0;
|
||||
this.stats.acctInvalidPackets = 0;
|
||||
this.stats.acctUnknownClients = 0;
|
||||
}
|
||||
|
||||
private async startAuthServer(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.authSocket = plugins.dgram.createSocket('udp4');
|
||||
|
||||
this.authSocket.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
this.authSocket.on('message', (msg, rinfo) => {
|
||||
this.handleAuthMessage(msg, rinfo).catch((err) => {
|
||||
console.error('Error handling auth message:', err);
|
||||
});
|
||||
});
|
||||
|
||||
this.authSocket.bind(this.options.authPort, this.options.bindAddress, () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async startAcctServer(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.acctSocket = plugins.dgram.createSocket('udp4');
|
||||
|
||||
this.acctSocket.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
this.acctSocket.on('message', (msg, rinfo) => {
|
||||
this.handleAcctMessage(msg, rinfo).catch((err) => {
|
||||
console.error('Error handling acct message:', err);
|
||||
});
|
||||
});
|
||||
|
||||
this.acctSocket.bind(this.options.acctPort, this.options.bindAddress, () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async handleAuthMessage(msg: Buffer, rinfo: plugins.dgram.RemoteInfo): Promise<void> {
|
||||
// Validate packet size
|
||||
if (msg.length > this.options.maxPacketSize) {
|
||||
this.stats.authInvalidPackets++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get client secret
|
||||
const secret = this.secrets.getSecret(rinfo.address);
|
||||
if (!secret) {
|
||||
this.stats.authUnknownClients++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse packet
|
||||
let packet: IRadiusPacket;
|
||||
try {
|
||||
packet = RadiusPacket.decode(msg);
|
||||
} catch (err) {
|
||||
this.stats.authInvalidPackets++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Only handle Access-Request
|
||||
if (packet.code !== ERadiusCode.AccessRequest) {
|
||||
this.stats.authInvalidPackets++;
|
||||
return;
|
||||
}
|
||||
|
||||
this.stats.authRequests++;
|
||||
|
||||
// Check for duplicate
|
||||
const duplicateKey = `${rinfo.address}:${rinfo.port}:${packet.identifier}`;
|
||||
const cached = this.recentRequests.get(duplicateKey);
|
||||
if (cached && cached.response) {
|
||||
// Retransmit cached response
|
||||
this.authSocket!.send(cached.response, rinfo.port, rinfo.address);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark as in-progress (no response yet)
|
||||
this.recentRequests.set(duplicateKey, { timestamp: Date.now() });
|
||||
|
||||
// Build authentication request context
|
||||
const authRequest = this.buildAuthRequest(packet, secret, rinfo);
|
||||
|
||||
// Call authentication handler
|
||||
let response: IAuthenticationResponse;
|
||||
if (this.authenticationHandler) {
|
||||
try {
|
||||
response = await this.authenticationHandler(authRequest);
|
||||
} catch (err) {
|
||||
response = {
|
||||
code: ERadiusCode.AccessReject,
|
||||
replyMessage: 'Internal server error',
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// No handler configured - reject all
|
||||
response = {
|
||||
code: ERadiusCode.AccessReject,
|
||||
replyMessage: 'Authentication service not configured',
|
||||
};
|
||||
}
|
||||
|
||||
// Build response packet
|
||||
const responsePacket = this.buildAuthResponse(
|
||||
response,
|
||||
packet.identifier,
|
||||
packet.authenticator,
|
||||
secret
|
||||
);
|
||||
|
||||
// Update stats
|
||||
switch (response.code) {
|
||||
case ERadiusCode.AccessAccept:
|
||||
this.stats.authAccepts++;
|
||||
break;
|
||||
case ERadiusCode.AccessReject:
|
||||
this.stats.authRejects++;
|
||||
break;
|
||||
case ERadiusCode.AccessChallenge:
|
||||
this.stats.authChallenges++;
|
||||
break;
|
||||
}
|
||||
|
||||
// Cache response for duplicate detection
|
||||
this.recentRequests.set(duplicateKey, {
|
||||
timestamp: Date.now(),
|
||||
response: responsePacket,
|
||||
});
|
||||
|
||||
// Send response
|
||||
this.authSocket!.send(responsePacket, rinfo.port, rinfo.address);
|
||||
}
|
||||
|
||||
private async handleAcctMessage(msg: Buffer, rinfo: plugins.dgram.RemoteInfo): Promise<void> {
|
||||
// Validate packet size
|
||||
if (msg.length > this.options.maxPacketSize) {
|
||||
this.stats.acctInvalidPackets++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get client secret
|
||||
const secret = this.secrets.getSecret(rinfo.address);
|
||||
if (!secret) {
|
||||
this.stats.acctUnknownClients++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify accounting request authenticator
|
||||
if (!RadiusAuthenticator.verifyAccountingRequestAuthenticator(msg, secret)) {
|
||||
this.stats.acctInvalidPackets++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse packet
|
||||
let packet: IRadiusPacket;
|
||||
try {
|
||||
packet = RadiusPacket.decode(msg);
|
||||
} catch (err) {
|
||||
this.stats.acctInvalidPackets++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Only handle Accounting-Request
|
||||
if (packet.code !== ERadiusCode.AccountingRequest) {
|
||||
this.stats.acctInvalidPackets++;
|
||||
return;
|
||||
}
|
||||
|
||||
this.stats.acctRequests++;
|
||||
|
||||
// Check for duplicate
|
||||
const duplicateKey = `acct:${rinfo.address}:${rinfo.port}:${packet.identifier}`;
|
||||
const cached = this.recentRequests.get(duplicateKey);
|
||||
if (cached && cached.response) {
|
||||
// Retransmit cached response
|
||||
this.acctSocket!.send(cached.response, rinfo.port, rinfo.address);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark as in-progress
|
||||
this.recentRequests.set(duplicateKey, { timestamp: Date.now() });
|
||||
|
||||
// Build accounting request context
|
||||
const acctRequest = this.buildAcctRequest(packet, rinfo);
|
||||
|
||||
// Call accounting handler
|
||||
let response: IAccountingResponse;
|
||||
if (this.accountingHandler) {
|
||||
try {
|
||||
response = await this.accountingHandler(acctRequest);
|
||||
} catch (err) {
|
||||
// Don't respond if we can't record the accounting data
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// No handler configured - accept all
|
||||
response = { success: true };
|
||||
}
|
||||
|
||||
// Only respond if accounting was successful
|
||||
if (!response.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build response packet
|
||||
const responsePacket = RadiusPacket.createAccountingResponse(
|
||||
packet.identifier,
|
||||
packet.authenticator,
|
||||
secret,
|
||||
response.attributes || []
|
||||
);
|
||||
|
||||
this.stats.acctResponses++;
|
||||
|
||||
// Cache response for duplicate detection
|
||||
this.recentRequests.set(duplicateKey, {
|
||||
timestamp: Date.now(),
|
||||
response: responsePacket,
|
||||
});
|
||||
|
||||
// Send response
|
||||
this.acctSocket!.send(responsePacket, rinfo.port, rinfo.address);
|
||||
}
|
||||
|
||||
private buildAuthRequest(
|
||||
packet: IRadiusPacket,
|
||||
secret: string,
|
||||
rinfo: plugins.dgram.RemoteInfo
|
||||
): IAuthenticationRequest {
|
||||
const getUsernameAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.UserName);
|
||||
const username = getUsernameAttr ? getUsernameAttr.toString('utf8') : '';
|
||||
|
||||
// Decrypt PAP password if present
|
||||
const passwordAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.UserPassword);
|
||||
let password: string | undefined;
|
||||
if (passwordAttr) {
|
||||
try {
|
||||
password = RadiusAuthenticator.decryptPassword(passwordAttr, packet.authenticator, secret);
|
||||
} catch {
|
||||
password = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Get CHAP attributes
|
||||
const chapPassword = RadiusPacket.getAttribute(packet, ERadiusAttributeType.ChapPassword);
|
||||
let chapChallenge = RadiusPacket.getAttribute(packet, ERadiusAttributeType.ChapChallenge);
|
||||
// If no CHAP-Challenge attribute, use Request Authenticator as challenge
|
||||
if (chapPassword && !chapChallenge) {
|
||||
chapChallenge = packet.authenticator;
|
||||
}
|
||||
|
||||
// Get NAS attributes
|
||||
const nasIpAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasIpAddress);
|
||||
const nasIpAddress = nasIpAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.NasIpAddress, nasIpAttr) as string : undefined;
|
||||
|
||||
const nasIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasIdentifier);
|
||||
const nasIdentifier = nasIdAttr ? nasIdAttr.toString('utf8') : undefined;
|
||||
|
||||
const nasPortAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasPort);
|
||||
const nasPort = nasPortAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.NasPort, nasPortAttr) as number : undefined;
|
||||
|
||||
const nasPortTypeAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasPortType);
|
||||
const nasPortType = nasPortTypeAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.NasPortType, nasPortTypeAttr) as ENasPortType : undefined;
|
||||
|
||||
// Get other common attributes
|
||||
const calledStationIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.CalledStationId);
|
||||
const calledStationId = calledStationIdAttr ? calledStationIdAttr.toString('utf8') : undefined;
|
||||
|
||||
const callingStationIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.CallingStationId);
|
||||
const callingStationId = callingStationIdAttr ? callingStationIdAttr.toString('utf8') : undefined;
|
||||
|
||||
const serviceTypeAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.ServiceType);
|
||||
const serviceType = serviceTypeAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.ServiceType, serviceTypeAttr) as EServiceType : undefined;
|
||||
|
||||
const framedProtocolAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.FramedProtocol);
|
||||
const framedProtocol = framedProtocolAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.FramedProtocol, framedProtocolAttr) as number : undefined;
|
||||
|
||||
const stateAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.State);
|
||||
|
||||
return {
|
||||
username,
|
||||
password,
|
||||
chapPassword,
|
||||
chapChallenge,
|
||||
nasIpAddress,
|
||||
nasIdentifier,
|
||||
nasPort,
|
||||
nasPortType,
|
||||
calledStationId,
|
||||
callingStationId,
|
||||
serviceType,
|
||||
framedProtocol,
|
||||
state: stateAttr,
|
||||
rawPacket: packet,
|
||||
clientAddress: rinfo.address,
|
||||
clientPort: rinfo.port,
|
||||
};
|
||||
}
|
||||
|
||||
private buildAcctRequest(
|
||||
packet: IRadiusPacket,
|
||||
rinfo: plugins.dgram.RemoteInfo
|
||||
): IAccountingRequest {
|
||||
const statusTypeAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctStatusType);
|
||||
const statusType = statusTypeAttr
|
||||
? RadiusAttributes.parseValue(ERadiusAttributeType.AcctStatusType, statusTypeAttr) as EAcctStatusType
|
||||
: EAcctStatusType.Start;
|
||||
|
||||
const sessionIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctSessionId);
|
||||
const sessionId = sessionIdAttr ? sessionIdAttr.toString('utf8') : '';
|
||||
|
||||
const usernameAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.UserName);
|
||||
const username = usernameAttr ? usernameAttr.toString('utf8') : undefined;
|
||||
|
||||
const nasIpAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasIpAddress);
|
||||
const nasIpAddress = nasIpAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.NasIpAddress, nasIpAttr) as string : undefined;
|
||||
|
||||
const nasIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasIdentifier);
|
||||
const nasIdentifier = nasIdAttr ? nasIdAttr.toString('utf8') : undefined;
|
||||
|
||||
const nasPortAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasPort);
|
||||
const nasPort = nasPortAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.NasPort, nasPortAttr) as number : undefined;
|
||||
|
||||
const nasPortTypeAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasPortType);
|
||||
const nasPortType = nasPortTypeAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.NasPortType, nasPortTypeAttr) as ENasPortType : undefined;
|
||||
|
||||
const delayTimeAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctDelayTime);
|
||||
const delayTime = delayTimeAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctDelayTime, delayTimeAttr) as number : undefined;
|
||||
|
||||
const inputOctetsAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctInputOctets);
|
||||
const inputOctets = inputOctetsAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctInputOctets, inputOctetsAttr) as number : undefined;
|
||||
|
||||
const outputOctetsAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctOutputOctets);
|
||||
const outputOctets = outputOctetsAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctOutputOctets, outputOctetsAttr) as number : undefined;
|
||||
|
||||
const sessionTimeAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctSessionTime);
|
||||
const sessionTime = sessionTimeAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctSessionTime, sessionTimeAttr) as number : undefined;
|
||||
|
||||
const inputPacketsAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctInputPackets);
|
||||
const inputPackets = inputPacketsAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctInputPackets, inputPacketsAttr) as number : undefined;
|
||||
|
||||
const outputPacketsAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctOutputPackets);
|
||||
const outputPackets = outputPacketsAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctOutputPackets, outputPacketsAttr) as number : undefined;
|
||||
|
||||
const terminateCauseAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctTerminateCause);
|
||||
const terminateCause = terminateCauseAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctTerminateCause, terminateCauseAttr) as number : undefined;
|
||||
|
||||
const authenticAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctAuthentic);
|
||||
const authentic = authenticAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctAuthentic, authenticAttr) as number : undefined;
|
||||
|
||||
const multiSessionIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctMultiSessionId);
|
||||
const multiSessionId = multiSessionIdAttr ? multiSessionIdAttr.toString('utf8') : undefined;
|
||||
|
||||
const linkCountAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctLinkCount);
|
||||
const linkCount = linkCountAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctLinkCount, linkCountAttr) as number : undefined;
|
||||
|
||||
const calledStationIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.CalledStationId);
|
||||
const calledStationId = calledStationIdAttr ? calledStationIdAttr.toString('utf8') : undefined;
|
||||
|
||||
const callingStationIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.CallingStationId);
|
||||
const callingStationId = callingStationIdAttr ? callingStationIdAttr.toString('utf8') : undefined;
|
||||
|
||||
return {
|
||||
statusType,
|
||||
sessionId,
|
||||
username,
|
||||
nasIpAddress,
|
||||
nasIdentifier,
|
||||
nasPort,
|
||||
nasPortType,
|
||||
delayTime,
|
||||
inputOctets,
|
||||
outputOctets,
|
||||
sessionTime,
|
||||
inputPackets,
|
||||
outputPackets,
|
||||
terminateCause,
|
||||
authentic,
|
||||
multiSessionId,
|
||||
linkCount,
|
||||
calledStationId,
|
||||
callingStationId,
|
||||
rawPacket: packet,
|
||||
clientAddress: rinfo.address,
|
||||
clientPort: rinfo.port,
|
||||
};
|
||||
}
|
||||
|
||||
private buildAuthResponse(
|
||||
response: IAuthenticationResponse,
|
||||
identifier: number,
|
||||
requestAuthenticator: Buffer,
|
||||
secret: string
|
||||
): Buffer {
|
||||
const attributes: Array<{ type: number | string; value: string | number | Buffer }> = [];
|
||||
|
||||
// Add reply message if present
|
||||
if (response.replyMessage) {
|
||||
attributes.push({ type: ERadiusAttributeType.ReplyMessage, value: response.replyMessage });
|
||||
}
|
||||
|
||||
// Add session timeout if present
|
||||
if (response.sessionTimeout !== undefined) {
|
||||
attributes.push({ type: ERadiusAttributeType.SessionTimeout, value: response.sessionTimeout });
|
||||
}
|
||||
|
||||
// Add idle timeout if present
|
||||
if (response.idleTimeout !== undefined) {
|
||||
attributes.push({ type: ERadiusAttributeType.IdleTimeout, value: response.idleTimeout });
|
||||
}
|
||||
|
||||
// Add state if present
|
||||
if (response.state) {
|
||||
attributes.push({ type: ERadiusAttributeType.State, value: response.state });
|
||||
}
|
||||
|
||||
// Add class if present
|
||||
if (response.class) {
|
||||
attributes.push({ type: ERadiusAttributeType.Class, value: response.class });
|
||||
}
|
||||
|
||||
// Add framed IP if present
|
||||
if (response.framedIpAddress) {
|
||||
attributes.push({ type: ERadiusAttributeType.FramedIpAddress, value: response.framedIpAddress });
|
||||
}
|
||||
|
||||
// Add framed IP netmask if present
|
||||
if (response.framedIpNetmask) {
|
||||
attributes.push({ type: ERadiusAttributeType.FramedIpNetmask, value: response.framedIpNetmask });
|
||||
}
|
||||
|
||||
// Add framed routes if present
|
||||
if (response.framedRoutes) {
|
||||
for (const route of response.framedRoutes) {
|
||||
attributes.push({ type: ERadiusAttributeType.FramedRoute, value: route });
|
||||
}
|
||||
}
|
||||
|
||||
// Add vendor attributes if present
|
||||
if (response.vendorAttributes) {
|
||||
for (const vsa of response.vendorAttributes) {
|
||||
const vsaBuffer = RadiusAttributes.encodeVSA(vsa);
|
||||
attributes.push({ type: ERadiusAttributeType.VendorSpecific, value: vsaBuffer });
|
||||
}
|
||||
}
|
||||
|
||||
// Add custom attributes if present
|
||||
if (response.attributes) {
|
||||
attributes.push(...response.attributes);
|
||||
}
|
||||
|
||||
// Create response based on code
|
||||
switch (response.code) {
|
||||
case ERadiusCode.AccessAccept:
|
||||
return RadiusPacket.createAccessAccept(identifier, requestAuthenticator, secret, attributes);
|
||||
case ERadiusCode.AccessReject:
|
||||
return RadiusPacket.createAccessReject(identifier, requestAuthenticator, secret, attributes);
|
||||
case ERadiusCode.AccessChallenge:
|
||||
return RadiusPacket.createAccessChallenge(identifier, requestAuthenticator, secret, attributes);
|
||||
default:
|
||||
return RadiusPacket.createAccessReject(identifier, requestAuthenticator, secret, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
private cleanupDuplicateCache(): void {
|
||||
const now = Date.now();
|
||||
const expiry = this.options.duplicateDetectionWindow;
|
||||
|
||||
for (const [key, entry] of this.recentRequests) {
|
||||
if (now - entry.timestamp > expiry) {
|
||||
this.recentRequests.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default RadiusServer;
|
||||
Reference in New Issue
Block a user