import * as plugins from '../plugins.js'; import { logger } from '../logger.js'; import type { StorageManager } from '../storage/index.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 storageManager?: StorageManager; private clientSecrets: Map = new Map(); private running: boolean = false; // Statistics private stats = { authRequests: 0, authAccepts: 0, authRejects: 0, accountingRequests: 0, startTime: 0, }; constructor(config: IRadiusServerConfig, storageManager?: StorageManager) { this.config = { authPort: config.authPort ?? 1812, acctPort: config.acctPort ?? 1813, bindAddress: config.bindAddress ?? '0.0.0.0', ...config, }; this.storageManager = storageManager; // Initialize VLAN manager this.vlanManager = new VlanManager(config.vlanAssignment, storageManager); // Initialize accounting manager this.accountingManager = new AccountingManager(config.accounting, storageManager); } /** * 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); } // Build client secrets map this.buildClientSecretsMap(); // Create the RADIUS server this.radiusServer = new plugins.smartradius.RadiusServer({ authPort: this.config.authPort, acctPort: this.config.acctPort, bindAddress: this.config.bindAddress, defaultSecret: this.getDefaultSecret(), authenticationHandler: this.handleAuthentication.bind(this), accountingHandler: this.handleAccounting.bind(this), }); // Configure per-client secrets for (const [ip, secret] of this.clientSecrets) { this.radiusServer.setClientSecret(ip, secret); } // 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.running = false; logger.log('info', 'RADIUS server stopped'); } /** * Handle authentication request */ private async handleAuthentication(request: any): Promise { this.stats.authRequests++; const authData: IAuthRequestData = { username: request.attributes?.UserName || '', password: request.attributes?.UserPassword, nasIpAddress: request.attributes?.NasIpAddress || request.source?.address || '', nasPort: request.attributes?.NasPort, nasPortType: request.attributes?.NasPortType, nasIdentifier: request.attributes?.NasIdentifier, calledStationId: request.attributes?.CalledStationId, callingStationId: request.attributes?.CallingStationId, serviceType: request.attributes?.ServiceType, }; 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: any = { code: plugins.smartradius.ERadiusCode.AccessAccept, replyMessage: result.replyMessage, }; // Add VLAN attributes if assigned if (result.vlanId !== undefined) { response.tunnelType = 13; // VLAN response.tunnelMediumType = 6; // IEEE 802 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: any): Promise { this.stats.accountingRequests++; if (!this.config.accounting?.enabled) { // Still respond even if not tracking return { code: plugins.smartradius.ERadiusCode.AccountingResponse }; } const statusType = request.attributes?.AcctStatusType; const sessionId = request.attributes?.AcctSessionId || ''; const accountingData = { sessionId, username: request.attributes?.UserName || '', macAddress: request.attributes?.CallingStationId, nasIpAddress: request.attributes?.NasIpAddress || request.source?.address || '', nasPort: request.attributes?.NasPort, nasPortType: request.attributes?.NasPortType, nasIdentifier: request.attributes?.NasIdentifier, calledStationId: request.attributes?.CalledStationId, callingStationId: request.attributes?.CallingStationId, inputOctets: request.attributes?.AcctInputOctets, outputOctets: request.attributes?.AcctOutputOctets, inputPackets: request.attributes?.AcctInputPackets, outputPackets: request.attributes?.AcctOutputPackets, sessionTime: request.attributes?.AcctSessionTime, terminateCause: request.attributes?.AcctTerminateCause, serviceType: request.attributes?.ServiceType, }; 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) { logger.log('error', `RADIUS accounting error: ${error.message}`); } return { code: plugins.smartradius.ERadiusCode.AccountingResponse }; } /** * 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(): void { this.clientSecrets.clear(); for (const client of this.config.clients) { if (!client.enabled) { continue; } // Handle CIDR ranges if (client.ipRange.includes('/')) { // For CIDR ranges, we'll use the network address as key // In practice, smartradius may handle this differently const [network] = client.ipRange.split('/'); this.clientSecrets.set(network, client.secret); } else { this.clientSecrets.set(client.ipRange, client.secret); } } } /** * Get default secret for unknown clients */ private getDefaultSecret(): string { // Use first enabled client's secret as default, or a random one for (const client of this.config.clients) { if (client.enabled) { return client.secret; } } return plugins.crypto.randomBytes(16).toString('hex'); } /** * 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); if (existingIndex >= 0) { this.config.clients[existingIndex] = client; } else { this.config.clients.push(client); } // Update client secrets if running if (this.running && this.radiusServer && client.enabled) { if (client.ipRange.includes('/')) { const [network] = client.ipRange.split('/'); this.radiusServer.setClientSecret(network, client.secret); this.clientSecrets.set(network, client.secret); } else { this.radiusServer.setClientSecret(client.ipRange, client.secret); this.clientSecrets.set(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); // Remove from secrets map if (client.ipRange.includes('/')) { const [network] = client.ipRange.split('/'); this.clientSecrets.delete(network); } else { this.clientSecrets.delete(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; } }