import * as plugins from '../plugins.js'; import { logger } from '../logger.js'; import { VlanManager, type IMacVlanMapping, type IVlanManagerConfig } from './classes.vlan.manager.js'; import { AccountingManager, type IAccountingSession, type IAccountingManagerConfig } from './classes.accounting.manager.js'; /** * RADIUS client (NAS) configuration */ export interface IRadiusClient { /** Client name for identification */ name: string; /** IP address or CIDR range */ ipRange: string; /** Shared secret for this client */ secret: string; /** Optional description */ description?: string; /** Whether this client is enabled */ enabled: boolean; } /** * RADIUS server configuration */ export interface IRadiusServerConfig { /** Authentication port (default: 1812) */ authPort?: number; /** Accounting port (default: 1813) */ acctPort?: number; /** Bind address (default: 0.0.0.0) */ bindAddress?: string; /** NAS clients configuration */ clients: IRadiusClient[]; /** VLAN assignment configuration */ vlanAssignment?: IVlanManagerConfig & { /** Static MAC to VLAN mappings */ mappings?: Array>; }; /** Accounting configuration */ accounting?: IAccountingManagerConfig & { /** Whether accounting is enabled */ enabled: boolean; }; } /** * RADIUS authentication result */ export interface IRadiusAuthResult { /** Whether authentication was successful */ success: boolean; /** Reject reason (if not successful) */ rejectReason?: string; /** Reply message to send to client */ replyMessage?: string; /** Session timeout in seconds */ sessionTimeout?: number; /** Idle timeout in seconds */ idleTimeout?: number; /** VLAN to assign */ vlanId?: number; /** Framed IP address to assign */ framedIpAddress?: string; } /** * Authentication request data from RADIUS */ export interface IAuthRequestData { username: string; password?: string; nasIpAddress: string; nasPort?: number; nasPortType?: string; nasIdentifier?: string; calledStationId?: string; callingStationId?: string; serviceType?: string; framedMtu?: number; } /** * RADIUS Server wrapper that provides: * - MAC Authentication Bypass (MAB) for network devices * - VLAN assignment based on MAC address * - Accounting for session tracking and billing * - Integration with SmartProxy routing */ export class RadiusServer { private radiusServer?: plugins.smartradius.RadiusServer; private vlanManager: VlanManager; private accountingManager: AccountingManager; private config: IRadiusServerConfig; private running: boolean = false; // Statistics private stats = { authRequests: 0, authAccepts: 0, authRejects: 0, accountingRequests: 0, startTime: 0, }; constructor(config: IRadiusServerConfig) { this.config = { authPort: config.authPort ?? 1812, acctPort: config.acctPort ?? 1813, bindAddress: config.bindAddress ?? '0.0.0.0', ...config, }; // Initialize VLAN manager this.vlanManager = new VlanManager(config.vlanAssignment); // Initialize accounting manager this.accountingManager = new AccountingManager(config.accounting); } /** * Start the RADIUS server */ async start(): Promise { if (this.running) { logger.log('warn', 'RADIUS server is already running'); return; } logger.log('info', `Starting RADIUS server on ${this.config.bindAddress}:${this.config.authPort} (auth) and ${this.config.acctPort} (acct)`); // Initialize managers await this.vlanManager.initialize(); await this.accountingManager.initialize(); // Import static VLAN mappings if provided if (this.config.vlanAssignment?.mappings) { await this.vlanManager.importMappings(this.config.vlanAssignment.mappings); } const cidrSecrets = this.buildClientSecretsMap(); // Create the RADIUS server this.radiusServer = new plugins.smartradius.RadiusServer({ authPort: this.config.authPort, acctPort: this.config.acctPort, bindAddress: this.config.bindAddress, cidrSecrets, authenticationHandler: this.handleAuthentication.bind(this), accountingHandler: this.handleAccounting.bind(this), }); // Start the server await this.radiusServer.start(); this.running = true; this.stats.startTime = Date.now(); logger.log('info', `RADIUS server started with ${this.config.clients.length} configured clients`); } /** * Stop the RADIUS server */ async stop(): Promise { if (!this.running) { return; } logger.log('info', 'Stopping RADIUS server...'); if (this.radiusServer) { await this.radiusServer.stop(); this.radiusServer = undefined; } this.accountingManager.stop(); this.running = false; logger.log('info', 'RADIUS server stopped'); } /** * Handle authentication request */ private async handleAuthentication( request: plugins.smartradius.IAuthenticationRequest, ): Promise { this.stats.authRequests++; const authData: IAuthRequestData = { username: request.username || '', password: request.password, nasIpAddress: request.nasIpAddress || request.clientAddress || '', nasPort: request.nasPort, nasPortType: request.nasPortType !== undefined ? String(request.nasPortType) : undefined, nasIdentifier: request.nasIdentifier, calledStationId: request.calledStationId, callingStationId: request.callingStationId, serviceType: request.serviceType !== undefined ? String(request.serviceType) : undefined, framedMtu: request.framedMtu, }; logger.log('debug', `RADIUS Auth Request: user=${authData.username}, NAS=${authData.nasIpAddress}`); // Perform MAC Authentication Bypass (MAB) // In MAB, the username is typically the MAC address const result = await this.performMabAuthentication(authData); if (result.success) { this.stats.authAccepts++; logger.log('info', `RADIUS Auth Accept: user=${authData.username}, VLAN=${result.vlanId}`); // Build response with VLAN attributes const response: plugins.smartradius.IAuthenticationResponse = { code: plugins.smartradius.ERadiusCode.AccessAccept, replyMessage: result.replyMessage, }; // Add VLAN attributes if assigned if (result.vlanId !== undefined) { response.tunnelType = plugins.smartradius.ETunnelType.Vlan; response.tunnelMediumType = plugins.smartradius.ETunnelMediumType.Ieee802; response.tunnelPrivateGroupId = String(result.vlanId); } // Add session timeout if specified if (result.sessionTimeout) { response.sessionTimeout = result.sessionTimeout; } // Add idle timeout if specified if (result.idleTimeout) { response.idleTimeout = result.idleTimeout; } // Add framed IP if specified if (result.framedIpAddress) { response.framedIpAddress = result.framedIpAddress; } return response; } else { this.stats.authRejects++; logger.log('warn', `RADIUS Auth Reject: user=${authData.username}, reason=${result.rejectReason}`); return { code: plugins.smartradius.ERadiusCode.AccessReject, replyMessage: result.rejectReason || 'Access Denied', }; } } /** * Handle accounting request */ private async handleAccounting( request: plugins.smartradius.IAccountingRequest, ): Promise { this.stats.accountingRequests++; if (!this.config.accounting?.enabled) { // Still respond even if not tracking return { success: true }; } const statusType = request.statusType; const sessionId = request.sessionId || ''; const accountingData = { sessionId, username: request.username || '', macAddress: request.callingStationId, nasIpAddress: request.nasIpAddress || request.clientAddress || '', nasPort: request.nasPort, nasPortType: request.nasPortType !== undefined ? String(request.nasPortType) : undefined, nasIdentifier: request.nasIdentifier, calledStationId: request.calledStationId, callingStationId: request.callingStationId, inputOctets: request.inputOctets, outputOctets: request.outputOctets, inputPackets: request.inputPackets, outputPackets: request.outputPackets, sessionTime: request.sessionTime, terminateCause: request.terminateCause !== undefined ? String(request.terminateCause) : undefined, framedIpAddress: request.framedIpAddress, serviceType: request.serviceType !== undefined ? String(request.serviceType) : undefined, }; try { switch (statusType) { case plugins.smartradius.EAcctStatusType.Start: logger.log('debug', `RADIUS Acct Start: session=${sessionId}, user=${accountingData.username}`); await this.accountingManager.handleAccountingStart(accountingData); break; case plugins.smartradius.EAcctStatusType.Stop: logger.log('debug', `RADIUS Acct Stop: session=${sessionId}`); await this.accountingManager.handleAccountingStop(accountingData); break; case plugins.smartradius.EAcctStatusType.InterimUpdate: logger.log('debug', `RADIUS Acct Interim: session=${sessionId}`); await this.accountingManager.handleAccountingUpdate(accountingData); break; default: logger.log('debug', `RADIUS Acct Unknown status type: ${statusType}`); } } catch (error: unknown) { logger.log('error', `RADIUS accounting error: ${(error as Error).message}`); } return { success: true }; } /** * Perform MAC Authentication Bypass */ private async performMabAuthentication(data: IAuthRequestData): Promise { // Extract MAC address from username or CallingStationId const macAddress = this.extractMacAddress(data); if (!macAddress) { return { success: false, rejectReason: 'No MAC address found', }; } // Look up VLAN assignment const vlanResult = this.vlanManager.assignVlan(macAddress); if (!vlanResult.assigned) { return { success: false, rejectReason: 'Unknown MAC address', }; } // Build successful result const result: IRadiusAuthResult = { success: true, vlanId: vlanResult.vlan, replyMessage: vlanResult.isDefault ? `Assigned to default VLAN ${vlanResult.vlan}` : `Assigned to VLAN ${vlanResult.vlan}`, }; // Apply any additional settings from the matched rule if (vlanResult.matchedRule) { // Future: Add session timeout, idle timeout, etc. from rule } return result; } /** * Extract MAC address from authentication data */ private extractMacAddress(data: IAuthRequestData): string | null { // Try CallingStationId first (most common for MAB) if (data.callingStationId) { return this.normalizeMac(data.callingStationId); } // Try username (often MAC address in MAB) if (data.username && this.looksLikeMac(data.username)) { return this.normalizeMac(data.username); } return null; } /** * Check if a string looks like a MAC address */ private looksLikeMac(value: string): boolean { // Remove common separators and check length const cleaned = value.replace(/[-:. ]/g, ''); return /^[0-9a-fA-F]{12}$/.test(cleaned); } /** * Normalize MAC address format */ private normalizeMac(mac: string): string { return this.vlanManager.normalizeMac(mac); } /** * Build client secrets map from configuration */ private buildClientSecretsMap(): Record { const cidrSecrets: Record = {}; for (const client of this.config.clients) { if (!client.enabled) { continue; } cidrSecrets[client.ipRange] = client.secret; } return cidrSecrets; } /** * Add a RADIUS client */ async addClient(client: IRadiusClient): Promise { // Check if client already exists const existingIndex = this.config.clients.findIndex(c => c.name === client.name); const previousClient = existingIndex >= 0 ? this.config.clients[existingIndex] : undefined; if (existingIndex >= 0) { this.config.clients[existingIndex] = client; } else { this.config.clients.push(client); } if (this.running && this.radiusServer) { if (previousClient) { this.radiusServer.removeNetworkSecret(previousClient.ipRange); } if (client.enabled) { this.radiusServer.setNetworkSecret(client.ipRange, client.secret); } } logger.log('info', `RADIUS client ${client.enabled ? 'added' : 'disabled'}: ${client.name} (${client.ipRange})`); } /** * Remove a RADIUS client */ removeClient(name: string): boolean { const index = this.config.clients.findIndex(c => c.name === name); if (index >= 0) { const client = this.config.clients[index]; this.config.clients.splice(index, 1); if (this.radiusServer) { this.radiusServer.removeNetworkSecret(client.ipRange); } logger.log('info', `RADIUS client removed: ${name}`); return true; } return false; } /** * Get configured clients */ getClients(): IRadiusClient[] { return [...this.config.clients]; } /** * Get VLAN manager for direct access to VLAN operations */ getVlanManager(): VlanManager { return this.vlanManager; } /** * Get accounting manager for direct access to accounting operations */ getAccountingManager(): AccountingManager { return this.accountingManager; } /** * Get server statistics */ getStats(): { running: boolean; uptime: number; authRequests: number; authAccepts: number; authRejects: number; accountingRequests: number; activeSessions: number; vlanMappings: number; clients: number; } { return { running: this.running, uptime: this.running ? Date.now() - this.stats.startTime : 0, authRequests: this.stats.authRequests, authAccepts: this.stats.authAccepts, authRejects: this.stats.authRejects, accountingRequests: this.stats.accountingRequests, activeSessions: this.accountingManager.getStats().activeSessions, vlanMappings: this.vlanManager.getStats().totalMappings, clients: this.config.clients.filter(c => c.enabled).length, }; } /** * Check if server is running */ isRunning(): boolean { return this.running; } }