feat(radius): add RADIUS server with MAC authentication (MAB), VLAN assignment, accounting and OpsServer API handlers
This commit is contained in:
12
changelog.md
12
changelog.md
@@ -1,5 +1,17 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-01 - 2.13.0 - feat(radius)
|
||||||
|
add RADIUS server with MAC authentication (MAB), VLAN assignment, accounting and OpsServer API handlers
|
||||||
|
|
||||||
|
- Introduce full RADIUS module under ts/radius: classes.radius.server, classes.vlan.manager, classes.accounting.manager (authentication, VLAN mapping, OUI patterns, accounting, persistence).
|
||||||
|
- Integrate RADIUS into DcRouter: add radiusConfig option, setupRadiusServer(), updateRadiusConfig(), start/stop lifecycle handling and startup summary output.
|
||||||
|
- Add OpsServer RadiusHandler (ts/opsserver/handlers/radius.handler.ts) exposing TypedRequest endpoints for client management, VLAN mappings, accounting reports and statistics.
|
||||||
|
- Add typed request interfaces for RADIUS under ts_interfaces/requests/radius.ts and export them from the requests index.
|
||||||
|
- Wire smartradius into plugins (ts/plugins.ts) and export the new module; export RADIUS from ts/index.ts and re-export RADIUS types from classes.dcrouter.
|
||||||
|
- Update package.json & npmextra.json: add tswatch script and dev watcher configuration, add @push.rocks/smartradius dependency and a test_watch/devserver.ts dev server entrypoint.
|
||||||
|
- Refactor several web UI components (ops-dashboard, ops-view-*) to use 'accessor' for @state properties (small UI state API adjustments).
|
||||||
|
- Documentation: update readme.hints.md with RADIUS integration notes and examples.
|
||||||
|
|
||||||
## 2026-02-01 - 2.12.6 - fix(tests)
|
## 2026-02-01 - 2.12.6 - fix(tests)
|
||||||
update tests and test helpers to current email/DNS APIs, use non-privileged ports, and improve robustness and resilience
|
update tests and test helpers to current email/DNS APIs, use non-privileged ports, and improve robustness and resilience
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
{
|
{
|
||||||
|
"@git.zone/tswatch": {
|
||||||
|
"watchers": [
|
||||||
|
{
|
||||||
|
"name": "dcrouter-dev",
|
||||||
|
"watch": ["ts/**/*.ts", "ts_*/**/*.ts", "test_watch/devserver.ts"],
|
||||||
|
"command": "pnpm run build && tsrun test_watch/devserver.ts",
|
||||||
|
"restart": true,
|
||||||
|
"debounce": 500,
|
||||||
|
"runOnStart": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"@git.zone/tsbundle": {
|
"@git.zone/tsbundle": {
|
||||||
"bundles": [
|
"bundles": [
|
||||||
{
|
{
|
||||||
|
|||||||
11
package.json
11
package.json
@@ -13,7 +13,8 @@
|
|||||||
"start": "(node --max_old_space_size=250 ./cli.js)",
|
"start": "(node --max_old_space_size=250 ./cli.js)",
|
||||||
"startTs": "(node cli.ts.js)",
|
"startTs": "(node cli.ts.js)",
|
||||||
"build": "(tsbuild tsfolders --allowimplicitany && npm run bundle)",
|
"build": "(tsbuild tsfolders --allowimplicitany && npm run bundle)",
|
||||||
"bundle": "(tsbundle)"
|
"bundle": "(tsbundle)",
|
||||||
|
"watch": "tswatch"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^4.1.2",
|
"@git.zone/tsbuild": "^4.1.2",
|
||||||
@@ -47,6 +48,7 @@
|
|||||||
"@push.rocks/smartpath": "^5.0.5",
|
"@push.rocks/smartpath": "^5.0.5",
|
||||||
"@push.rocks/smartpromise": "^4.0.3",
|
"@push.rocks/smartpromise": "^4.0.3",
|
||||||
"@push.rocks/smartproxy": "^19.6.15",
|
"@push.rocks/smartproxy": "^19.6.15",
|
||||||
|
"@push.rocks/smartradius": "^1.0.3",
|
||||||
"@push.rocks/smartrequest": "^2.1.0",
|
"@push.rocks/smartrequest": "^2.1.0",
|
||||||
"@push.rocks/smartrule": "^2.0.1",
|
"@push.rocks/smartrule": "^2.0.1",
|
||||||
"@push.rocks/smartrx": "^3.0.10",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
@@ -80,7 +82,12 @@
|
|||||||
"email templating",
|
"email templating",
|
||||||
"rule management",
|
"rule management",
|
||||||
"SMTP STARTTLS",
|
"SMTP STARTTLS",
|
||||||
"DNS management"
|
"DNS management",
|
||||||
|
"RADIUS",
|
||||||
|
"AAA",
|
||||||
|
"network authentication",
|
||||||
|
"VLAN assignment",
|
||||||
|
"MAC authentication"
|
||||||
],
|
],
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
|
|||||||
11
pnpm-lock.yaml
generated
11
pnpm-lock.yaml
generated
@@ -74,6 +74,9 @@ importers:
|
|||||||
'@push.rocks/smartproxy':
|
'@push.rocks/smartproxy':
|
||||||
specifier: ^19.6.15
|
specifier: ^19.6.15
|
||||||
version: 19.6.17(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
version: 19.6.17(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
||||||
|
'@push.rocks/smartradius':
|
||||||
|
specifier: ^1.0.3
|
||||||
|
version: 1.1.0
|
||||||
'@push.rocks/smartrequest':
|
'@push.rocks/smartrequest':
|
||||||
specifier: ^2.1.0
|
specifier: ^2.1.0
|
||||||
version: 2.1.0
|
version: 2.1.0
|
||||||
@@ -1069,6 +1072,9 @@ packages:
|
|||||||
'@push.rocks/smartpuppeteer@2.0.5':
|
'@push.rocks/smartpuppeteer@2.0.5':
|
||||||
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
||||||
|
|
||||||
|
'@push.rocks/smartradius@1.1.0':
|
||||||
|
resolution: {integrity: sha512-eocddp/bDcB5a/JOt5lezz0uBWezOKpnDQgMx+I4bl8eJ20KIWh0B6PhYuKYjGuDwo/t01p+s+m0gG7IgyPmzQ==}
|
||||||
|
|
||||||
'@push.rocks/smartrequest@2.1.0':
|
'@push.rocks/smartrequest@2.1.0':
|
||||||
resolution: {integrity: sha512-3eHLTRInHA+u+W98TqJwgTES7rRimBAsJC4JxVNQC3UUezmblAhM5/TIQsEBQTsbjAY8SeQKy6NHzW6iTiaD8w==}
|
resolution: {integrity: sha512-3eHLTRInHA+u+W98TqJwgTES7rRimBAsJC4JxVNQC3UUezmblAhM5/TIQsEBQTsbjAY8SeQKy6NHzW6iTiaD8w==}
|
||||||
|
|
||||||
@@ -6502,6 +6508,11 @@ snapshots:
|
|||||||
- typescript
|
- typescript
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
|
'@push.rocks/smartradius@1.1.0':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
|
||||||
'@push.rocks/smartrequest@2.1.0':
|
'@push.rocks/smartrequest@2.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
|||||||
@@ -1,5 +1,62 @@
|
|||||||
# Implementation Hints and Learnings
|
# Implementation Hints and Learnings
|
||||||
|
|
||||||
|
## RADIUS Server Integration (2026-02-01)
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
DcRouter now supports RADIUS server functionality for network authentication via `@push.rocks/smartradius`.
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
- **MAC Authentication Bypass (MAB)** - Authenticate network devices based on MAC address
|
||||||
|
- **VLAN Assignment** - Assign VLANs based on MAC address or OUI patterns
|
||||||
|
- **RADIUS Accounting** - Track sessions, data usage, and billing
|
||||||
|
|
||||||
|
### Configuration Example
|
||||||
|
```typescript
|
||||||
|
const dcRouter = new DcRouter({
|
||||||
|
radiusConfig: {
|
||||||
|
authPort: 1812, // Authentication port (default)
|
||||||
|
acctPort: 1813, // Accounting port (default)
|
||||||
|
clients: [
|
||||||
|
{
|
||||||
|
name: 'switch-1',
|
||||||
|
ipRange: '192.168.1.0/24',
|
||||||
|
secret: 'shared-secret',
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
vlanAssignment: {
|
||||||
|
defaultVlan: 100, // VLAN for unknown MACs
|
||||||
|
allowUnknownMacs: true,
|
||||||
|
mappings: [
|
||||||
|
{ mac: '00:11:22:33:44:55', vlan: 10, enabled: true },
|
||||||
|
{ mac: '00:11:22', vlan: 20, enabled: true } // OUI pattern
|
||||||
|
]
|
||||||
|
},
|
||||||
|
accounting: {
|
||||||
|
enabled: true,
|
||||||
|
retentionDays: 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Components
|
||||||
|
- `RadiusServer` - Main server wrapping smartradius
|
||||||
|
- `VlanManager` - MAC-to-VLAN mapping with OUI pattern support
|
||||||
|
- `AccountingManager` - Session tracking and billing data
|
||||||
|
|
||||||
|
### OpsServer API Endpoints
|
||||||
|
- `getRadiusClients` / `setRadiusClient` / `removeRadiusClient` - Client management
|
||||||
|
- `getVlanMappings` / `setVlanMapping` / `removeVlanMapping` - VLAN mappings
|
||||||
|
- `testVlanAssignment` - Test what VLAN a MAC would get
|
||||||
|
- `getRadiusSessions` / `disconnectRadiusSession` - Session management
|
||||||
|
- `getRadiusStatistics` / `getRadiusAccountingSummary` - Statistics
|
||||||
|
|
||||||
|
### Files
|
||||||
|
- `ts/radius/` - RADIUS module
|
||||||
|
- `ts/opsserver/handlers/radius.handler.ts` - OpsServer handler
|
||||||
|
- `ts_interfaces/requests/radius.ts` - TypedRequest interfaces
|
||||||
|
|
||||||
## Test Fix: test.dcrouter.email.ts (2026-02-01)
|
## Test Fix: test.dcrouter.email.ts (2026-02-01)
|
||||||
|
|
||||||
### Issue
|
### Issue
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
# Metrics Implementation Plan with @push.rocks/smartmetrics
|
|
||||||
|
|
||||||
Command to reread CLAUDE.md: `cat /home/centraluser/eu.central.ingress-2/CLAUDE.md`
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
This plan outlines the migration from placeholder/demo metrics to real metrics using @push.rocks/smartmetrics for the dcrouter project.
|
|
||||||
|
|
||||||
## Current State Analysis
|
|
||||||
|
|
||||||
### Currently Implemented (Real Data)
|
|
||||||
- CPU usage (basic calculation from os.loadavg)
|
|
||||||
- Memory usage (from process.memoryUsage)
|
|
||||||
- System uptime
|
|
||||||
|
|
||||||
### Currently Stubbed (Returns 0 or Demo Data)
|
|
||||||
- Active connections (HTTP/HTTPS/WebSocket)
|
|
||||||
- Total connections
|
|
||||||
- Requests per second
|
|
||||||
- Email statistics (sent/received/failed/queued/bounce rate)
|
|
||||||
- DNS statistics (queries/cache hits/response times)
|
|
||||||
- Security metrics (blocked IPs/auth failures/spam detection)
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
### Phase 1: Core Infrastructure Setup
|
|
||||||
|
|
||||||
1. **Install Dependencies**
|
|
||||||
```bash
|
|
||||||
pnpm install --save @push.rocks/smartmetrics
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Update plugins.ts**
|
|
||||||
- Add smartmetrics to ts/plugins.ts
|
|
||||||
- Import as: `import * as smartmetrics from '@push.rocks/smartmetrics';`
|
|
||||||
|
|
||||||
3. **Create Metrics Manager Class**
|
|
||||||
- Location: `ts/monitoring/classes.metricsmanager.ts`
|
|
||||||
- Initialize SmartMetrics with existing logger
|
|
||||||
- Configure for dcrouter service identification
|
|
||||||
- Set up automatic metric collection intervals
|
|
||||||
|
|
||||||
### Phase 2: Connection Tracking Implementation
|
|
||||||
|
|
||||||
1. **HTTP/HTTPS Connection Tracking**
|
|
||||||
- Instrument the SmartProxy connection handlers
|
|
||||||
- Track active connections in real-time
|
|
||||||
- Monitor connection lifecycle (open/close events)
|
|
||||||
- Location: Update connection managers in routing system
|
|
||||||
|
|
||||||
2. **Email Connection Tracking**
|
|
||||||
- Instrument SMTP server connection handlers
|
|
||||||
- Track both incoming and outgoing connections
|
|
||||||
- Location: `ts/mail/delivery/smtpserver/connection-manager.ts`
|
|
||||||
|
|
||||||
3. **DNS Query Tracking**
|
|
||||||
- Instrument DNS server handlers
|
|
||||||
- Track query counts and response times
|
|
||||||
- Location: `ts/mail/routing/classes.dns.manager.ts`
|
|
||||||
|
|
||||||
### Phase 3: Email Metrics Collection
|
|
||||||
|
|
||||||
1. **Email Processing Metrics**
|
|
||||||
- Track sent/received/failed emails
|
|
||||||
- Monitor queue sizes
|
|
||||||
- Calculate delivery and bounce rates
|
|
||||||
- Location: Instrument `classes.delivery.queue.ts` and `classes.emailsendjob.ts`
|
|
||||||
|
|
||||||
2. **Email Performance Metrics**
|
|
||||||
- Track processing times
|
|
||||||
- Monitor queue throughput
|
|
||||||
- Location: Update delivery system classes
|
|
||||||
|
|
||||||
### Phase 4: Security Metrics Integration
|
|
||||||
|
|
||||||
1. **Security Event Tracking**
|
|
||||||
- Track blocked IPs from IPReputationChecker
|
|
||||||
- Monitor authentication failures
|
|
||||||
- Count spam/malware/phishing detections
|
|
||||||
- Location: Instrument security classes in `ts/security/`
|
|
||||||
|
|
||||||
### Phase 5: Stats Handler Refactoring
|
|
||||||
|
|
||||||
1. **Update Stats Handler**
|
|
||||||
- Location: `ts/opsserver/handlers/stats.handler.ts`
|
|
||||||
- Replace all stub implementations with MetricsManager calls
|
|
||||||
- Maintain existing API interface structure
|
|
||||||
|
|
||||||
2. **Metrics Aggregation**
|
|
||||||
- Implement proper time-window aggregations
|
|
||||||
- Add historical data storage (last hour/day)
|
|
||||||
- Calculate rates and percentages accurately
|
|
||||||
|
|
||||||
### Phase 6: Prometheus Integration (Optional Enhancement)
|
|
||||||
|
|
||||||
1. **Enable Prometheus Endpoint**
|
|
||||||
- Add Prometheus metrics endpoint
|
|
||||||
- Configure port (default: 9090)
|
|
||||||
- Document metrics for monitoring systems
|
|
||||||
|
|
||||||
## Implementation Details
|
|
||||||
|
|
||||||
### MetricsManager Core Structure
|
|
||||||
```typescript
|
|
||||||
export class MetricsManager {
|
|
||||||
private smartMetrics: smartmetrics.SmartMetrics;
|
|
||||||
private connectionTrackers: Map<string, ConnectionTracker>;
|
|
||||||
private emailMetrics: EmailMetricsCollector;
|
|
||||||
private dnsMetrics: DnsMetricsCollector;
|
|
||||||
private securityMetrics: SecurityMetricsCollector;
|
|
||||||
|
|
||||||
// Real-time counters
|
|
||||||
private activeConnections = {
|
|
||||||
http: 0,
|
|
||||||
https: 0,
|
|
||||||
websocket: 0,
|
|
||||||
smtp: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize and start collection
|
|
||||||
public async start(): Promise<void>;
|
|
||||||
|
|
||||||
// Get aggregated metrics for stats handler
|
|
||||||
public async getServerStats(): Promise<IServerStats>;
|
|
||||||
public async getEmailStats(): Promise<IEmailStats>;
|
|
||||||
public async getDnsStats(): Promise<IDnsStats>;
|
|
||||||
public async getSecurityStats(): Promise<ISecurityStats>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Connection Tracking Pattern
|
|
||||||
```typescript
|
|
||||||
// Example for HTTP connections
|
|
||||||
onConnectionOpen(type: string) {
|
|
||||||
this.activeConnections[type]++;
|
|
||||||
this.totalConnections[type]++;
|
|
||||||
}
|
|
||||||
|
|
||||||
onConnectionClose(type: string) {
|
|
||||||
this.activeConnections[type]--;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Email Metrics Pattern
|
|
||||||
```typescript
|
|
||||||
// Track email events
|
|
||||||
onEmailSent() { this.emailsSentToday++; }
|
|
||||||
onEmailReceived() { this.emailsReceivedToday++; }
|
|
||||||
onEmailFailed() { this.emailsFailedToday++; }
|
|
||||||
onEmailQueued() { this.queueSize++; }
|
|
||||||
onEmailDequeued() { this.queueSize--; }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
1. **Unit Tests**
|
|
||||||
- Test MetricsManager initialization
|
|
||||||
- Test metric collection accuracy
|
|
||||||
- Test aggregation calculations
|
|
||||||
|
|
||||||
2. **Integration Tests**
|
|
||||||
- Test metrics flow from source to API
|
|
||||||
- Verify real-time updates
|
|
||||||
- Test under load conditions
|
|
||||||
|
|
||||||
3. **Debug Utilities**
|
|
||||||
- Create `.nogit/debug/test-metrics.ts` for quick testing
|
|
||||||
- Add metrics dump endpoint for debugging
|
|
||||||
|
|
||||||
## Migration Steps
|
|
||||||
|
|
||||||
1. Implement MetricsManager without breaking existing code
|
|
||||||
2. Wire up one metric type at a time
|
|
||||||
3. Verify each metric shows real data
|
|
||||||
4. Remove TODO comments from stats handler
|
|
||||||
5. Update tests to expect real values
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
|
|
||||||
- [ ] All metrics show real, accurate data
|
|
||||||
- [ ] No performance degradation
|
|
||||||
- [ ] Metrics update in real-time
|
|
||||||
- [ ] Historical data is collected
|
|
||||||
- [ ] All TODO comments removed from stats handler
|
|
||||||
- [ ] Tests pass with real metric values
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- SmartMetrics provides CPU and memory metrics out of the box
|
|
||||||
- We'll need custom collectors for application-specific metrics
|
|
||||||
- Consider adding metric persistence for historical data
|
|
||||||
- Prometheus integration provides industry-standard monitoring
|
|
||||||
|
|
||||||
## Questions to Address
|
|
||||||
|
|
||||||
1. Should we persist metrics to disk for historical analysis?
|
|
||||||
2. What time windows should we support (5min, 1hour, 1day)?
|
|
||||||
3. Should we add alerting thresholds?
|
|
||||||
4. Do we need custom metric types beyond the current interface?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
This plan ensures a systematic migration from demo metrics to real, actionable data using @push.rocks/smartmetrics while maintaining the existing API structure and adding powerful monitoring capabilities.
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
# Module Adjustments for Metrics Collection
|
|
||||||
|
|
||||||
Command to reread CLAUDE.md: `cat /home/centraluser/eu.central.ingress-2/CLAUDE.md`
|
|
||||||
|
|
||||||
## SmartProxy Adjustments
|
|
||||||
|
|
||||||
### Current State
|
|
||||||
SmartProxy (@push.rocks/smartproxy) provides:
|
|
||||||
- Route-level `maxConnections` limiting
|
|
||||||
- Event emission system (currently only for certificates)
|
|
||||||
- NFTables integration with packet statistics
|
|
||||||
- Connection monitoring during active sessions
|
|
||||||
|
|
||||||
### Missing Capabilities for Metrics
|
|
||||||
1. **No Connection Lifecycle Events**
|
|
||||||
- No `connection-open` or `connection-close` events
|
|
||||||
- No way to track active connections in real-time
|
|
||||||
- No exposure of internal connection tracking
|
|
||||||
|
|
||||||
2. **No Statistics API**
|
|
||||||
- No methods like `getActiveConnections()` or `getConnectionStats()`
|
|
||||||
- No access to connection counts per route
|
|
||||||
- No throughput or performance metrics exposed
|
|
||||||
|
|
||||||
3. **Limited Event System**
|
|
||||||
- Currently only emits certificate-related events
|
|
||||||
- No connection, request, or performance events
|
|
||||||
|
|
||||||
### Required Adjustments
|
|
||||||
1. **Add Connection Tracking Events**
|
|
||||||
```typescript
|
|
||||||
// Emit on new connection
|
|
||||||
smartProxy.emit('connection-open', {
|
|
||||||
type: 'http' | 'https' | 'websocket',
|
|
||||||
routeName: string,
|
|
||||||
clientIp: string,
|
|
||||||
timestamp: Date
|
|
||||||
});
|
|
||||||
|
|
||||||
// Emit on connection close
|
|
||||||
smartProxy.emit('connection-close', {
|
|
||||||
connectionId: string,
|
|
||||||
duration: number,
|
|
||||||
bytesTransferred: number
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Add Statistics API**
|
|
||||||
```typescript
|
|
||||||
interface IProxyStats {
|
|
||||||
getActiveConnections(): number;
|
|
||||||
getConnectionsByRoute(): Map<string, number>;
|
|
||||||
getTotalConnections(): number;
|
|
||||||
getRequestsPerSecond(): number;
|
|
||||||
getThroughput(): { bytesIn: number, bytesOut: number };
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Expose Internal Metrics**
|
|
||||||
- Make connection pools accessible
|
|
||||||
- Expose route-level statistics
|
|
||||||
- Provide request/response metrics
|
|
||||||
|
|
||||||
### Alternative Approach
|
|
||||||
Since SmartProxy is already used with socket handlers for email routing, we could:
|
|
||||||
1. Wrap all SmartProxy socket handlers with a metrics-aware wrapper
|
|
||||||
2. Use the existing socket-handler pattern to intercept all connections
|
|
||||||
3. Track connections at the dcrouter level rather than modifying SmartProxy
|
|
||||||
|
|
||||||
## SmartDNS Adjustments
|
|
||||||
|
|
||||||
### Current State
|
|
||||||
SmartDNS (@push.rocks/smartdns) provides:
|
|
||||||
- DNS query handling via registered handlers
|
|
||||||
- Support for UDP (port 53) and DNS-over-HTTPS
|
|
||||||
- Domain pattern matching and routing
|
|
||||||
- DNSSEC support
|
|
||||||
|
|
||||||
### Missing Capabilities for Metrics
|
|
||||||
1. **No Query Tracking**
|
|
||||||
- No counters for total queries
|
|
||||||
- No breakdown by query type (A, AAAA, MX, etc.)
|
|
||||||
- No domain popularity tracking
|
|
||||||
|
|
||||||
2. **No Performance Metrics**
|
|
||||||
- No response time tracking
|
|
||||||
- No cache hit/miss statistics
|
|
||||||
- No error rate tracking
|
|
||||||
|
|
||||||
3. **No Event Emission**
|
|
||||||
- No query lifecycle events
|
|
||||||
- No cache events
|
|
||||||
- No error events
|
|
||||||
|
|
||||||
### Required Adjustments
|
|
||||||
1. **Add Query Interceptor/Middleware**
|
|
||||||
```typescript
|
|
||||||
// Wrap handler registration to add metrics
|
|
||||||
smartDns.use((query, next) => {
|
|
||||||
metricsCollector.trackQuery(query);
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
next((response) => {
|
|
||||||
metricsCollector.trackResponse(response, Date.now() - startTime);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Add Event Emissions**
|
|
||||||
```typescript
|
|
||||||
// Query events
|
|
||||||
smartDns.emit('query-received', {
|
|
||||||
type: query.type,
|
|
||||||
domain: query.domain,
|
|
||||||
source: 'udp' | 'https',
|
|
||||||
clientIp: string
|
|
||||||
});
|
|
||||||
|
|
||||||
smartDns.emit('query-answered', {
|
|
||||||
cached: boolean,
|
|
||||||
responseTime: number,
|
|
||||||
responseCode: string
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Add Statistics API**
|
|
||||||
```typescript
|
|
||||||
interface IDnsStats {
|
|
||||||
getTotalQueries(): number;
|
|
||||||
getQueriesPerSecond(): number;
|
|
||||||
getCacheStats(): { hits: number, misses: number, hitRate: number };
|
|
||||||
getTopDomains(limit: number): Array<{ domain: string, count: number }>;
|
|
||||||
getQueryTypeBreakdown(): Record<string, number>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Alternative Approach
|
|
||||||
Since we control the handler registration in dcrouter:
|
|
||||||
1. Create a metrics-aware handler wrapper at the dcrouter level
|
|
||||||
2. Wrap all DNS handlers before registration
|
|
||||||
3. Track metrics without modifying SmartDNS itself
|
|
||||||
|
|
||||||
## Implementation Strategy
|
|
||||||
|
|
||||||
### Option 1: Fork and Modify Dependencies
|
|
||||||
- Fork @push.rocks/smartproxy and @push.rocks/smartdns
|
|
||||||
- Add metrics capabilities directly
|
|
||||||
- Maintain custom versions
|
|
||||||
- **Pros**: Clean integration, full control
|
|
||||||
- **Cons**: Maintenance burden, divergence from upstream
|
|
||||||
|
|
||||||
### Option 2: Wrapper Approach at DcRouter Level
|
|
||||||
- Create wrapper classes that intercept all operations
|
|
||||||
- Track metrics at the application level
|
|
||||||
- No modifications to dependencies
|
|
||||||
- **Pros**: No dependency modifications, easier to maintain
|
|
||||||
- **Cons**: May miss some internal events, slightly higher overhead
|
|
||||||
|
|
||||||
### Option 3: Contribute Back to Upstream
|
|
||||||
- Submit PRs to add metrics capabilities to original packages
|
|
||||||
- Work with maintainers to add event emissions and stats APIs
|
|
||||||
- **Pros**: Benefits everyone, no fork maintenance
|
|
||||||
- **Cons**: Slower process, may not align with maintainer vision
|
|
||||||
|
|
||||||
## Recommendation
|
|
||||||
|
|
||||||
**Use Option 2 (Wrapper Approach)** for immediate implementation:
|
|
||||||
1. Create `MetricsAwareProxy` and `MetricsAwareDns` wrapper classes
|
|
||||||
2. Intercept all operations and track metrics
|
|
||||||
3. Minimal changes to existing codebase
|
|
||||||
4. Can migrate to Option 3 later if upstream accepts contributions
|
|
||||||
|
|
||||||
This approach allows us to implement comprehensive metrics collection without modifying external dependencies, maintaining compatibility and reducing maintenance burden.
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
# Network Metrics Integration Status
|
|
||||||
|
|
||||||
## Command: `pnpm run build && curl https://code.foss.global/push.rocks/smartproxy/raw/branch/master/readme.md`
|
|
||||||
|
|
||||||
## Completed Tasks (2025-06-23)
|
|
||||||
|
|
||||||
### ✅ SmartProxy Metrics API Integration
|
|
||||||
- Updated MetricsManager to use new SmartProxy v19.6.7 metrics API
|
|
||||||
- Replaced deprecated `getStats()` with `getMetrics()` and `getStatistics()`
|
|
||||||
- Fixed method calls to use grouped API structure:
|
|
||||||
- `metrics.connections.active()` for active connections
|
|
||||||
- `metrics.throughput.instant()` for real-time throughput
|
|
||||||
- `metrics.connections.topIPs()` for top connected IPs
|
|
||||||
|
|
||||||
### ✅ Removed Mock Data
|
|
||||||
- Removed hardcoded `0.0.0.0` IPs in security.handler.ts
|
|
||||||
- Removed `Math.random()` trend data in ops-view-network.ts
|
|
||||||
- Now using real IP data from SmartProxy metrics
|
|
||||||
|
|
||||||
### ✅ Enhanced Metrics Functionality
|
|
||||||
- Email metrics: delivery time tracking, top recipients, activity log
|
|
||||||
- DNS metrics: query rate calculations, response time tracking
|
|
||||||
- Security metrics: incident logging with severity levels
|
|
||||||
|
|
||||||
### ✅ Fixed Network Traffic Display
|
|
||||||
- All throughput now shown in bits per second (kbit/s, Mbit/s, Gbit/s)
|
|
||||||
- Network graph shows separate lines for inbound (green) and outbound (purple)
|
|
||||||
- Fixed throughput calculation to use same data source as tiles
|
|
||||||
- Added tooltips showing both timestamp and value
|
|
||||||
|
|
||||||
### ✅ Fixed Requests/sec Tile
|
|
||||||
- Shows actual request counts (derived from connections)
|
|
||||||
- Trend line now shows request history, not throughput
|
|
||||||
- Consistent data between number and trend visualization
|
|
||||||
|
|
||||||
## Current Architecture
|
|
||||||
|
|
||||||
### Data Flow
|
|
||||||
1. SmartProxy collects metrics via its internal MetricsCollector
|
|
||||||
2. MetricsManager retrieves data using `smartProxy.getMetrics()`
|
|
||||||
3. Handlers transform metrics for UI consumption
|
|
||||||
4. UI components display real-time data with auto-refresh
|
|
||||||
|
|
||||||
### Key Components
|
|
||||||
- **MetricsManager**: Central metrics aggregation and tracking
|
|
||||||
- **SmartProxy Integration**: Uses grouped metrics API
|
|
||||||
- **UI Components**: ops-view-network shows real-time traffic graphs
|
|
||||||
- **State Management**: Uses appstate for reactive updates
|
|
||||||
|
|
||||||
## Known Limitations
|
|
||||||
- Request counting is derived from connection data (not true HTTP request counts)
|
|
||||||
- Some metrics still need backend implementation (e.g., per-connection bytes)
|
|
||||||
- Historical data limited to current session
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
```bash
|
|
||||||
# Build and run
|
|
||||||
pnpm build
|
|
||||||
pnpm start
|
|
||||||
|
|
||||||
# Check metrics endpoints
|
|
||||||
curl http://localhost:4000/api/stats/server
|
|
||||||
curl http://localhost:4000/api/stats/network
|
|
||||||
```
|
|
||||||
|
|
||||||
## Success Metrics
|
|
||||||
- [x] Real-time throughput data displayed correctly
|
|
||||||
- [x] No mock data in production UI
|
|
||||||
- [x] Consistent units across all displays
|
|
||||||
- [x] Separate in/out traffic visualization
|
|
||||||
- [x] Working trend lines in stat tiles
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# Plan: Implement dees-statsgrid in DCRouter UI
|
|
||||||
|
|
||||||
Command to reread CLAUDE.md: `Read /home/centraluser/eu.central.ingress-2/CLAUDE.md`
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Replace the current stats cards with the new dees-statsgrid component from @design.estate/dees-catalog for better visualization and consistency.
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
### 1. Update Overview View (`ops-view-overview.ts`)
|
|
||||||
- Replace the custom stats cards with dees-statsgrid
|
|
||||||
- Use appropriate tile types for different metrics:
|
|
||||||
- `gauge` for CPU and Memory usage
|
|
||||||
- `number` for Active Connections, Total Requests, etc.
|
|
||||||
- `trend` for time-series data like requests over time
|
|
||||||
|
|
||||||
### 2. Update Network View (`ops-view-network.ts`)
|
|
||||||
- Replace the current stats cards section with dees-statsgrid
|
|
||||||
- Configure tiles for:
|
|
||||||
- Active Connections (number)
|
|
||||||
- Requests/sec (number with trend)
|
|
||||||
- Throughput In/Out (number with units)
|
|
||||||
- Protocol distribution (percentage)
|
|
||||||
|
|
||||||
### 3. Create Consistent Color Scheme
|
|
||||||
- Success/Normal: #22c55e (green)
|
|
||||||
- Warning: #f59e0b (amber)
|
|
||||||
- Error/Critical: #ef4444 (red)
|
|
||||||
- Info: #3b82f6 (blue)
|
|
||||||
|
|
||||||
### 4. Add Interactive Features
|
|
||||||
- Click actions to show detailed views
|
|
||||||
- Context menu for refresh, export, etc.
|
|
||||||
- Real-time updates from metrics data
|
|
||||||
|
|
||||||
### 5. Integration Points
|
|
||||||
- Connect to existing appstate for data
|
|
||||||
- Use MetricsManager data for real values
|
|
||||||
- Update on the 1-second refresh interval
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
- Consistent UI component usage
|
|
||||||
- Better visual hierarchy
|
|
||||||
- Built-in responsive design
|
|
||||||
- More visualization options (gauges, trends)
|
|
||||||
- Reduced custom CSS maintenance
|
|
||||||
35
test_watch/devserver.ts
Normal file
35
test_watch/devserver.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { DcRouter } from '../ts/index.js';
|
||||||
|
|
||||||
|
const devRouter = new DcRouter({
|
||||||
|
// Configure services as needed for development
|
||||||
|
// OpsServer always starts on port 3000
|
||||||
|
|
||||||
|
// Example: Add SmartProxy routes
|
||||||
|
// smartProxyConfig: {
|
||||||
|
// routes: [...]
|
||||||
|
// },
|
||||||
|
|
||||||
|
// Example: Add email configuration
|
||||||
|
// emailConfig: {
|
||||||
|
// ports: [2525],
|
||||||
|
// hostname: 'localhost',
|
||||||
|
// domains: [],
|
||||||
|
// routes: []
|
||||||
|
// },
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Starting DcRouter in development mode...');
|
||||||
|
|
||||||
|
await devRouter.start();
|
||||||
|
|
||||||
|
// Graceful shutdown handlers
|
||||||
|
const shutdown = async () => {
|
||||||
|
console.log('\nShutting down...');
|
||||||
|
await devRouter.stop();
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGINT', shutdown);
|
||||||
|
process.on('SIGTERM', shutdown);
|
||||||
|
|
||||||
|
console.log('DcRouter dev server running. Press Ctrl+C to stop.');
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '2.12.6',
|
version: '2.13.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { StorageManager, type IStorageConfig } from './storage/index.js';
|
|||||||
|
|
||||||
import { OpsServer } from './opsserver/index.js';
|
import { OpsServer } from './opsserver/index.js';
|
||||||
import { MetricsManager } from './monitoring/index.js';
|
import { MetricsManager } from './monitoring/index.js';
|
||||||
|
import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
|
||||||
|
|
||||||
export interface IDcRouterOptions {
|
export interface IDcRouterOptions {
|
||||||
/**
|
/**
|
||||||
@@ -109,6 +110,12 @@ export interface IDcRouterOptions {
|
|||||||
|
|
||||||
/** Storage configuration */
|
/** Storage configuration */
|
||||||
storage?: IStorageConfig;
|
storage?: IStorageConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RADIUS server configuration for network authentication
|
||||||
|
* Enables MAC Authentication Bypass (MAB) and VLAN assignment
|
||||||
|
*/
|
||||||
|
radiusConfig?: IRadiusServerConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -132,6 +139,7 @@ export class DcRouter {
|
|||||||
public smartProxy?: plugins.smartproxy.SmartProxy;
|
public smartProxy?: plugins.smartproxy.SmartProxy;
|
||||||
public dnsServer?: plugins.smartdns.dnsServerMod.DnsServer;
|
public dnsServer?: plugins.smartdns.dnsServerMod.DnsServer;
|
||||||
public emailServer?: UnifiedEmailServer;
|
public emailServer?: UnifiedEmailServer;
|
||||||
|
public radiusServer?: RadiusServer;
|
||||||
public storageManager: StorageManager;
|
public storageManager: StorageManager;
|
||||||
public opsServer: OpsServer;
|
public opsServer: OpsServer;
|
||||||
public metricsManager?: MetricsManager;
|
public metricsManager?: MetricsManager;
|
||||||
@@ -181,11 +189,16 @@ export class DcRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set up DNS server if configured with nameservers and scopes
|
// Set up DNS server if configured with nameservers and scopes
|
||||||
if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0 &&
|
if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0 &&
|
||||||
this.options.dnsScopes && this.options.dnsScopes.length > 0) {
|
this.options.dnsScopes && this.options.dnsScopes.length > 0) {
|
||||||
await this.setupDnsWithSocketHandler();
|
await this.setupDnsWithSocketHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up RADIUS server if configured
|
||||||
|
if (this.options.radiusConfig) {
|
||||||
|
await this.setupRadiusServer();
|
||||||
|
}
|
||||||
|
|
||||||
this.logStartupSummary();
|
this.logStartupSummary();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error starting DcRouter:', error);
|
console.error('❌ Error starting DcRouter:', error);
|
||||||
@@ -261,12 +274,23 @@ export class DcRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RADIUS service summary
|
||||||
|
if (this.radiusServer && this.options.radiusConfig) {
|
||||||
|
console.log('\n🔐 RADIUS Service:');
|
||||||
|
console.log(` ├─ Auth Port: ${this.options.radiusConfig.authPort || 1812}`);
|
||||||
|
console.log(` ├─ Acct Port: ${this.options.radiusConfig.acctPort || 1813}`);
|
||||||
|
console.log(` ├─ Clients configured: ${this.options.radiusConfig.clients?.length || 0}`);
|
||||||
|
const vlanStats = this.radiusServer.getVlanManager().getStats();
|
||||||
|
console.log(` ├─ VLAN mappings: ${vlanStats.totalMappings}`);
|
||||||
|
console.log(` └─ Accounting: ${this.options.radiusConfig.accounting?.enabled ? 'Enabled' : 'Disabled'}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Storage summary
|
// Storage summary
|
||||||
if (this.storageManager && this.options.storage) {
|
if (this.storageManager && this.options.storage) {
|
||||||
console.log('\n💾 Storage:');
|
console.log('\n💾 Storage:');
|
||||||
console.log(` └─ Path: ${this.options.storage.fsPath || 'default'}`);
|
console.log(` └─ Path: ${this.options.storage.fsPath || 'default'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('\n✅ All services are running\n');
|
console.log('\n✅ All services are running\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,16 +606,21 @@ export class DcRouter {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
// Stop metrics manager if running
|
// Stop metrics manager if running
|
||||||
this.metricsManager ? this.metricsManager.stop().catch(err => console.error('Error stopping MetricsManager:', err)) : Promise.resolve(),
|
this.metricsManager ? this.metricsManager.stop().catch(err => console.error('Error stopping MetricsManager:', err)) : Promise.resolve(),
|
||||||
|
|
||||||
// Stop unified email server if running
|
// Stop unified email server if running
|
||||||
this.emailServer ? this.emailServer.stop().catch(err => console.error('Error stopping email server:', err)) : Promise.resolve(),
|
this.emailServer ? this.emailServer.stop().catch(err => console.error('Error stopping email server:', err)) : Promise.resolve(),
|
||||||
|
|
||||||
// Stop HTTP SmartProxy if running
|
// Stop HTTP SmartProxy if running
|
||||||
this.smartProxy ? this.smartProxy.stop().catch(err => console.error('Error stopping SmartProxy:', err)) : Promise.resolve(),
|
this.smartProxy ? this.smartProxy.stop().catch(err => console.error('Error stopping SmartProxy:', err)) : Promise.resolve(),
|
||||||
|
|
||||||
// Stop DNS server if running
|
// Stop DNS server if running
|
||||||
this.dnsServer ?
|
this.dnsServer ?
|
||||||
this.dnsServer.stop().catch(err => console.error('Error stopping DNS server:', err)) :
|
this.dnsServer.stop().catch(err => console.error('Error stopping DNS server:', err)) :
|
||||||
|
Promise.resolve(),
|
||||||
|
|
||||||
|
// Stop RADIUS server if running
|
||||||
|
this.radiusServer ?
|
||||||
|
this.radiusServer.stop().catch(err => console.error('Error stopping RADIUS server:', err)) :
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -1338,9 +1367,47 @@ export class DcRouter {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up RADIUS server for network authentication
|
||||||
|
*/
|
||||||
|
private async setupRadiusServer(): Promise<void> {
|
||||||
|
if (!this.options.radiusConfig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', 'Setting up RADIUS server...');
|
||||||
|
|
||||||
|
this.radiusServer = new RadiusServer(this.options.radiusConfig, this.storageManager);
|
||||||
|
await this.radiusServer.start();
|
||||||
|
|
||||||
|
logger.log('info', `RADIUS server started on ports ${this.options.radiusConfig.authPort || 1812} (auth) and ${this.options.radiusConfig.acctPort || 1813} (acct)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update RADIUS configuration at runtime
|
||||||
|
*/
|
||||||
|
public async updateRadiusConfig(config: IRadiusServerConfig): Promise<void> {
|
||||||
|
// Stop existing RADIUS server if running
|
||||||
|
if (this.radiusServer) {
|
||||||
|
await this.radiusServer.stop();
|
||||||
|
this.radiusServer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update configuration
|
||||||
|
this.options.radiusConfig = config;
|
||||||
|
|
||||||
|
// Start with new configuration
|
||||||
|
await this.setupRadiusServer();
|
||||||
|
|
||||||
|
logger.log('info', 'RADIUS configuration updated');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-export email server types for convenience
|
// Re-export email server types for convenience
|
||||||
export type { IUnifiedEmailServerOptions };
|
export type { IUnifiedEmailServerOptions };
|
||||||
|
|
||||||
|
// Re-export RADIUS types for convenience
|
||||||
|
export type { IRadiusServerConfig };
|
||||||
|
|
||||||
export default DcRouter;
|
export default DcRouter;
|
||||||
|
|||||||
@@ -4,4 +4,7 @@ export * from './mail/index.js';
|
|||||||
// DcRouter
|
// DcRouter
|
||||||
export * from './classes.dcrouter.js';
|
export * from './classes.dcrouter.js';
|
||||||
|
|
||||||
|
// RADIUS module
|
||||||
|
export * from './radius/index.js';
|
||||||
|
|
||||||
export const runCli = async () => {}
|
export const runCli = async () => {}
|
||||||
@@ -16,6 +16,7 @@ export class OpsServer {
|
|||||||
private logsHandler: handlers.LogsHandler;
|
private logsHandler: handlers.LogsHandler;
|
||||||
private securityHandler: handlers.SecurityHandler;
|
private securityHandler: handlers.SecurityHandler;
|
||||||
private statsHandler: handlers.StatsHandler;
|
private statsHandler: handlers.StatsHandler;
|
||||||
|
private radiusHandler: handlers.RadiusHandler;
|
||||||
|
|
||||||
constructor(dcRouterRefArg: DcRouter) {
|
constructor(dcRouterRefArg: DcRouter) {
|
||||||
this.dcRouterRef = dcRouterRefArg;
|
this.dcRouterRef = dcRouterRefArg;
|
||||||
@@ -53,7 +54,8 @@ export class OpsServer {
|
|||||||
this.logsHandler = new handlers.LogsHandler(this);
|
this.logsHandler = new handlers.LogsHandler(this);
|
||||||
this.securityHandler = new handlers.SecurityHandler(this);
|
this.securityHandler = new handlers.SecurityHandler(this);
|
||||||
this.statsHandler = new handlers.StatsHandler(this);
|
this.statsHandler = new handlers.StatsHandler(this);
|
||||||
|
this.radiusHandler = new handlers.RadiusHandler(this);
|
||||||
|
|
||||||
console.log('✅ OpsServer TypedRequest handlers initialized');
|
console.log('✅ OpsServer TypedRequest handlers initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ export * from './admin.handler.js';
|
|||||||
export * from './config.handler.js';
|
export * from './config.handler.js';
|
||||||
export * from './logs.handler.js';
|
export * from './logs.handler.js';
|
||||||
export * from './security.handler.js';
|
export * from './security.handler.js';
|
||||||
export * from './stats.handler.js';
|
export * from './stats.handler.js';
|
||||||
|
export * from './radius.handler.js';
|
||||||
405
ts/opsserver/handlers/radius.handler.ts
Normal file
405
ts/opsserver/handlers/radius.handler.ts
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
import * as plugins from '../../plugins.js';
|
||||||
|
import type { OpsServer } from '../classes.opsserver.js';
|
||||||
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||||
|
|
||||||
|
export class RadiusHandler {
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
|
constructor(private opsServerRef: OpsServer) {
|
||||||
|
// Add this handler's router to the parent
|
||||||
|
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
this.registerHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerHandlers(): void {
|
||||||
|
// ========================================================================
|
||||||
|
// RADIUS Client Management
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
// Get all RADIUS clients
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusClients>(
|
||||||
|
'getRadiusClients',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
|
||||||
|
|
||||||
|
if (!radiusServer) {
|
||||||
|
return { clients: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const clients = radiusServer.getClients();
|
||||||
|
return {
|
||||||
|
clients: clients.map(c => ({
|
||||||
|
name: c.name,
|
||||||
|
ipRange: c.ipRange,
|
||||||
|
description: c.description,
|
||||||
|
enabled: c.enabled,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add or update a RADIUS client
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SetRadiusClient>(
|
||||||
|
'setRadiusClient',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
|
||||||
|
|
||||||
|
if (!radiusServer) {
|
||||||
|
return { success: false, message: 'RADIUS server not configured' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await radiusServer.addClient(dataArg.client);
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, message: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove a RADIUS client
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveRadiusClient>(
|
||||||
|
'removeRadiusClient',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
|
||||||
|
|
||||||
|
if (!radiusServer) {
|
||||||
|
return { success: false, message: 'RADIUS server not configured' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const removed = radiusServer.removeClient(dataArg.name);
|
||||||
|
return {
|
||||||
|
success: removed,
|
||||||
|
message: removed ? undefined : 'Client not found',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// VLAN Mapping Management
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
// Get all VLAN mappings
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetVlanMappings>(
|
||||||
|
'getVlanMappings',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
|
||||||
|
|
||||||
|
if (!radiusServer) {
|
||||||
|
return {
|
||||||
|
mappings: [],
|
||||||
|
config: { defaultVlan: 1, allowUnknownMacs: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const vlanManager = radiusServer.getVlanManager();
|
||||||
|
const mappings = vlanManager.getAllMappings();
|
||||||
|
const config = vlanManager.getConfig();
|
||||||
|
|
||||||
|
return {
|
||||||
|
mappings: mappings.map(m => ({
|
||||||
|
mac: m.mac,
|
||||||
|
vlan: m.vlan,
|
||||||
|
description: m.description,
|
||||||
|
enabled: m.enabled,
|
||||||
|
createdAt: m.createdAt,
|
||||||
|
updatedAt: m.updatedAt,
|
||||||
|
})),
|
||||||
|
config: {
|
||||||
|
defaultVlan: config.defaultVlan,
|
||||||
|
allowUnknownMacs: config.allowUnknownMacs,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add or update a VLAN mapping
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SetVlanMapping>(
|
||||||
|
'setVlanMapping',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
|
||||||
|
|
||||||
|
if (!radiusServer) {
|
||||||
|
return { success: false, message: 'RADIUS server not configured' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const vlanManager = radiusServer.getVlanManager();
|
||||||
|
const mapping = await vlanManager.addMapping(dataArg.mapping);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
mapping: {
|
||||||
|
mac: mapping.mac,
|
||||||
|
vlan: mapping.vlan,
|
||||||
|
description: mapping.description,
|
||||||
|
enabled: mapping.enabled,
|
||||||
|
createdAt: mapping.createdAt,
|
||||||
|
updatedAt: mapping.updatedAt,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, message: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove a VLAN mapping
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveVlanMapping>(
|
||||||
|
'removeVlanMapping',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
|
||||||
|
|
||||||
|
if (!radiusServer) {
|
||||||
|
return { success: false, message: 'RADIUS server not configured' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const vlanManager = radiusServer.getVlanManager();
|
||||||
|
const removed = await vlanManager.removeMapping(dataArg.mac);
|
||||||
|
return {
|
||||||
|
success: removed,
|
||||||
|
message: removed ? undefined : 'Mapping not found',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update VLAN configuration
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateVlanConfig>(
|
||||||
|
'updateVlanConfig',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
|
||||||
|
|
||||||
|
if (!radiusServer) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
config: { defaultVlan: 1, allowUnknownMacs: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const vlanManager = radiusServer.getVlanManager();
|
||||||
|
vlanManager.updateConfig({
|
||||||
|
defaultVlan: dataArg.defaultVlan,
|
||||||
|
allowUnknownMacs: dataArg.allowUnknownMacs,
|
||||||
|
});
|
||||||
|
|
||||||
|
const config = vlanManager.getConfig();
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
config: {
|
||||||
|
defaultVlan: config.defaultVlan,
|
||||||
|
allowUnknownMacs: config.allowUnknownMacs,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test VLAN assignment
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_TestVlanAssignment>(
|
||||||
|
'testVlanAssignment',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
|
||||||
|
|
||||||
|
if (!radiusServer) {
|
||||||
|
return { assigned: false, vlan: 0, isDefault: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const vlanManager = radiusServer.getVlanManager();
|
||||||
|
const result = vlanManager.assignVlan(dataArg.mac);
|
||||||
|
|
||||||
|
return {
|
||||||
|
assigned: result.assigned,
|
||||||
|
vlan: result.vlan,
|
||||||
|
isDefault: result.isDefault,
|
||||||
|
matchedRule: result.matchedRule
|
||||||
|
? {
|
||||||
|
mac: result.matchedRule.mac,
|
||||||
|
vlan: result.matchedRule.vlan,
|
||||||
|
description: result.matchedRule.description,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Accounting / Session Management
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
// Get active sessions
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusSessions>(
|
||||||
|
'getRadiusSessions',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
|
||||||
|
|
||||||
|
if (!radiusServer) {
|
||||||
|
return { sessions: [], totalCount: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountingManager = radiusServer.getAccountingManager();
|
||||||
|
let sessions = accountingManager.getActiveSessions();
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if (dataArg.filter) {
|
||||||
|
if (dataArg.filter.username) {
|
||||||
|
sessions = sessions.filter(s => s.username === dataArg.filter!.username);
|
||||||
|
}
|
||||||
|
if (dataArg.filter.nasIpAddress) {
|
||||||
|
sessions = sessions.filter(s => s.nasIpAddress === dataArg.filter!.nasIpAddress);
|
||||||
|
}
|
||||||
|
if (dataArg.filter.vlanId !== undefined) {
|
||||||
|
sessions = sessions.filter(s => s.vlanId === dataArg.filter!.vlanId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessions: sessions.map(s => ({
|
||||||
|
sessionId: s.sessionId,
|
||||||
|
username: s.username,
|
||||||
|
macAddress: s.macAddress,
|
||||||
|
nasIpAddress: s.nasIpAddress,
|
||||||
|
nasIdentifier: s.nasIdentifier,
|
||||||
|
vlanId: s.vlanId,
|
||||||
|
framedIpAddress: s.framedIpAddress,
|
||||||
|
startTime: s.startTime,
|
||||||
|
lastUpdateTime: s.lastUpdateTime,
|
||||||
|
status: s.status,
|
||||||
|
inputOctets: s.inputOctets,
|
||||||
|
outputOctets: s.outputOctets,
|
||||||
|
sessionTime: s.sessionTime,
|
||||||
|
})),
|
||||||
|
totalCount: sessions.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Disconnect a session
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DisconnectRadiusSession>(
|
||||||
|
'disconnectRadiusSession',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
|
||||||
|
|
||||||
|
if (!radiusServer) {
|
||||||
|
return { success: false, message: 'RADIUS server not configured' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountingManager = radiusServer.getAccountingManager();
|
||||||
|
const disconnected = await accountingManager.disconnectSession(
|
||||||
|
dataArg.sessionId,
|
||||||
|
dataArg.reason || 'AdminReset'
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: disconnected,
|
||||||
|
message: disconnected ? undefined : 'Session not found',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get accounting summary
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusAccountingSummary>(
|
||||||
|
'getRadiusAccountingSummary',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
|
||||||
|
|
||||||
|
if (!radiusServer) {
|
||||||
|
return {
|
||||||
|
summary: {
|
||||||
|
periodStart: dataArg.startTime,
|
||||||
|
periodEnd: dataArg.endTime,
|
||||||
|
totalSessions: 0,
|
||||||
|
activeSessions: 0,
|
||||||
|
totalInputBytes: 0,
|
||||||
|
totalOutputBytes: 0,
|
||||||
|
totalSessionTime: 0,
|
||||||
|
averageSessionDuration: 0,
|
||||||
|
uniqueUsers: 0,
|
||||||
|
sessionsByVlan: {},
|
||||||
|
topUsersByTraffic: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountingManager = radiusServer.getAccountingManager();
|
||||||
|
const summary = await accountingManager.getSummary(dataArg.startTime, dataArg.endTime);
|
||||||
|
|
||||||
|
return { summary };
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Statistics
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
// Get RADIUS statistics
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusStatistics>(
|
||||||
|
'getRadiusStatistics',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
|
||||||
|
|
||||||
|
if (!radiusServer) {
|
||||||
|
return {
|
||||||
|
stats: {
|
||||||
|
running: false,
|
||||||
|
uptime: 0,
|
||||||
|
authRequests: 0,
|
||||||
|
authAccepts: 0,
|
||||||
|
authRejects: 0,
|
||||||
|
accountingRequests: 0,
|
||||||
|
activeSessions: 0,
|
||||||
|
vlanMappings: 0,
|
||||||
|
clients: 0,
|
||||||
|
},
|
||||||
|
vlanStats: {
|
||||||
|
totalMappings: 0,
|
||||||
|
enabledMappings: 0,
|
||||||
|
exactMatches: 0,
|
||||||
|
ouiPatterns: 0,
|
||||||
|
wildcardPatterns: 0,
|
||||||
|
},
|
||||||
|
accountingStats: {
|
||||||
|
activeSessions: 0,
|
||||||
|
totalSessionsStarted: 0,
|
||||||
|
totalSessionsStopped: 0,
|
||||||
|
totalInputBytes: 0,
|
||||||
|
totalOutputBytes: 0,
|
||||||
|
interimUpdatesReceived: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = radiusServer.getStats();
|
||||||
|
const vlanStats = radiusServer.getVlanManager().getStats();
|
||||||
|
const accountingStats = radiusServer.getAccountingManager().getStats();
|
||||||
|
|
||||||
|
return {
|
||||||
|
stats,
|
||||||
|
vlanStats,
|
||||||
|
accountingStats,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,12 +55,13 @@ import * as smartnetwork from '@push.rocks/smartnetwork';
|
|||||||
import * as smartpath from '@push.rocks/smartpath';
|
import * as smartpath from '@push.rocks/smartpath';
|
||||||
import * as smartproxy from '@push.rocks/smartproxy';
|
import * as smartproxy from '@push.rocks/smartproxy';
|
||||||
import * as smartpromise from '@push.rocks/smartpromise';
|
import * as smartpromise from '@push.rocks/smartpromise';
|
||||||
|
import * as smartradius from '@push.rocks/smartradius';
|
||||||
import * as smartrequest from '@push.rocks/smartrequest';
|
import * as smartrequest from '@push.rocks/smartrequest';
|
||||||
import * as smartrule from '@push.rocks/smartrule';
|
import * as smartrule from '@push.rocks/smartrule';
|
||||||
import * as smartrx from '@push.rocks/smartrx';
|
import * as smartrx from '@push.rocks/smartrx';
|
||||||
import * as smartunique from '@push.rocks/smartunique';
|
import * as smartunique from '@push.rocks/smartunique';
|
||||||
|
|
||||||
export { projectinfo, qenv, smartacme, smartdata, smartdns, smartfile, smartguard, smartjwt, smartlog, smartmail, smartmetrics, smartnetwork, smartpath, smartproxy, smartpromise, smartrequest, smartrule, smartrx, smartunique };
|
export { projectinfo, qenv, smartacme, smartdata, smartdns, smartfile, smartguard, smartjwt, smartlog, smartmail, smartmetrics, smartnetwork, smartpath, smartproxy, smartpromise, smartradius, smartrequest, smartrule, smartrx, smartunique };
|
||||||
|
|
||||||
// Define SmartLog types for use in error handling
|
// Define SmartLog types for use in error handling
|
||||||
export type TLogLevel = 'error' | 'warn' | 'info' | 'success' | 'debug';
|
export type TLogLevel = 'error' | 'warn' | 'info' | 'success' | 'debug';
|
||||||
|
|||||||
607
ts/radius/classes.accounting.manager.ts
Normal file
607
ts/radius/classes.accounting.manager.ts
Normal file
@@ -0,0 +1,607 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { logger } from '../logger.js';
|
||||||
|
import type { StorageManager } from '../storage/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RADIUS accounting session
|
||||||
|
*/
|
||||||
|
export interface IAccountingSession {
|
||||||
|
/** Unique session ID from RADIUS */
|
||||||
|
sessionId: string;
|
||||||
|
/** Username (often MAC address for MAB) */
|
||||||
|
username: string;
|
||||||
|
/** MAC address of the device */
|
||||||
|
macAddress?: string;
|
||||||
|
/** NAS IP address (switch/AP) */
|
||||||
|
nasIpAddress: string;
|
||||||
|
/** NAS port (physical or virtual) */
|
||||||
|
nasPort?: number;
|
||||||
|
/** NAS port type */
|
||||||
|
nasPortType?: string;
|
||||||
|
/** NAS identifier (name) */
|
||||||
|
nasIdentifier?: string;
|
||||||
|
/** Assigned VLAN */
|
||||||
|
vlanId?: number;
|
||||||
|
/** Assigned IP address (if any) */
|
||||||
|
framedIpAddress?: string;
|
||||||
|
/** Called station ID (usually BSSID for wireless) */
|
||||||
|
calledStationId?: string;
|
||||||
|
/** Calling station ID (usually client MAC) */
|
||||||
|
callingStationId?: string;
|
||||||
|
/** Session start time */
|
||||||
|
startTime: number;
|
||||||
|
/** Session end time (0 if active) */
|
||||||
|
endTime: number;
|
||||||
|
/** Last update time (interim accounting) */
|
||||||
|
lastUpdateTime: number;
|
||||||
|
/** Session status */
|
||||||
|
status: 'active' | 'stopped' | 'terminated';
|
||||||
|
/** Termination cause (if stopped) */
|
||||||
|
terminateCause?: string;
|
||||||
|
/** Input octets (bytes received by NAS from client) */
|
||||||
|
inputOctets: number;
|
||||||
|
/** Output octets (bytes sent by NAS to client) */
|
||||||
|
outputOctets: number;
|
||||||
|
/** Input packets */
|
||||||
|
inputPackets: number;
|
||||||
|
/** Output packets */
|
||||||
|
outputPackets: number;
|
||||||
|
/** Session duration in seconds */
|
||||||
|
sessionTime: number;
|
||||||
|
/** Service type */
|
||||||
|
serviceType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accounting summary for a time period
|
||||||
|
*/
|
||||||
|
export interface IAccountingSummary {
|
||||||
|
/** Time period start */
|
||||||
|
periodStart: number;
|
||||||
|
/** Time period end */
|
||||||
|
periodEnd: number;
|
||||||
|
/** Total sessions */
|
||||||
|
totalSessions: number;
|
||||||
|
/** Active sessions */
|
||||||
|
activeSessions: number;
|
||||||
|
/** Total input bytes */
|
||||||
|
totalInputBytes: number;
|
||||||
|
/** Total output bytes */
|
||||||
|
totalOutputBytes: number;
|
||||||
|
/** Total session time (seconds) */
|
||||||
|
totalSessionTime: number;
|
||||||
|
/** Average session duration (seconds) */
|
||||||
|
averageSessionDuration: number;
|
||||||
|
/** Unique users/devices */
|
||||||
|
uniqueUsers: number;
|
||||||
|
/** Sessions by VLAN */
|
||||||
|
sessionsByVlan: Record<number, number>;
|
||||||
|
/** Top users by traffic */
|
||||||
|
topUsersByTraffic: Array<{ username: string; totalBytes: number }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accounting manager configuration
|
||||||
|
*/
|
||||||
|
export interface IAccountingManagerConfig {
|
||||||
|
/** Storage key prefix */
|
||||||
|
storagePrefix?: string;
|
||||||
|
/** Session retention period in days (default: 30) */
|
||||||
|
retentionDays?: number;
|
||||||
|
/** Enable detailed session logging */
|
||||||
|
detailedLogging?: boolean;
|
||||||
|
/** Maximum active sessions to track in memory */
|
||||||
|
maxActiveSessions?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages RADIUS accounting data including:
|
||||||
|
* - Session tracking (start/stop/interim)
|
||||||
|
* - Data usage tracking (bytes in/out)
|
||||||
|
* - Session history and retention
|
||||||
|
* - Billing reports and summaries
|
||||||
|
*/
|
||||||
|
export class AccountingManager {
|
||||||
|
private activeSessions: Map<string, IAccountingSession> = new Map();
|
||||||
|
private config: Required<IAccountingManagerConfig>;
|
||||||
|
private storageManager?: StorageManager;
|
||||||
|
|
||||||
|
// Counters for statistics
|
||||||
|
private stats = {
|
||||||
|
totalSessionsStarted: 0,
|
||||||
|
totalSessionsStopped: 0,
|
||||||
|
totalInputBytes: 0,
|
||||||
|
totalOutputBytes: 0,
|
||||||
|
interimUpdatesReceived: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(config?: IAccountingManagerConfig, storageManager?: StorageManager) {
|
||||||
|
this.config = {
|
||||||
|
storagePrefix: config?.storagePrefix ?? '/radius/accounting',
|
||||||
|
retentionDays: config?.retentionDays ?? 30,
|
||||||
|
detailedLogging: config?.detailedLogging ?? false,
|
||||||
|
maxActiveSessions: config?.maxActiveSessions ?? 10000,
|
||||||
|
};
|
||||||
|
this.storageManager = storageManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the accounting manager
|
||||||
|
*/
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
if (this.storageManager) {
|
||||||
|
await this.loadActiveSessions();
|
||||||
|
}
|
||||||
|
logger.log('info', `AccountingManager initialized with ${this.activeSessions.size} active sessions`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle accounting start request
|
||||||
|
*/
|
||||||
|
async handleAccountingStart(data: {
|
||||||
|
sessionId: string;
|
||||||
|
username: string;
|
||||||
|
macAddress?: string;
|
||||||
|
nasIpAddress: string;
|
||||||
|
nasPort?: number;
|
||||||
|
nasPortType?: string;
|
||||||
|
nasIdentifier?: string;
|
||||||
|
vlanId?: number;
|
||||||
|
framedIpAddress?: string;
|
||||||
|
calledStationId?: string;
|
||||||
|
callingStationId?: string;
|
||||||
|
serviceType?: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
const session: IAccountingSession = {
|
||||||
|
sessionId: data.sessionId,
|
||||||
|
username: data.username,
|
||||||
|
macAddress: data.macAddress,
|
||||||
|
nasIpAddress: data.nasIpAddress,
|
||||||
|
nasPort: data.nasPort,
|
||||||
|
nasPortType: data.nasPortType,
|
||||||
|
nasIdentifier: data.nasIdentifier,
|
||||||
|
vlanId: data.vlanId,
|
||||||
|
framedIpAddress: data.framedIpAddress,
|
||||||
|
calledStationId: data.calledStationId,
|
||||||
|
callingStationId: data.callingStationId,
|
||||||
|
serviceType: data.serviceType,
|
||||||
|
startTime: now,
|
||||||
|
endTime: 0,
|
||||||
|
lastUpdateTime: now,
|
||||||
|
status: 'active',
|
||||||
|
inputOctets: 0,
|
||||||
|
outputOctets: 0,
|
||||||
|
inputPackets: 0,
|
||||||
|
outputPackets: 0,
|
||||||
|
sessionTime: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if we're at capacity
|
||||||
|
if (this.activeSessions.size >= this.config.maxActiveSessions) {
|
||||||
|
// Remove oldest session
|
||||||
|
const oldest = this.findOldestSession();
|
||||||
|
if (oldest) {
|
||||||
|
await this.evictSession(oldest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeSessions.set(data.sessionId, session);
|
||||||
|
this.stats.totalSessionsStarted++;
|
||||||
|
|
||||||
|
if (this.config.detailedLogging) {
|
||||||
|
logger.log('info', `Accounting Start: session=${data.sessionId}, user=${data.username}, NAS=${data.nasIpAddress}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist session
|
||||||
|
if (this.storageManager) {
|
||||||
|
await this.persistSession(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle accounting interim update request
|
||||||
|
*/
|
||||||
|
async handleAccountingUpdate(data: {
|
||||||
|
sessionId: string;
|
||||||
|
inputOctets?: number;
|
||||||
|
outputOctets?: number;
|
||||||
|
inputPackets?: number;
|
||||||
|
outputPackets?: number;
|
||||||
|
sessionTime?: number;
|
||||||
|
}): Promise<void> {
|
||||||
|
const session = this.activeSessions.get(data.sessionId);
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
logger.log('warn', `Interim update for unknown session: ${data.sessionId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update session metrics
|
||||||
|
if (data.inputOctets !== undefined) {
|
||||||
|
session.inputOctets = data.inputOctets;
|
||||||
|
}
|
||||||
|
if (data.outputOctets !== undefined) {
|
||||||
|
session.outputOctets = data.outputOctets;
|
||||||
|
}
|
||||||
|
if (data.inputPackets !== undefined) {
|
||||||
|
session.inputPackets = data.inputPackets;
|
||||||
|
}
|
||||||
|
if (data.outputPackets !== undefined) {
|
||||||
|
session.outputPackets = data.outputPackets;
|
||||||
|
}
|
||||||
|
if (data.sessionTime !== undefined) {
|
||||||
|
session.sessionTime = data.sessionTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
session.lastUpdateTime = Date.now();
|
||||||
|
this.stats.interimUpdatesReceived++;
|
||||||
|
|
||||||
|
if (this.config.detailedLogging) {
|
||||||
|
logger.log('debug', `Accounting Interim: session=${data.sessionId}, in=${data.inputOctets}, out=${data.outputOctets}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update persisted session
|
||||||
|
if (this.storageManager) {
|
||||||
|
await this.persistSession(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle accounting stop request
|
||||||
|
*/
|
||||||
|
async handleAccountingStop(data: {
|
||||||
|
sessionId: string;
|
||||||
|
terminateCause?: string;
|
||||||
|
inputOctets?: number;
|
||||||
|
outputOctets?: number;
|
||||||
|
inputPackets?: number;
|
||||||
|
outputPackets?: number;
|
||||||
|
sessionTime?: number;
|
||||||
|
}): Promise<void> {
|
||||||
|
const session = this.activeSessions.get(data.sessionId);
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
logger.log('warn', `Stop for unknown session: ${data.sessionId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update final metrics
|
||||||
|
if (data.inputOctets !== undefined) {
|
||||||
|
session.inputOctets = data.inputOctets;
|
||||||
|
}
|
||||||
|
if (data.outputOctets !== undefined) {
|
||||||
|
session.outputOctets = data.outputOctets;
|
||||||
|
}
|
||||||
|
if (data.inputPackets !== undefined) {
|
||||||
|
session.inputPackets = data.inputPackets;
|
||||||
|
}
|
||||||
|
if (data.outputPackets !== undefined) {
|
||||||
|
session.outputPackets = data.outputPackets;
|
||||||
|
}
|
||||||
|
if (data.sessionTime !== undefined) {
|
||||||
|
session.sessionTime = data.sessionTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
session.endTime = Date.now();
|
||||||
|
session.lastUpdateTime = session.endTime;
|
||||||
|
session.status = 'stopped';
|
||||||
|
session.terminateCause = data.terminateCause;
|
||||||
|
|
||||||
|
// Update global stats
|
||||||
|
this.stats.totalSessionsStopped++;
|
||||||
|
this.stats.totalInputBytes += session.inputOctets;
|
||||||
|
this.stats.totalOutputBytes += session.outputOctets;
|
||||||
|
|
||||||
|
if (this.config.detailedLogging) {
|
||||||
|
logger.log('info', `Accounting Stop: session=${data.sessionId}, duration=${session.sessionTime}s, in=${session.inputOctets}, out=${session.outputOctets}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archive the session
|
||||||
|
if (this.storageManager) {
|
||||||
|
await this.archiveSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from active sessions
|
||||||
|
this.activeSessions.delete(data.sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an active session by ID
|
||||||
|
*/
|
||||||
|
getSession(sessionId: string): IAccountingSession | undefined {
|
||||||
|
return this.activeSessions.get(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active sessions
|
||||||
|
*/
|
||||||
|
getActiveSessions(): IAccountingSession[] {
|
||||||
|
return Array.from(this.activeSessions.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active sessions by username
|
||||||
|
*/
|
||||||
|
getSessionsByUsername(username: string): IAccountingSession[] {
|
||||||
|
return Array.from(this.activeSessions.values()).filter(s => s.username === username);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active sessions by NAS IP
|
||||||
|
*/
|
||||||
|
getSessionsByNas(nasIpAddress: string): IAccountingSession[] {
|
||||||
|
return Array.from(this.activeSessions.values()).filter(s => s.nasIpAddress === nasIpAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active sessions by VLAN
|
||||||
|
*/
|
||||||
|
getSessionsByVlan(vlanId: number): IAccountingSession[] {
|
||||||
|
return Array.from(this.activeSessions.values()).filter(s => s.vlanId === vlanId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get accounting summary for a time period
|
||||||
|
*/
|
||||||
|
async getSummary(startTime: number, endTime: number): Promise<IAccountingSummary> {
|
||||||
|
// Get archived sessions for the time period
|
||||||
|
const archivedSessions = await this.getArchivedSessions(startTime, endTime);
|
||||||
|
|
||||||
|
// Combine with active sessions that started within the period
|
||||||
|
const activeSessions = Array.from(this.activeSessions.values()).filter(
|
||||||
|
s => s.startTime >= startTime && s.startTime <= endTime
|
||||||
|
);
|
||||||
|
|
||||||
|
const allSessions = [...archivedSessions, ...activeSessions];
|
||||||
|
|
||||||
|
// Calculate summary
|
||||||
|
let totalInputBytes = 0;
|
||||||
|
let totalOutputBytes = 0;
|
||||||
|
let totalSessionTime = 0;
|
||||||
|
const uniqueUsers = new Set<string>();
|
||||||
|
const sessionsByVlan: Record<number, number> = {};
|
||||||
|
const userTraffic: Record<string, number> = {};
|
||||||
|
|
||||||
|
for (const session of allSessions) {
|
||||||
|
totalInputBytes += session.inputOctets;
|
||||||
|
totalOutputBytes += session.outputOctets;
|
||||||
|
totalSessionTime += session.sessionTime;
|
||||||
|
uniqueUsers.add(session.username);
|
||||||
|
|
||||||
|
if (session.vlanId !== undefined) {
|
||||||
|
sessionsByVlan[session.vlanId] = (sessionsByVlan[session.vlanId] || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userBytes = session.inputOctets + session.outputOctets;
|
||||||
|
userTraffic[session.username] = (userTraffic[session.username] || 0) + userBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top users by traffic
|
||||||
|
const topUsersByTraffic = Object.entries(userTraffic)
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, 10)
|
||||||
|
.map(([username, totalBytes]) => ({ username, totalBytes }));
|
||||||
|
|
||||||
|
return {
|
||||||
|
periodStart: startTime,
|
||||||
|
periodEnd: endTime,
|
||||||
|
totalSessions: allSessions.length,
|
||||||
|
activeSessions: activeSessions.length,
|
||||||
|
totalInputBytes,
|
||||||
|
totalOutputBytes,
|
||||||
|
totalSessionTime,
|
||||||
|
averageSessionDuration: allSessions.length > 0 ? totalSessionTime / allSessions.length : 0,
|
||||||
|
uniqueUsers: uniqueUsers.size,
|
||||||
|
sessionsByVlan,
|
||||||
|
topUsersByTraffic,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get statistics
|
||||||
|
*/
|
||||||
|
getStats(): {
|
||||||
|
activeSessions: number;
|
||||||
|
totalSessionsStarted: number;
|
||||||
|
totalSessionsStopped: number;
|
||||||
|
totalInputBytes: number;
|
||||||
|
totalOutputBytes: number;
|
||||||
|
interimUpdatesReceived: number;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
activeSessions: this.activeSessions.size,
|
||||||
|
...this.stats,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect a session (admin action)
|
||||||
|
*/
|
||||||
|
async disconnectSession(sessionId: string, reason: string = 'AdminReset'): Promise<boolean> {
|
||||||
|
const session = this.activeSessions.get(sessionId);
|
||||||
|
if (!session) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.handleAccountingStop({
|
||||||
|
sessionId,
|
||||||
|
terminateCause: reason,
|
||||||
|
sessionTime: Math.floor((Date.now() - session.startTime) / 1000),
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up old archived sessions based on retention policy
|
||||||
|
*/
|
||||||
|
async cleanupOldSessions(): Promise<number> {
|
||||||
|
if (!this.storageManager) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cutoffTime = Date.now() - this.config.retentionDays * 24 * 60 * 60 * 1000;
|
||||||
|
let deletedCount = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keys = await this.storageManager.list(`${this.config.storagePrefix}/archive/`);
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
try {
|
||||||
|
const session = await this.storageManager.getJSON<IAccountingSession>(key);
|
||||||
|
if (session && session.endTime > 0 && session.endTime < cutoffTime) {
|
||||||
|
await this.storageManager.delete(key);
|
||||||
|
deletedCount++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore individual errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deletedCount > 0) {
|
||||||
|
logger.log('info', `Cleaned up ${deletedCount} old accounting sessions`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.log('error', `Failed to cleanup old sessions: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deletedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the oldest active session
|
||||||
|
*/
|
||||||
|
private findOldestSession(): string | null {
|
||||||
|
let oldestTime = Infinity;
|
||||||
|
let oldestSessionId: string | null = null;
|
||||||
|
|
||||||
|
for (const [sessionId, session] of this.activeSessions) {
|
||||||
|
if (session.lastUpdateTime < oldestTime) {
|
||||||
|
oldestTime = session.lastUpdateTime;
|
||||||
|
oldestSessionId = sessionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldestSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evict a session from memory
|
||||||
|
*/
|
||||||
|
private async evictSession(sessionId: string): Promise<void> {
|
||||||
|
const session = this.activeSessions.get(sessionId);
|
||||||
|
if (session) {
|
||||||
|
session.status = 'terminated';
|
||||||
|
session.terminateCause = 'SessionEvicted';
|
||||||
|
session.endTime = Date.now();
|
||||||
|
|
||||||
|
if (this.storageManager) {
|
||||||
|
await this.archiveSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeSessions.delete(sessionId);
|
||||||
|
logger.log('warn', `Evicted session ${sessionId} due to capacity limit`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load active sessions from storage
|
||||||
|
*/
|
||||||
|
private async loadActiveSessions(): Promise<void> {
|
||||||
|
if (!this.storageManager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keys = await this.storageManager.list(`${this.config.storagePrefix}/active/`);
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
try {
|
||||||
|
const session = await this.storageManager.getJSON<IAccountingSession>(key);
|
||||||
|
if (session && session.status === 'active') {
|
||||||
|
this.activeSessions.set(session.sessionId, session);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore individual errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.log('warn', `Failed to load active sessions: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist a session to storage
|
||||||
|
*/
|
||||||
|
private async persistSession(session: IAccountingSession): Promise<void> {
|
||||||
|
if (!this.storageManager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `${this.config.storagePrefix}/active/${session.sessionId}.json`;
|
||||||
|
try {
|
||||||
|
await this.storageManager.setJSON(key, session);
|
||||||
|
} catch (error) {
|
||||||
|
logger.log('error', `Failed to persist session ${session.sessionId}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Archive a completed session
|
||||||
|
*/
|
||||||
|
private async archiveSession(session: IAccountingSession): Promise<void> {
|
||||||
|
if (!this.storageManager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Remove from active
|
||||||
|
const activeKey = `${this.config.storagePrefix}/active/${session.sessionId}.json`;
|
||||||
|
await this.storageManager.delete(activeKey);
|
||||||
|
|
||||||
|
// Add to archive with date-based path
|
||||||
|
const date = new Date(session.endTime);
|
||||||
|
const archiveKey = `${this.config.storagePrefix}/archive/${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}/${session.sessionId}.json`;
|
||||||
|
await this.storageManager.setJSON(archiveKey, session);
|
||||||
|
} catch (error) {
|
||||||
|
logger.log('error', `Failed to archive session ${session.sessionId}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get archived sessions for a time period
|
||||||
|
*/
|
||||||
|
private async getArchivedSessions(startTime: number, endTime: number): Promise<IAccountingSession[]> {
|
||||||
|
if (!this.storageManager) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessions: IAccountingSession[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keys = await this.storageManager.list(`${this.config.storagePrefix}/archive/`);
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
try {
|
||||||
|
const session = await this.storageManager.getJSON<IAccountingSession>(key);
|
||||||
|
if (
|
||||||
|
session &&
|
||||||
|
session.endTime > 0 &&
|
||||||
|
session.startTime <= endTime &&
|
||||||
|
session.endTime >= startTime
|
||||||
|
) {
|
||||||
|
sessions.push(session);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore individual errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.log('warn', `Failed to get archived sessions: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessions;
|
||||||
|
}
|
||||||
|
}
|
||||||
532
ts/radius/classes.radius.server.ts
Normal file
532
ts/radius/classes.radius.server.ts
Normal file
@@ -0,0 +1,532 @@
|
|||||||
|
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<Omit<IMacVlanMapping, 'createdAt' | 'updatedAt'>>;
|
||||||
|
};
|
||||||
|
/** 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<string, string> = 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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
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<IRadiusAuthResult> {
|
||||||
|
// 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<void> {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
363
ts/radius/classes.vlan.manager.ts
Normal file
363
ts/radius/classes.vlan.manager.ts
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { logger } from '../logger.js';
|
||||||
|
import type { StorageManager } from '../storage/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MAC address to VLAN mapping
|
||||||
|
*/
|
||||||
|
export interface IMacVlanMapping {
|
||||||
|
/** MAC address (full) or OUI pattern (e.g., "00:11:22" for vendor prefix) */
|
||||||
|
mac: string;
|
||||||
|
/** VLAN ID to assign */
|
||||||
|
vlan: number;
|
||||||
|
/** Optional description */
|
||||||
|
description?: string;
|
||||||
|
/** Whether this mapping is enabled */
|
||||||
|
enabled: boolean;
|
||||||
|
/** Creation timestamp */
|
||||||
|
createdAt: number;
|
||||||
|
/** Last update timestamp */
|
||||||
|
updatedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VLAN assignment result
|
||||||
|
*/
|
||||||
|
export interface IVlanAssignmentResult {
|
||||||
|
/** Whether a VLAN was successfully assigned */
|
||||||
|
assigned: boolean;
|
||||||
|
/** The assigned VLAN ID (or default if not matched) */
|
||||||
|
vlan: number;
|
||||||
|
/** The matching rule (if any) */
|
||||||
|
matchedRule?: IMacVlanMapping;
|
||||||
|
/** Whether default VLAN was used */
|
||||||
|
isDefault: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VlanManager configuration
|
||||||
|
*/
|
||||||
|
export interface IVlanManagerConfig {
|
||||||
|
/** Default VLAN for unknown MACs */
|
||||||
|
defaultVlan?: number;
|
||||||
|
/** Whether to allow unknown MACs (assign default VLAN) or reject */
|
||||||
|
allowUnknownMacs?: boolean;
|
||||||
|
/** Storage key prefix for persistence */
|
||||||
|
storagePrefix?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages MAC address to VLAN mappings with support for:
|
||||||
|
* - Exact MAC address matching
|
||||||
|
* - OUI (vendor prefix) pattern matching
|
||||||
|
* - Wildcard patterns
|
||||||
|
* - Default VLAN for unknown devices
|
||||||
|
*/
|
||||||
|
export class VlanManager {
|
||||||
|
private mappings: Map<string, IMacVlanMapping> = new Map();
|
||||||
|
private config: Required<IVlanManagerConfig>;
|
||||||
|
private storageManager?: StorageManager;
|
||||||
|
|
||||||
|
// Cache for normalized MAC lookups
|
||||||
|
private normalizedMacCache: Map<string, string> = new Map();
|
||||||
|
|
||||||
|
constructor(config?: IVlanManagerConfig, storageManager?: StorageManager) {
|
||||||
|
this.config = {
|
||||||
|
defaultVlan: config?.defaultVlan ?? 1,
|
||||||
|
allowUnknownMacs: config?.allowUnknownMacs ?? true,
|
||||||
|
storagePrefix: config?.storagePrefix ?? '/radius/vlan-mappings',
|
||||||
|
};
|
||||||
|
this.storageManager = storageManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the VLAN manager and load persisted mappings
|
||||||
|
*/
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
if (this.storageManager) {
|
||||||
|
await this.loadMappings();
|
||||||
|
}
|
||||||
|
logger.log('info', `VlanManager initialized with ${this.mappings.size} mappings, default VLAN: ${this.config.defaultVlan}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a MAC address to lowercase with colons
|
||||||
|
* Accepts formats: 00:11:22:33:44:55, 00-11-22-33-44-55, 001122334455
|
||||||
|
*/
|
||||||
|
normalizeMac(mac: string): string {
|
||||||
|
// Check cache first
|
||||||
|
const cached = this.normalizedMacCache.get(mac);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all separators and convert to lowercase
|
||||||
|
const cleaned = mac.toLowerCase().replace(/[-:]/g, '');
|
||||||
|
|
||||||
|
// Format with colons
|
||||||
|
const normalized = cleaned.match(/.{1,2}/g)?.join(':') || mac.toLowerCase();
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
this.normalizedMacCache.set(mac, normalized);
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a MAC address matches a pattern
|
||||||
|
* Supports:
|
||||||
|
* - Exact match: "00:11:22:33:44:55"
|
||||||
|
* - OUI match: "00:11:22" (matches any device with this vendor prefix)
|
||||||
|
* - Wildcard: "*" (matches all)
|
||||||
|
*/
|
||||||
|
macMatchesPattern(mac: string, pattern: string): boolean {
|
||||||
|
const normalizedMac = this.normalizeMac(mac);
|
||||||
|
const normalizedPattern = this.normalizeMac(pattern);
|
||||||
|
|
||||||
|
// Wildcard matches all
|
||||||
|
if (pattern === '*') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
if (normalizedMac === normalizedPattern) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OUI/prefix match (pattern is shorter than full MAC)
|
||||||
|
if (normalizedPattern.length < 17 && normalizedMac.startsWith(normalizedPattern)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add or update a MAC to VLAN mapping
|
||||||
|
*/
|
||||||
|
async addMapping(mapping: Omit<IMacVlanMapping, 'createdAt' | 'updatedAt'>): Promise<IMacVlanMapping> {
|
||||||
|
const normalizedMac = this.normalizeMac(mapping.mac);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
const existingMapping = this.mappings.get(normalizedMac);
|
||||||
|
const fullMapping: IMacVlanMapping = {
|
||||||
|
...mapping,
|
||||||
|
mac: normalizedMac,
|
||||||
|
createdAt: existingMapping?.createdAt || now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.mappings.set(normalizedMac, fullMapping);
|
||||||
|
|
||||||
|
// Persist to storage
|
||||||
|
if (this.storageManager) {
|
||||||
|
await this.saveMappings();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', `VLAN mapping ${existingMapping ? 'updated' : 'added'}: ${normalizedMac} -> VLAN ${mapping.vlan}`);
|
||||||
|
return fullMapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a MAC to VLAN mapping
|
||||||
|
*/
|
||||||
|
async removeMapping(mac: string): Promise<boolean> {
|
||||||
|
const normalizedMac = this.normalizeMac(mac);
|
||||||
|
const removed = this.mappings.delete(normalizedMac);
|
||||||
|
|
||||||
|
if (removed && this.storageManager) {
|
||||||
|
await this.saveMappings();
|
||||||
|
logger.log('info', `VLAN mapping removed: ${normalizedMac}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific mapping by MAC
|
||||||
|
*/
|
||||||
|
getMapping(mac: string): IMacVlanMapping | undefined {
|
||||||
|
return this.mappings.get(this.normalizeMac(mac));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all mappings
|
||||||
|
*/
|
||||||
|
getAllMappings(): IMacVlanMapping[] {
|
||||||
|
return Array.from(this.mappings.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine VLAN assignment for a MAC address
|
||||||
|
* Returns the most specific matching rule (exact > OUI > wildcard > default)
|
||||||
|
*/
|
||||||
|
assignVlan(mac: string): IVlanAssignmentResult {
|
||||||
|
const normalizedMac = this.normalizeMac(mac);
|
||||||
|
|
||||||
|
// First, try exact match
|
||||||
|
const exactMatch = this.mappings.get(normalizedMac);
|
||||||
|
if (exactMatch && exactMatch.enabled) {
|
||||||
|
return {
|
||||||
|
assigned: true,
|
||||||
|
vlan: exactMatch.vlan,
|
||||||
|
matchedRule: exactMatch,
|
||||||
|
isDefault: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try OUI/prefix matches (sorted by specificity - longer patterns first)
|
||||||
|
const patternMatches: IMacVlanMapping[] = [];
|
||||||
|
for (const mapping of this.mappings.values()) {
|
||||||
|
if (mapping.enabled && mapping.mac !== normalizedMac && this.macMatchesPattern(normalizedMac, mapping.mac)) {
|
||||||
|
patternMatches.push(mapping);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by pattern length (most specific first)
|
||||||
|
patternMatches.sort((a, b) => b.mac.length - a.mac.length);
|
||||||
|
|
||||||
|
if (patternMatches.length > 0) {
|
||||||
|
const bestMatch = patternMatches[0];
|
||||||
|
return {
|
||||||
|
assigned: true,
|
||||||
|
vlan: bestMatch.vlan,
|
||||||
|
matchedRule: bestMatch,
|
||||||
|
isDefault: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// No match - use default VLAN if allowed
|
||||||
|
if (this.config.allowUnknownMacs) {
|
||||||
|
return {
|
||||||
|
assigned: true,
|
||||||
|
vlan: this.config.defaultVlan,
|
||||||
|
isDefault: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown MAC and not allowed
|
||||||
|
return {
|
||||||
|
assigned: false,
|
||||||
|
vlan: 0,
|
||||||
|
isDefault: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk import mappings
|
||||||
|
*/
|
||||||
|
async importMappings(mappings: Array<Omit<IMacVlanMapping, 'createdAt' | 'updatedAt'>>): Promise<number> {
|
||||||
|
let imported = 0;
|
||||||
|
|
||||||
|
for (const mapping of mappings) {
|
||||||
|
await this.addMapping(mapping);
|
||||||
|
imported++;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', `Imported ${imported} VLAN mappings`);
|
||||||
|
return imported;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export all mappings
|
||||||
|
*/
|
||||||
|
exportMappings(): IMacVlanMapping[] {
|
||||||
|
return this.getAllMappings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update configuration
|
||||||
|
*/
|
||||||
|
updateConfig(config: Partial<IVlanManagerConfig>): void {
|
||||||
|
if (config.defaultVlan !== undefined) {
|
||||||
|
this.config.defaultVlan = config.defaultVlan;
|
||||||
|
}
|
||||||
|
if (config.allowUnknownMacs !== undefined) {
|
||||||
|
this.config.allowUnknownMacs = config.allowUnknownMacs;
|
||||||
|
}
|
||||||
|
logger.log('info', `VlanManager config updated: defaultVlan=${this.config.defaultVlan}, allowUnknown=${this.config.allowUnknownMacs}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current configuration
|
||||||
|
*/
|
||||||
|
getConfig(): Required<IVlanManagerConfig> {
|
||||||
|
return { ...this.config };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get statistics
|
||||||
|
*/
|
||||||
|
getStats(): {
|
||||||
|
totalMappings: number;
|
||||||
|
enabledMappings: number;
|
||||||
|
exactMatches: number;
|
||||||
|
ouiPatterns: number;
|
||||||
|
wildcardPatterns: number;
|
||||||
|
} {
|
||||||
|
let exactMatches = 0;
|
||||||
|
let ouiPatterns = 0;
|
||||||
|
let wildcardPatterns = 0;
|
||||||
|
let enabledMappings = 0;
|
||||||
|
|
||||||
|
for (const mapping of this.mappings.values()) {
|
||||||
|
if (mapping.enabled) {
|
||||||
|
enabledMappings++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping.mac === '*') {
|
||||||
|
wildcardPatterns++;
|
||||||
|
} else if (mapping.mac.length < 17) {
|
||||||
|
// OUI patterns are shorter than full MAC (17 chars with colons)
|
||||||
|
ouiPatterns++;
|
||||||
|
} else {
|
||||||
|
exactMatches++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalMappings: this.mappings.size,
|
||||||
|
enabledMappings,
|
||||||
|
exactMatches,
|
||||||
|
ouiPatterns,
|
||||||
|
wildcardPatterns,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load mappings from storage
|
||||||
|
*/
|
||||||
|
private async loadMappings(): Promise<void> {
|
||||||
|
if (!this.storageManager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await this.storageManager.getJSON<IMacVlanMapping[]>(this.config.storagePrefix);
|
||||||
|
if (data && Array.isArray(data)) {
|
||||||
|
for (const mapping of data) {
|
||||||
|
this.mappings.set(this.normalizeMac(mapping.mac), mapping);
|
||||||
|
}
|
||||||
|
logger.log('info', `Loaded ${data.length} VLAN mappings from storage`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.log('warn', `Failed to load VLAN mappings from storage: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save mappings to storage
|
||||||
|
*/
|
||||||
|
private async saveMappings(): Promise<void> {
|
||||||
|
if (!this.storageManager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mappings = Array.from(this.mappings.values());
|
||||||
|
await this.storageManager.setJSON(this.config.storagePrefix, mappings);
|
||||||
|
} catch (error) {
|
||||||
|
logger.log('error', `Failed to save VLAN mappings to storage: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
ts/radius/index.ts
Normal file
14
ts/radius/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* RADIUS module for DcRouter
|
||||||
|
*
|
||||||
|
* Provides:
|
||||||
|
* - MAC Authentication Bypass (MAB) for network device authentication
|
||||||
|
* - VLAN assignment based on MAC addresses
|
||||||
|
* - OUI (vendor prefix) pattern matching for device categorization
|
||||||
|
* - RADIUS accounting for session tracking and billing
|
||||||
|
* - Integration with StorageManager for persistence
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './classes.radius.server.js';
|
||||||
|
export * from './classes.vlan.manager.js';
|
||||||
|
export * from './classes.accounting.manager.js';
|
||||||
@@ -2,4 +2,5 @@ export * from './admin.js';
|
|||||||
export * from './config.js';
|
export * from './config.js';
|
||||||
export * from './logs.js';
|
export * from './logs.js';
|
||||||
export * from './stats.js';
|
export * from './stats.js';
|
||||||
export * from './combined.stats.js';
|
export * from './combined.stats.js';
|
||||||
|
export * from './radius.js';
|
||||||
329
ts_interfaces/requests/radius.ts
Normal file
329
ts_interfaces/requests/radius.ts
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import * as authInterfaces from '../data/auth.js';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// RADIUS Client Management
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all RADIUS clients (NAS devices)
|
||||||
|
*/
|
||||||
|
export interface IReq_GetRadiusClients extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_GetRadiusClients
|
||||||
|
> {
|
||||||
|
method: 'getRadiusClients';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
clients: Array<{
|
||||||
|
name: string;
|
||||||
|
ipRange: string;
|
||||||
|
description?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add or update a RADIUS client
|
||||||
|
*/
|
||||||
|
export interface IReq_SetRadiusClient extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_SetRadiusClient
|
||||||
|
> {
|
||||||
|
method: 'setRadiusClient';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
client: {
|
||||||
|
name: string;
|
||||||
|
ipRange: string;
|
||||||
|
secret: string;
|
||||||
|
description?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a RADIUS client
|
||||||
|
*/
|
||||||
|
export interface IReq_RemoveRadiusClient extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_RemoveRadiusClient
|
||||||
|
> {
|
||||||
|
method: 'removeRadiusClient';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// VLAN Mapping Management
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all MAC-to-VLAN mappings
|
||||||
|
*/
|
||||||
|
export interface IReq_GetVlanMappings extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_GetVlanMappings
|
||||||
|
> {
|
||||||
|
method: 'getVlanMappings';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
mappings: Array<{
|
||||||
|
mac: string;
|
||||||
|
vlan: number;
|
||||||
|
description?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
}>;
|
||||||
|
config: {
|
||||||
|
defaultVlan: number;
|
||||||
|
allowUnknownMacs: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add or update a VLAN mapping
|
||||||
|
*/
|
||||||
|
export interface IReq_SetVlanMapping extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_SetVlanMapping
|
||||||
|
> {
|
||||||
|
method: 'setVlanMapping';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
mapping: {
|
||||||
|
mac: string;
|
||||||
|
vlan: number;
|
||||||
|
description?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
mapping?: {
|
||||||
|
mac: string;
|
||||||
|
vlan: number;
|
||||||
|
description?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
};
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a VLAN mapping
|
||||||
|
*/
|
||||||
|
export interface IReq_RemoveVlanMapping extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_RemoveVlanMapping
|
||||||
|
> {
|
||||||
|
method: 'removeVlanMapping';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
mac: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update VLAN configuration
|
||||||
|
*/
|
||||||
|
export interface IReq_UpdateVlanConfig extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_UpdateVlanConfig
|
||||||
|
> {
|
||||||
|
method: 'updateVlanConfig';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
defaultVlan?: number;
|
||||||
|
allowUnknownMacs?: boolean;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
config: {
|
||||||
|
defaultVlan: number;
|
||||||
|
allowUnknownMacs: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test VLAN assignment for a MAC address
|
||||||
|
*/
|
||||||
|
export interface IReq_TestVlanAssignment extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_TestVlanAssignment
|
||||||
|
> {
|
||||||
|
method: 'testVlanAssignment';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
mac: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
assigned: boolean;
|
||||||
|
vlan: number;
|
||||||
|
isDefault: boolean;
|
||||||
|
matchedRule?: {
|
||||||
|
mac: string;
|
||||||
|
vlan: number;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Accounting / Session Management
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active RADIUS sessions
|
||||||
|
*/
|
||||||
|
export interface IReq_GetRadiusSessions extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_GetRadiusSessions
|
||||||
|
> {
|
||||||
|
method: 'getRadiusSessions';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
filter?: {
|
||||||
|
username?: string;
|
||||||
|
nasIpAddress?: string;
|
||||||
|
vlanId?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
sessions: Array<{
|
||||||
|
sessionId: string;
|
||||||
|
username: string;
|
||||||
|
macAddress?: string;
|
||||||
|
nasIpAddress: string;
|
||||||
|
nasIdentifier?: string;
|
||||||
|
vlanId?: number;
|
||||||
|
framedIpAddress?: string;
|
||||||
|
startTime: number;
|
||||||
|
lastUpdateTime: number;
|
||||||
|
status: 'active' | 'stopped' | 'terminated';
|
||||||
|
inputOctets: number;
|
||||||
|
outputOctets: number;
|
||||||
|
sessionTime: number;
|
||||||
|
}>;
|
||||||
|
totalCount: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect a RADIUS session
|
||||||
|
*/
|
||||||
|
export interface IReq_DisconnectRadiusSession extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_DisconnectRadiusSession
|
||||||
|
> {
|
||||||
|
method: 'disconnectRadiusSession';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
sessionId: string;
|
||||||
|
reason?: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get accounting summary/report
|
||||||
|
*/
|
||||||
|
export interface IReq_GetRadiusAccountingSummary extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_GetRadiusAccountingSummary
|
||||||
|
> {
|
||||||
|
method: 'getRadiusAccountingSummary';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
summary: {
|
||||||
|
periodStart: number;
|
||||||
|
periodEnd: number;
|
||||||
|
totalSessions: number;
|
||||||
|
activeSessions: number;
|
||||||
|
totalInputBytes: number;
|
||||||
|
totalOutputBytes: number;
|
||||||
|
totalSessionTime: number;
|
||||||
|
averageSessionDuration: number;
|
||||||
|
uniqueUsers: number;
|
||||||
|
sessionsByVlan: Record<number, number>;
|
||||||
|
topUsersByTraffic: Array<{ username: string; totalBytes: number }>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Statistics
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get RADIUS server statistics
|
||||||
|
*/
|
||||||
|
export interface IReq_GetRadiusStatistics extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_GetRadiusStatistics
|
||||||
|
> {
|
||||||
|
method: 'getRadiusStatistics';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
stats: {
|
||||||
|
running: boolean;
|
||||||
|
uptime: number;
|
||||||
|
authRequests: number;
|
||||||
|
authAccepts: number;
|
||||||
|
authRejects: number;
|
||||||
|
accountingRequests: number;
|
||||||
|
activeSessions: number;
|
||||||
|
vlanMappings: number;
|
||||||
|
clients: number;
|
||||||
|
};
|
||||||
|
vlanStats: {
|
||||||
|
totalMappings: number;
|
||||||
|
enabledMappings: number;
|
||||||
|
exactMatches: number;
|
||||||
|
ouiPatterns: number;
|
||||||
|
wildcardPatterns: number;
|
||||||
|
};
|
||||||
|
accountingStats: {
|
||||||
|
activeSessions: number;
|
||||||
|
totalSessionsStarted: number;
|
||||||
|
totalSessionsStopped: number;
|
||||||
|
totalInputBytes: number;
|
||||||
|
totalOutputBytes: number;
|
||||||
|
interimUpdatesReceived: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '2.12.6',
|
version: '2.13.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ import { OpsViewSecurity } from './ops-view-security.js';
|
|||||||
|
|
||||||
@customElement('ops-dashboard')
|
@customElement('ops-dashboard')
|
||||||
export class OpsDashboard extends DeesElement {
|
export class OpsDashboard extends DeesElement {
|
||||||
@state() private loginState: appstate.ILoginState = {
|
@state() accessor loginState: appstate.ILoginState = {
|
||||||
identity: null,
|
identity: null,
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@state() private uiState: appstate.IUiState = {
|
@state() accessor uiState: appstate.IUiState = {
|
||||||
activeView: 'overview',
|
activeView: 'overview',
|
||||||
sidebarCollapsed: false,
|
sidebarCollapsed: false,
|
||||||
autoRefresh: true,
|
autoRefresh: true,
|
||||||
|
|||||||
@@ -14,17 +14,17 @@ import {
|
|||||||
@customElement('ops-view-config')
|
@customElement('ops-view-config')
|
||||||
export class OpsViewConfig extends DeesElement {
|
export class OpsViewConfig extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private configState: appstate.IConfigState = {
|
accessor configState: appstate.IConfigState = {
|
||||||
config: null,
|
config: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private editingSection: string | null = null;
|
accessor editingSection: string | null = null;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private editedConfig: any = null;
|
accessor editedConfig: any = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|||||||
@@ -33,25 +33,25 @@ interface IEmail {
|
|||||||
@customElement('ops-view-emails')
|
@customElement('ops-view-emails')
|
||||||
export class OpsViewEmails extends DeesElement {
|
export class OpsViewEmails extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private selectedFolder: 'inbox' | 'sent' | 'draft' | 'trash' = 'inbox';
|
accessor selectedFolder: 'inbox' | 'sent' | 'draft' | 'trash' = 'inbox';
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private emails: IEmail[] = [];
|
accessor emails: IEmail[] = [];
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private selectedEmail: IEmail | null = null;
|
accessor selectedEmail: IEmail | null = null;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private showCompose = false;
|
accessor showCompose = false;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private isLoading = false;
|
accessor isLoading = false;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private searchTerm = '';
|
accessor searchTerm = '';
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private emailDomains: string[] = [];
|
accessor emailDomains: string[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
@customElement('ops-view-logs')
|
@customElement('ops-view-logs')
|
||||||
export class OpsViewLogs extends DeesElement {
|
export class OpsViewLogs extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private logState: appstate.ILogState = {
|
accessor logState: appstate.ILogState = {
|
||||||
recentLogs: [],
|
recentLogs: [],
|
||||||
isStreaming: false,
|
isStreaming: false,
|
||||||
filters: {},
|
filters: {},
|
||||||
|
|||||||
@@ -28,20 +28,20 @@ interface INetworkRequest {
|
|||||||
@customElement('ops-view-network')
|
@customElement('ops-view-network')
|
||||||
export class OpsViewNetwork extends DeesElement {
|
export class OpsViewNetwork extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private statsState = appstate.statsStatePart.getState();
|
accessor statsState = appstate.statsStatePart.getState();
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private networkState = appstate.networkStatePart.getState();
|
accessor networkState = appstate.networkStatePart.getState();
|
||||||
|
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private networkRequests: INetworkRequest[] = [];
|
accessor networkRequests: INetworkRequest[] = [];
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private trafficDataIn: Array<{ x: string | number; y: number }> = [];
|
accessor trafficDataIn: Array<{ x: string | number; y: number }> = [];
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private trafficDataOut: Array<{ x: string | number; y: number }> = [];
|
accessor trafficDataOut: Array<{ x: string | number; y: number }> = [];
|
||||||
|
|
||||||
// Track if we need to update the chart to avoid unnecessary re-renders
|
// Track if we need to update the chart to avoid unnecessary re-renders
|
||||||
private lastChartUpdate = 0;
|
private lastChartUpdate = 0;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { type IStatsTile } from '@design.estate/dees-catalog';
|
|||||||
@customElement('ops-view-overview')
|
@customElement('ops-view-overview')
|
||||||
export class OpsViewOverview extends DeesElement {
|
export class OpsViewOverview extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private statsState: appstate.IStatsState = {
|
accessor statsState: appstate.IStatsState = {
|
||||||
serverStats: null,
|
serverStats: null,
|
||||||
emailStats: null,
|
emailStats: null,
|
||||||
dnsStats: null,
|
dnsStats: null,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { type IStatsTile } from '@design.estate/dees-catalog';
|
|||||||
@customElement('ops-view-security')
|
@customElement('ops-view-security')
|
||||||
export class OpsViewSecurity extends DeesElement {
|
export class OpsViewSecurity extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private statsState: appstate.IStatsState = {
|
accessor statsState: appstate.IStatsState = {
|
||||||
serverStats: null,
|
serverStats: null,
|
||||||
emailStats: null,
|
emailStats: null,
|
||||||
dnsStats: null,
|
dnsStats: null,
|
||||||
@@ -26,7 +26,7 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private selectedTab: 'overview' | 'blocked' | 'authentication' | 'email-security' = 'overview';
|
accessor selectedTab: 'overview' | 'blocked' | 'authentication' | 'email-security' = 'overview';
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|||||||
Reference in New Issue
Block a user