From be9f49fff969aaff09f27b3cea7520cc3c49f0e2 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sun, 1 Feb 2026 17:40:36 +0000 Subject: [PATCH] feat(smartradius): Implement full RADIUS server and client with RFC 2865/2866 compliance, including packet handling, authenticators, attributes, secrets manager, client APIs, and comprehensive tests and documentation --- changelog.md | 17 + npmextra.json | 8 +- package.json | 41 +- pnpm-lock.yaml | 69 +- readme.hints.md | 111 + readme.md | 323 +- spec/rfc2865.txt | 4259 ++++++++++++++++++++++ spec/rfc2866.txt | 1571 ++++++++ test/client/test.client.ts | 167 + test/client/test.integration.ts | 304 ++ test/client/test.timeout.ts | 149 + test/server/test.accounting.ts | 246 ++ test/server/test.attributes.ts | 211 ++ test/server/test.authenticator.ts | 205 ++ test/server/test.chap.ts | 209 ++ test/server/test.packet.ts | 190 + test/server/test.pap.ts | 282 ++ test/test.ts | 8 - ts/00_commitinfo_data.ts | 8 + ts/index.ts | 20 +- ts/paths.ts | 5 - ts/plugins.ts | 9 - ts/readme.md | 93 + ts/tspublish.json | 1 + ts_client/classes.radiusclient.ts | 531 +++ ts_client/index.ts | 4 + ts_client/interfaces.ts | 96 + ts_client/plugins.ts | 13 + ts_client/readme.md | 151 + ts_client/tspublish.json | 1 + ts_server/classes.radiusattributes.ts | 303 ++ ts_server/classes.radiusauthenticator.ts | 302 ++ ts_server/classes.radiuspacket.ts | 426 +++ ts_server/classes.radiussecrets.ts | 116 + ts_server/classes.radiusserver.ts | 649 ++++ ts_server/index.ts | 9 + ts_server/interfaces.ts | 140 + ts_server/plugins.ts | 7 + ts_server/readme.md | 135 + ts_server/tspublish.json | 1 + ts_shared/enums.ts | 222 ++ ts_shared/index.ts | 5 + ts_shared/interfaces.ts | 65 + ts_shared/readme.md | 81 + ts_shared/tspublish.json | 1 + 45 files changed, 11694 insertions(+), 70 deletions(-) create mode 100644 changelog.md create mode 100644 readme.hints.md create mode 100644 spec/rfc2865.txt create mode 100644 spec/rfc2866.txt create mode 100644 test/client/test.client.ts create mode 100644 test/client/test.integration.ts create mode 100644 test/client/test.timeout.ts create mode 100644 test/server/test.accounting.ts create mode 100644 test/server/test.attributes.ts create mode 100644 test/server/test.authenticator.ts create mode 100644 test/server/test.chap.ts create mode 100644 test/server/test.packet.ts create mode 100644 test/server/test.pap.ts delete mode 100644 test/test.ts create mode 100644 ts/00_commitinfo_data.ts delete mode 100644 ts/paths.ts delete mode 100644 ts/plugins.ts create mode 100644 ts/readme.md create mode 100644 ts/tspublish.json create mode 100644 ts_client/classes.radiusclient.ts create mode 100644 ts_client/index.ts create mode 100644 ts_client/interfaces.ts create mode 100644 ts_client/plugins.ts create mode 100644 ts_client/readme.md create mode 100644 ts_client/tspublish.json create mode 100644 ts_server/classes.radiusattributes.ts create mode 100644 ts_server/classes.radiusauthenticator.ts create mode 100644 ts_server/classes.radiuspacket.ts create mode 100644 ts_server/classes.radiussecrets.ts create mode 100644 ts_server/classes.radiusserver.ts create mode 100644 ts_server/index.ts create mode 100644 ts_server/interfaces.ts create mode 100644 ts_server/plugins.ts create mode 100644 ts_server/readme.md create mode 100644 ts_server/tspublish.json create mode 100644 ts_shared/enums.ts create mode 100644 ts_shared/index.ts create mode 100644 ts_shared/interfaces.ts create mode 100644 ts_shared/readme.md create mode 100644 ts_shared/tspublish.json diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..d2a2c33 --- /dev/null +++ b/changelog.md @@ -0,0 +1,17 @@ +# Changelog + +## 2026-02-01 - 1.1.0 - feat(smartradius) +Implement full RADIUS server and client with RFC 2865/2866 compliance, including packet handling, authenticators, attributes, secrets manager, client APIs, and comprehensive tests and documentation + +- Add ts_shared module with protocol enums and core interfaces (RFC 2865/2866) +- Add ts_server module: RadiusServer implementation, RadiusPacket encoder/decoder, RadiusAttributes, RadiusAuthenticator, RadiusSecrets and server README +- Add ts_client module: RadiusClient implementation, client interfaces, plugins and client README +- Add comprehensive unit and integration tests (PAP, CHAP, packet, attributes, authenticator, accounting, timeouts, client-server integration) under test/ +- Update package.json: description, build script (tsfolders), bumped devDependencies, new runtime dependencies (smartdelay, smartpromise), files list and keywords +- Add docs and helper files: README updates, ts module READMEs, readme.hints.md, and tspublish metadata for subpackages + +## 2026-02-01 - 1.0.1 - initial release +Initial commit and project scaffold. + +- Initial commit ("initial") +- Repository initialized and first release tagged as 1.0.1 \ No newline at end of file diff --git a/npmextra.json b/npmextra.json index 2663d39..c2726f9 100644 --- a/npmextra.json +++ b/npmextra.json @@ -11,10 +11,14 @@ "projectDomain": "push.rocks" }, "release": { - "accessLevel": "public" + "accessLevel": "public", + "registries": [ + "https://verdaccio.lossless.digital", + "https://registry.npmjs.org" + ] } }, "@ship.zone/szci": { "npmGlobalTools": [] } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 3118a15..0aa2769 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@push.rocks/smartradius", "version": "1.0.1", "private": false, - "description": "a radius server implementation", + "description": "A RADIUS server and client implementation for Node.js with full RFC 2865/2866 compliance", "main": "dist_ts/index.js", "typings": "dist_ts/index.d.ts", "type": "module", @@ -10,16 +10,41 @@ "license": "MIT", "scripts": { "test": "(tstest test/ --verbose --logfile --timeout 60)", - "build": "(tsbuild --web --allowimplicitany)", + "build": "(tsbuild tsfolders --allowimplicitany)", "buildDocs": "(tsdoc)" }, "devDependencies": { - "@git.zone/tsbuild": "^3.1.2", - "@git.zone/tsrun": "^2.0.0", - "@git.zone/tstest": "^3.1.3", - "@types/node": "^24.10.1" + "@git.zone/tsbuild": "^4.1.2", + "@git.zone/tsrun": "^2.0.1", + "@git.zone/tstest": "^3.1.8", + "@types/node": "^25.2.0" }, "dependencies": { - "@push.rocks/smartpath": "^6.0.0" - } + "@push.rocks/smartdelay": "^3.0.5", + "@push.rocks/smartpromise": "^4.2.3" + }, + "files": [ + "ts/**/*", + "ts_shared/**/*", + "ts_server/**/*", + "ts_client/**/*", + "dist_ts/**/*", + "dist_ts_shared/**/*", + "dist_ts_server/**/*", + "dist_ts_client/**/*", + "readme.md" + ], + "keywords": [ + "radius", + "authentication", + "accounting", + "aaa", + "pap", + "chap", + "rfc2865", + "rfc2866", + "network", + "server", + "client" + ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b5f277..dac640b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,22 +8,25 @@ importers: .: dependencies: - '@push.rocks/smartpath': - specifier: ^6.0.0 - version: 6.0.0 + '@push.rocks/smartdelay': + specifier: ^3.0.5 + version: 3.0.5 + '@push.rocks/smartpromise': + specifier: ^4.2.3 + version: 4.2.3 devDependencies: '@git.zone/tsbuild': - specifier: ^3.1.2 - version: 3.1.4 + specifier: ^4.1.2 + version: 4.1.2 '@git.zone/tsrun': - specifier: ^2.0.0 + specifier: ^2.0.1 version: 2.0.1 '@git.zone/tstest': - specifier: ^3.1.3 + specifier: ^3.1.8 version: 3.1.8(socks@2.8.7)(typescript@5.9.3) '@types/node': - specifier: ^24.10.1 - version: 24.10.9 + specifier: ^25.2.0 + version: 25.2.0 packages: @@ -405,8 +408,8 @@ packages: cpu: [x64] os: [win32] - '@git.zone/tsbuild@3.1.4': - resolution: {integrity: sha512-nZ5UaOx2GZbhW9T9zWzKiWMZOa6hjX9s9AeTo4SLdrjqCCsSZDtORl4hOpGXEwHbXjiMkbARTQbRcmcCU7HUkw==} + '@git.zone/tsbuild@4.1.2': + resolution: {integrity: sha512-S518ulKveO76pS6jrAELrnFaCw5nDAIZD9j6QzVmLYDiZuJmlRwPK3/2E8ugQ+b7ffpkwJ9MT685ooEGDcWQ4Q==} hasBin: true '@git.zone/tsbundle@2.8.3': @@ -1369,8 +1372,8 @@ packages: '@types/node@22.19.7': resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==} - '@types/node@24.10.9': - resolution: {integrity: sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==} + '@types/node@25.2.0': + resolution: {integrity: sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==} '@types/ping@0.4.4': resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==} @@ -4167,7 +4170,7 @@ snapshots: '@esbuild/win32-x64@0.27.2': optional: true - '@git.zone/tsbuild@3.1.4': + '@git.zone/tsbuild@4.1.2': dependencies: '@git.zone/tspublish': 1.11.0 '@push.rocks/early': 4.0.4 @@ -5845,27 +5848,27 @@ snapshots: '@types/bn.js@5.2.0': dependencies: - '@types/node': 24.10.9 + '@types/node': 25.2.0 '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 24.10.9 + '@types/node': 25.2.0 '@types/buffer-json@2.0.3': {} '@types/clean-css@4.2.11': dependencies: - '@types/node': 24.10.9 + '@types/node': 25.2.0 source-map: 0.6.1 '@types/connect@3.4.38': dependencies: - '@types/node': 24.10.9 + '@types/node': 25.2.0 '@types/cors@2.8.19': dependencies: - '@types/node': 24.10.9 + '@types/node': 25.2.0 '@types/debug@4.1.12': dependencies: @@ -5873,7 +5876,7 @@ snapshots: '@types/dns-packet@5.6.5': dependencies: - '@types/node': 24.10.9 + '@types/node': 25.2.0 '@types/elliptic@6.4.18': dependencies: @@ -5881,7 +5884,7 @@ snapshots: '@types/express-serve-static-core@5.1.1': dependencies: - '@types/node': 24.10.9 + '@types/node': 25.2.0 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 @@ -5895,7 +5898,7 @@ snapshots: '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 - '@types/node': 24.10.9 + '@types/node': 25.2.0 '@types/hast@3.0.4': dependencies: @@ -5917,7 +5920,7 @@ snapshots: '@types/jsonfile@6.1.4': dependencies: - '@types/node': 24.10.9 + '@types/node': 25.2.0 '@types/mdast@4.0.4': dependencies: @@ -5931,17 +5934,17 @@ snapshots: '@types/mute-stream@0.0.4': dependencies: - '@types/node': 24.10.9 + '@types/node': 25.2.0 '@types/node-forge@1.3.14': dependencies: - '@types/node': 24.10.9 + '@types/node': 25.2.0 '@types/node@22.19.7': dependencies: undici-types: 6.21.0 - '@types/node@24.10.9': + '@types/node@25.2.0': dependencies: undici-types: 7.16.0 @@ -5959,22 +5962,22 @@ snapshots: '@types/send@1.2.1': dependencies: - '@types/node': 24.10.9 + '@types/node': 25.2.0 '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 24.10.9 + '@types/node': 25.2.0 '@types/symbol-tree@3.2.5': {} '@types/tar-stream@3.1.4': dependencies: - '@types/node': 24.10.9 + '@types/node': 25.2.0 '@types/through2@2.0.41': dependencies: - '@types/node': 24.10.9 + '@types/node': 25.2.0 '@types/trusted-types@2.0.7': {} @@ -6000,11 +6003,11 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 24.10.9 + '@types/node': 25.2.0 '@types/yauzl@2.10.3': dependencies: - '@types/node': 24.10.9 + '@types/node': 25.2.0 optional: true '@ungap/structured-clone@1.3.0': {} @@ -6417,7 +6420,7 @@ snapshots: engine.io@6.6.4: dependencies: '@types/cors': 2.8.19 - '@types/node': 24.10.9 + '@types/node': 25.2.0 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.7.2 diff --git a/readme.hints.md b/readme.hints.md new file mode 100644 index 0000000..fbff29a --- /dev/null +++ b/readme.hints.md @@ -0,0 +1,111 @@ +# Project Hints - smartradius + +## Project Status +- **Current State**: Fully implemented RADIUS server and client +- **Purpose**: RADIUS protocol implementation for network AAA (Authentication, Authorization, Accounting) +- **Version**: 1.0.1 +- **RFC Compliance**: RFC 2865 (Authentication) and RFC 2866 (Accounting) + +## Architecture + +### Module Structure +``` +ts_server/ (order: 1) - RADIUS Server implementation +ts_client/ (order: 2) - RADIUS Client implementation +ts/ (order: 3) - Main exports (re-exports server + client) +``` + +### Key Classes + +#### Server Module (ts_server/) +- `RadiusServer` - Main server class with UDP listeners for auth (1812) and accounting (1813) +- `RadiusPacket` - Packet encoding/decoding per RFC 2865 Section 3 +- `RadiusAttributes` - Attribute dictionary with all standard RFC 2865/2866 attributes +- `RadiusAuthenticator` - Cryptographic operations (PAP, CHAP, MD5, HMAC-MD5) +- `RadiusSecrets` - Per-client shared secret management + +#### Client Module (ts_client/) +- `RadiusClient` - Client with PAP/CHAP auth and accounting, timeout/retry support + +## Implemented Features + +### Authentication (RFC 2865) +- PAP (Password Authentication Protocol) with MD5-based encryption +- CHAP (Challenge-Handshake Authentication Protocol) +- Access-Request/Accept/Reject/Challenge packet handling +- Message-Authenticator (HMAC-MD5) for EAP support +- All standard attributes (1-63) plus EAP support (79, 80) + +### Accounting (RFC 2866) +- Accounting-Request/Response packets +- Status types: Start, Stop, Interim-Update, Accounting-On/Off +- Full session tracking attributes +- Termination cause codes + +### Protocol Features +- Duplicate request detection and response caching +- Response authenticator verification +- Configurable timeout and retry with exponential backoff +- Per-client shared secret management +- Vendor-Specific Attributes (VSA) support + +## Dependencies +```json +{ + "@push.rocks/smartdelay": "^3.0.5", + "@push.rocks/smartpromise": "^4.2.3" +} +``` + +Node.js built-ins: `dgram` (UDP), `crypto` (MD5/HMAC) + +## Build System +- Uses `@git.zone/tsbuild` v4.x with tsfolders mode +- Build command: `pnpm build` (compiles ts_server โ†’ ts_client โ†’ ts) +- Test command: `pnpm test` + +## Test Coverage +- 92 tests across 9 test files +- Server tests: packet, attributes, authenticator, PAP, CHAP, accounting +- Client tests: client functionality, timeout/retry, integration + +## Usage Examples + +### Server +```typescript +import { RadiusServer, ERadiusCode } from '@push.rocks/smartradius'; + +const server = new RadiusServer({ + authPort: 1812, + acctPort: 1813, + defaultSecret: 'shared-secret', + authenticationHandler: async (request) => { + if (request.username === 'user' && request.password === 'pass') { + return { code: ERadiusCode.AccessAccept }; + } + return { code: ERadiusCode.AccessReject }; + }, +}); +await server.start(); +``` + +### Client +```typescript +import { RadiusClient } from '@push.rocks/smartradius'; + +const client = new RadiusClient({ + host: '127.0.0.1', + secret: 'shared-secret', +}); +await client.connect(); +const response = await client.authenticatePap('user', 'pass'); +console.log(response.accepted); +``` + +## RFC Specifications +Downloaded to `./spec/`: +- `rfc2865.txt` - RADIUS Authentication +- `rfc2866.txt` - RADIUS Accounting + +## Last Updated +2026-02-01 - Full implementation complete with RFC 2865/2866 compliance diff --git a/readme.md b/readme.md index 4ca52c8..4ca66e0 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,322 @@ # @push.rocks/smartradius -a radius server implementation -## How to create the docs -To create docs run gitzone aidoc. \ No newline at end of file +A TypeScript RADIUS server and client library with full RFC 2865/2866 compliance for network authentication, authorization, and accounting (AAA). + +## Issue Reporting and Security + +For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly. + +## Install + +Install via npm or pnpm: + +```bash +npm install @push.rocks/smartradius +# or +pnpm add @push.rocks/smartradius +``` + +## Usage + +This module provides a TypeScript-based RADIUS (Remote Authentication Dial-In User Service) server and client implementation. RADIUS is a networking protocol that provides centralized Authentication, Authorization, and Accounting (AAA) management for users who connect and use a network service. + +### What is RADIUS? + +RADIUS is an industry-standard protocol used for: + +- ๐Ÿ” **Authentication** - Verifying user credentials (username/password via PAP or CHAP) +- ๐ŸŽซ **Authorization** - Determining what resources authenticated users can access +- ๐Ÿ“Š **Accounting** - Tracking network usage for billing, auditing, and statistics + +### Common Use Cases + +- ๐Ÿ“ก **Enterprise Wi-Fi** - 802.1X authentication for wireless networks +- ๐Ÿ”’ **VPN Access** - Authenticating remote users connecting via VPN +- ๐Ÿ›ก๏ธ **Network Access Control** - Managing who can access network resources +- ๐ŸŒ **ISP Authentication** - Authenticating dial-up and broadband users +- ๐Ÿ“ฑ **Device Authentication** - IoT and network device access control + +### Architecture + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” RADIUS โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” Auth โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Client โ”‚ โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚ RADIUS Server โ”‚ โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚ User Store โ”‚ +โ”‚ (NAS/AP) โ”‚ UDP 1812/1813 โ”‚ (smartradius) โ”‚ (Backend) โ”‚ (LDAP/DB) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Creating a RADIUS Server + +```typescript +import { RadiusServer, ERadiusCode } from '@push.rocks/smartradius'; + +// Create a RADIUS server +const server = new RadiusServer({ + authPort: 1812, // Authentication port (default) + acctPort: 1813, // Accounting port (default) + bindAddress: '0.0.0.0', // Listen on all interfaces + defaultSecret: 'shared-secret-with-clients', + + // Authentication handler + authenticationHandler: async (request) => { + console.log(`Auth request from ${request.username}`); + + // PAP authentication + if (request.password !== undefined) { + if (request.username === 'testuser' && request.password === 'testpass') { + return { + code: ERadiusCode.AccessAccept, + replyMessage: 'Welcome!', + sessionTimeout: 3600, + framedIpAddress: '10.0.0.100', + }; + } + } + + // CHAP authentication + if (request.chapPassword && request.chapChallenge) { + const { RadiusAuthenticator } = await import('@push.rocks/smartradius'); + const isValid = RadiusAuthenticator.verifyChapResponse( + request.chapPassword, + request.chapChallenge, + 'expected-password' + ); + if (isValid) { + return { code: ERadiusCode.AccessAccept }; + } + } + + return { + code: ERadiusCode.AccessReject, + replyMessage: 'Invalid credentials', + }; + }, + + // Accounting handler + accountingHandler: async (request) => { + console.log(`Accounting ${request.statusType}: session ${request.sessionId}`); + // Record accounting data to database + return { success: true }; + }, +}); + +// Start the server +await server.start(); +console.log('RADIUS server running on ports 1812/1813'); + +// Per-client secrets (optional) +server.setClientSecret('192.168.1.100', 'client-specific-secret'); + +// Get statistics +const stats = server.getStats(); +console.log(`Processed ${stats.authRequests} auth requests`); + +// Stop the server +await server.stop(); +``` + +### Creating a RADIUS Client + +```typescript +import { RadiusClient, EAcctStatusType } from '@push.rocks/smartradius'; + +// Create a RADIUS client +const client = new RadiusClient({ + host: '192.168.1.1', + authPort: 1812, + acctPort: 1813, + secret: 'shared-secret', + timeout: 5000, // 5 second timeout + retries: 3, // Retry 3 times + nasIdentifier: 'my-nas-device', +}); + +// Connect (binds UDP socket) +await client.connect(); + +// PAP Authentication +const papResponse = await client.authenticatePap('username', 'password'); +if (papResponse.accepted) { + console.log('PAP Auth successful!'); + console.log('Session timeout:', papResponse.sessionTimeout); + console.log('Assigned IP:', papResponse.framedIpAddress); +} + +// CHAP Authentication +const chapResponse = await client.authenticateChap('username', 'password'); +if (chapResponse.accepted) { + console.log('CHAP Auth successful!'); +} + +// Authentication with custom attributes +const customResponse = await client.authenticate({ + username: 'user', + password: 'pass', + nasPort: 1, + calledStationId: 'AA-BB-CC-DD-EE-FF', + callingStationId: '11-22-33-44-55-66', +}); + +// Accounting: Session Start +await client.accountingStart('session-001', 'username'); + +// Accounting: Interim Update +await client.accountingUpdate('session-001', { + username: 'username', + sessionTime: 300, // 5 minutes + inputOctets: 1024000, // 1 MB received + outputOctets: 2048000, // 2 MB sent +}); + +// Accounting: Session Stop +await client.accountingStop('session-001', { + username: 'username', + sessionTime: 600, + inputOctets: 2048000, + outputOctets: 4096000, + terminateCause: 1, // User-Request +}); + +// Disconnect +await client.disconnect(); +``` + +### Working with RADIUS Packets Directly + +```typescript +import { + RadiusPacket, + RadiusAuthenticator, + RadiusAttributes, + ERadiusCode, + ERadiusAttributeType, +} from '@push.rocks/smartradius'; + +// Create an Access-Request packet +const packet = RadiusPacket.createAccessRequest(1, 'secret', [ + { type: ERadiusAttributeType.UserName, value: 'testuser' }, + { type: ERadiusAttributeType.UserPassword, value: 'testpass' }, + { type: ERadiusAttributeType.NasIpAddress, value: '192.168.1.1' }, +]); + +// Decode a received packet +const decoded = RadiusPacket.decodeAndParse(receivedBuffer); +console.log('Code:', RadiusPacket.getCodeName(decoded.code)); +console.log('Attributes:', decoded.parsedAttributes); + +// Verify response authenticator +const isValid = RadiusAuthenticator.verifyResponseAuthenticator( + responseBuffer, + requestAuthenticator, + secret +); + +// Encrypt/decrypt PAP password +const encrypted = RadiusAuthenticator.encryptPassword(password, authenticator, secret); +const decrypted = RadiusAuthenticator.decryptPassword(encrypted, authenticator, secret); + +// Calculate CHAP response +const chapResponse = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge); + +// Create Vendor-Specific Attribute +const vsaAttr = RadiusAttributes.createVendorAttribute( + 9, // Cisco vendor ID + 1, // Vendor-specific type + Buffer.from('cisco-av-pair=value') +); +``` + +### Supported Features + +#### Authentication (RFC 2865) +- โœ… PAP (Password Authentication Protocol) +- โœ… CHAP (Challenge-Handshake Authentication Protocol) +- โœ… Access-Request, Access-Accept, Access-Reject, Access-Challenge packets +- โœ… Message-Authenticator (HMAC-MD5) for EAP support +- โœ… All standard attributes (1-63) plus EAP attributes (79, 80) + +#### Accounting (RFC 2866) +- โœ… Accounting-Request, Accounting-Response packets +- โœ… Status types: Start, Stop, Interim-Update, Accounting-On/Off +- โœ… Session tracking: time, octets, packets +- โœ… All termination cause codes + +#### Protocol Features +- โœ… Duplicate request detection with response caching +- โœ… Response authenticator verification +- โœ… Configurable timeout and retry with exponential backoff +- โœ… Per-client shared secret management +- โœ… Vendor-Specific Attributes (VSA) support + +### Enums and Types + +```typescript +// Packet codes +enum ERadiusCode { + AccessRequest = 1, + AccessAccept = 2, + AccessReject = 3, + AccountingRequest = 4, + AccountingResponse = 5, + AccessChallenge = 11, +} + +// Accounting status types +enum EAcctStatusType { + Start = 1, + Stop = 2, + InterimUpdate = 3, + AccountingOn = 7, + AccountingOff = 8, +} + +// Termination causes +enum EAcctTerminateCause { + UserRequest = 1, + LostCarrier = 2, + IdleTimeout = 4, + SessionTimeout = 5, + AdminReset = 6, + // ... and more +} +``` + +## Module Structure + +This library is organized into sub-modules for clean separation of concerns: + +| Module | Description | +|--------|-------------| +| `ts_shared` | Protocol definitions - RFC enums and core interfaces | +| `ts_server` | Server implementation - RadiusServer, packet handling, crypto | +| `ts_client` | Client implementation - RadiusClient with retry logic | +| `ts` | Main entry point - re-exports everything | + +## RFC Compliance + +This implementation follows: +- **RFC 2865** - Remote Authentication Dial In User Service (RADIUS) +- **RFC 2866** - RADIUS Accounting + +The RFC specification files are included in the `./spec/` directory for reference. + +## License and Legal Information + +This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file. + +**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file. + +### Trademarks + +This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein. + +Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar. + +### Company Information + +Task Venture Capital GmbH +Registered at District Court Bremen HRB 35230 HB, Germany + +For any legal inquiries or further information, please contact us via email at hello@task.vc. + +By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works. diff --git a/spec/rfc2865.txt b/spec/rfc2865.txt new file mode 100644 index 0000000..10ec231 --- /dev/null +++ b/spec/rfc2865.txt @@ -0,0 +1,4259 @@ + + + + + + +Network Working Group C. Rigney +Request for Comments: 2865 S. Willens +Obsoletes: 2138 Livingston +Category: Standards Track A. Rubens + Merit + W. Simpson + Daydreamer + June 2000 + + + Remote Authentication Dial In User Service (RADIUS) + +Status of this Memo + + This document specifies an Internet standards track protocol for the + Internet community, and requests discussion and suggestions for + improvements. Please refer to the current edition of the "Internet + Official Protocol Standards" (STD 1) for the standardization state + and status of this protocol. Distribution of this memo is unlimited. + +Copyright Notice + + Copyright (C) The Internet Society (2000). All Rights Reserved. + +IESG Note: + + This protocol is widely implemented and used. Experience has shown + that it can suffer degraded performance and lost data when used in + large scale systems, in part because it does not include provisions + for congestion control. Readers of this document may find it + beneficial to track the progress of the IETF's AAA working group, + which may develop a successor protocol that better addresses the + scaling and congestion control issues. + +Abstract + + This document describes a protocol for carrying authentication, + authorization, and configuration information between a Network Access + Server which desires to authenticate its links and a shared + Authentication Server. + +Implementation Note + + This memo documents the RADIUS protocol. The early deployment of + RADIUS was done using UDP port number 1645, which conflicts with the + "datametrics" service. The officially assigned port number for + RADIUS is 1812. + + + + +Rigney, et al. Standards Track [Page 1] + +RFC 2865 RADIUS June 2000 + + +Table of Contents + + 1. Introduction .......................................... 3 + 1.1 Specification of Requirements ................... 4 + 1.2 Terminology ..................................... 5 + 2. Operation ............................................. 5 + 2.1 Challenge/Response .............................. 7 + 2.2 Interoperation with PAP and CHAP ................ 8 + 2.3 Proxy ........................................... 8 + 2.4 Why UDP? ........................................ 11 + 2.5 Retransmission Hints ............................ 12 + 2.6 Keep-Alives Considered Harmful .................. 13 + 3. Packet Format ......................................... 13 + 4. Packet Types .......................................... 17 + 4.1 Access-Request .................................. 17 + 4.2 Access-Accept ................................... 18 + 4.3 Access-Reject ................................... 20 + 4.4 Access-Challenge ................................ 21 + 5. Attributes ............................................ 22 + 5.1 User-Name ....................................... 26 + 5.2 User-Password ................................... 27 + 5.3 CHAP-Password ................................... 28 + 5.4 NAS-IP-Address .................................. 29 + 5.5 NAS-Port ........................................ 30 + 5.6 Service-Type .................................... 31 + 5.7 Framed-Protocol ................................. 33 + 5.8 Framed-IP-Address ............................... 34 + 5.9 Framed-IP-Netmask ............................... 34 + 5.10 Framed-Routing .................................. 35 + 5.11 Filter-Id ....................................... 36 + 5.12 Framed-MTU ...................................... 37 + 5.13 Framed-Compression .............................. 37 + 5.14 Login-IP-Host ................................... 38 + 5.15 Login-Service ................................... 39 + 5.16 Login-TCP-Port .................................. 40 + 5.17 (unassigned) .................................... 41 + 5.18 Reply-Message ................................... 41 + 5.19 Callback-Number ................................. 42 + 5.20 Callback-Id ..................................... 42 + 5.21 (unassigned) .................................... 43 + 5.22 Framed-Route .................................... 43 + 5.23 Framed-IPX-Network .............................. 44 + 5.24 State ........................................... 45 + 5.25 Class ........................................... 46 + 5.26 Vendor-Specific ................................. 47 + 5.27 Session-Timeout ................................. 48 + 5.28 Idle-Timeout .................................... 49 + 5.29 Termination-Action .............................. 49 + + + +Rigney, et al. Standards Track [Page 2] + +RFC 2865 RADIUS June 2000 + + + 5.30 Called-Station-Id ............................... 50 + 5.31 Calling-Station-Id .............................. 51 + 5.32 NAS-Identifier .................................. 52 + 5.33 Proxy-State ..................................... 53 + 5.34 Login-LAT-Service ............................... 54 + 5.35 Login-LAT-Node .................................. 55 + 5.36 Login-LAT-Group ................................. 56 + 5.37 Framed-AppleTalk-Link ........................... 57 + 5.38 Framed-AppleTalk-Network ........................ 58 + 5.39 Framed-AppleTalk-Zone ........................... 58 + 5.40 CHAP-Challenge .................................. 59 + 5.41 NAS-Port-Type ................................... 60 + 5.42 Port-Limit ...................................... 61 + 5.43 Login-LAT-Port .................................. 62 + 5.44 Table of Attributes ............................. 63 + 6. IANA Considerations ................................... 64 + 6.1 Definition of Terms ............................. 64 + 6.2 Recommended Registration Policies ............... 65 + 7. Examples .............................................. 66 + 7.1 User Telnet to Specified Host ................... 66 + 7.2 Framed User Authenticating with CHAP ............ 67 + 7.3 User with Challenge-Response card ............... 68 + 8. Security Considerations ............................... 71 + 9. Change Log ............................................ 71 + 10. References ............................................ 73 + 11. Acknowledgements ...................................... 74 + 12. Chair's Address ....................................... 74 + 13. Authors' Addresses .................................... 75 + 14. Full Copyright Statement .............................. 76 + +1. Introduction + + This document obsoletes RFC 2138 [1]. A summary of the changes + between this document and RFC 2138 is available in the "Change Log" + appendix. + + Managing dispersed serial line and modem pools for large numbers of + users can create the need for significant administrative support. + Since modem pools are by definition a link to the outside world, they + require careful attention to security, authorization and accounting. + This can be best achieved by managing a single "database" of users, + which allows for authentication (verifying user name and password) as + well as configuration information detailing the type of service to + deliver to the user (for example, SLIP, PPP, telnet, rlogin). + + + + + + + +Rigney, et al. Standards Track [Page 3] + +RFC 2865 RADIUS June 2000 + + + Key features of RADIUS are: + + Client/Server Model + + A Network Access Server (NAS) operates as a client of RADIUS. The + client is responsible for passing user information to designated + RADIUS servers, and then acting on the response which is returned. + + RADIUS servers are responsible for receiving user connection + requests, authenticating the user, and then returning all + configuration information necessary for the client to deliver + service to the user. + + A RADIUS server can act as a proxy client to other RADIUS servers + or other kinds of authentication servers. + + Network Security + + Transactions between the client and RADIUS server are + authenticated through the use of a shared secret, which is never + sent over the network. In addition, any user passwords are sent + encrypted between the client and RADIUS server, to eliminate the + possibility that someone snooping on an unsecure network could + determine a user's password. + + Flexible Authentication Mechanisms + + The RADIUS server can support a variety of methods to authenticate + a user. When it is provided with the user name and original + password given by the user, it can support PPP PAP or CHAP, UNIX + login, and other authentication mechanisms. + + Extensible Protocol + + All transactions are comprised of variable length Attribute- + Length-Value 3-tuples. New attribute values can be added without + disturbing existing implementations of the protocol. + +1.1. Specification of Requirements + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this + document are to be interpreted as described in BCP 14 [2]. These key + words mean the same thing whether capitalized or not. + + An implementation is not compliant if it fails to satisfy one or more + of the must or must not requirements for the protocols it implements. + An implementation that satisfies all the must, must not, should and + + + +Rigney, et al. Standards Track [Page 4] + +RFC 2865 RADIUS June 2000 + + + should not requirements for its protocols is said to be + "unconditionally compliant"; one that satisfies all the must and must + not requirements but not all the should or should not requirements + for its protocols is said to be "conditionally compliant". + + A NAS that does not implement a given service MUST NOT implement the + RADIUS attributes for that service. For example, a NAS that is + unable to offer ARAP service MUST NOT implement the RADIUS attributes + for ARAP. A NAS MUST treat a RADIUS access-accept authorizing an + unavailable service as an access-reject instead. + +1.2. Terminology + + This document frequently uses the following terms: + + service The NAS provides a service to the dial-in user, such as PPP + or Telnet. + + session Each service provided by the NAS to a dial-in user + constitutes a session, with the beginning of the session + defined as the point where service is first provided and + the end of the session defined as the point where service + is ended. A user may have multiple sessions in parallel or + series if the NAS supports that. + + silently discard + This means the implementation discards the packet without + further processing. The implementation SHOULD provide the + capability of logging the error, including the contents of + the silently discarded packet, and SHOULD record the event + in a statistics counter. + +2. Operation + + When a client is configured to use RADIUS, any user of the client + presents authentication information to the client. This might be + with a customizable login prompt, where the user is expected to enter + their username and password. Alternatively, the user might use a + link framing protocol such as the Point-to-Point Protocol (PPP), + which has authentication packets which carry this information. + + Once the client has obtained such information, it may choose to + authenticate using RADIUS. To do so, the client creates an "Access- + Request" containing such Attributes as the user's name, the user's + password, the ID of the client and the Port ID which the user is + accessing. When a password is present, it is hidden using a method + based on the RSA Message Digest Algorithm MD5 [3]. + + + + +Rigney, et al. Standards Track [Page 5] + +RFC 2865 RADIUS June 2000 + + + The Access-Request is submitted to the RADIUS server via the network. + If no response is returned within a length of time, the request is + re-sent a number of times. The client can also forward requests to + an alternate server or servers in the event that the primary server + is down or unreachable. An alternate server can be used either after + a number of tries to the primary server fail, or in a round-robin + fashion. Retry and fallback algorithms are the topic of current + research and are not specified in detail in this document. + + Once the RADIUS server receives the request, it validates the sending + client. A request from a client for which the RADIUS server does not + have a shared secret MUST be silently discarded. If the client is + valid, the RADIUS server consults a database of users to find the + user whose name matches the request. The user entry in the database + contains a list of requirements which must be met to allow access for + the user. This always includes verification of the password, but can + also specify the client(s) or port(s) to which the user is allowed + access. + + The RADIUS server MAY make requests of other servers in order to + satisfy the request, in which case it acts as a client. + + If any Proxy-State attributes were present in the Access-Request, + they MUST be copied unmodified and in order into the response packet. + Other Attributes can be placed before, after, or even between the + Proxy-State attributes. + + If any condition is not met, the RADIUS server sends an "Access- + Reject" response indicating that this user request is invalid. If + desired, the server MAY include a text message in the Access-Reject + which MAY be displayed by the client to the user. No other + Attributes (except Proxy-State) are permitted in an Access-Reject. + + If all conditions are met and the RADIUS server wishes to issue a + challenge to which the user must respond, the RADIUS server sends an + "Access-Challenge" response. It MAY include a text message to be + displayed by the client to the user prompting for a response to the + challenge, and MAY include a State attribute. + + If the client receives an Access-Challenge and supports + challenge/response it MAY display the text message, if any, to the + user, and then prompt the user for a response. The client then re- + submits its original Access-Request with a new request ID, with the + User-Password Attribute replaced by the response (encrypted), and + including the State Attribute from the Access-Challenge, if any. + Only 0 or 1 instances of the State Attribute SHOULD be + + + + + +Rigney, et al. Standards Track [Page 6] + +RFC 2865 RADIUS June 2000 + + + present in a request. The server can respond to this new Access- + Request with either an Access-Accept, an Access-Reject, or another + Access-Challenge. + + If all conditions are met, the list of configuration values for the + user are placed into an "Access-Accept" response. These values + include the type of service (for example: SLIP, PPP, Login User) and + all necessary values to deliver the desired service. For SLIP and + PPP, this may include values such as IP address, subnet mask, MTU, + desired compression, and desired packet filter identifiers. For + character mode users, this may include values such as desired + protocol and host. + +2.1. Challenge/Response + + In challenge/response authentication, the user is given an + unpredictable number and challenged to encrypt it and give back the + result. Authorized users are equipped with special devices such as + smart cards or software that facilitate calculation of the correct + response with ease. Unauthorized users, lacking the appropriate + device or software and lacking knowledge of the secret key necessary + to emulate such a device or software, can only guess at the response. + + The Access-Challenge packet typically contains a Reply-Message + including a challenge to be displayed to the user, such as a numeric + value unlikely ever to be repeated. Typically this is obtained from + an external server that knows what type of authenticator is in the + possession of the authorized user and can therefore choose a random + or non-repeating pseudorandom number of an appropriate radix and + length. + + The user then enters the challenge into his device (or software) and + it calculates a response, which the user enters into the client which + forwards it to the RADIUS server via a second Access-Request. If the + response matches the expected response the RADIUS server replies with + an Access-Accept, otherwise an Access-Reject. + + Example: The NAS sends an Access-Request packet to the RADIUS Server + with NAS-Identifier, NAS-Port, User-Name, User-Password (which may + just be a fixed string like "challenge" or ignored). The server + sends back an Access-Challenge packet with State and a Reply-Message + along the lines of "Challenge 12345678, enter your response at the + prompt" which the NAS displays. The NAS prompts for the response and + sends a NEW Access-Request to the server (with a new ID) with NAS- + Identifier, NAS-Port, User-Name, User-Password (the response just + entered by the user, encrypted), and the same State Attribute that + + + + + +Rigney, et al. Standards Track [Page 7] + +RFC 2865 RADIUS June 2000 + + + came with the Access-Challenge. The server then sends back either an + Access-Accept or Access-Reject based on whether the response matches + the required value, or it can even send another Access-Challenge. + +2.2. Interoperation with PAP and CHAP + + For PAP, the NAS takes the PAP ID and password and sends them in an + Access-Request packet as the User-Name and User-Password. The NAS MAY + include the Attributes Service-Type = Framed-User and Framed-Protocol + = PPP as a hint to the RADIUS server that PPP service is expected. + + For CHAP, the NAS generates a random challenge (preferably 16 octets) + and sends it to the user, who returns a CHAP response along with a + CHAP ID and CHAP username. The NAS then sends an Access-Request + packet to the RADIUS server with the CHAP username as the User-Name + and with the CHAP ID and CHAP response as the CHAP-Password + (Attribute 3). The random challenge can either be included in the + CHAP-Challenge attribute or, if it is 16 octets long, it can be + placed in the Request Authenticator field of the Access-Request + packet. The NAS MAY include the Attributes Service-Type = Framed- + User and Framed-Protocol = PPP as a hint to the RADIUS server that + PPP service is expected. + + The RADIUS server looks up a password based on the User-Name, + encrypts the challenge using MD5 on the CHAP ID octet, that password, + and the CHAP challenge (from the CHAP-Challenge attribute if present, + otherwise from the Request Authenticator), and compares that result + to the CHAP-Password. If they match, the server sends back an + Access-Accept, otherwise it sends back an Access-Reject. + + If the RADIUS server is unable to perform the requested + authentication it MUST return an Access-Reject. For example, CHAP + requires that the user's password be available in cleartext to the + server so that it can encrypt the CHAP challenge and compare that to + the CHAP response. If the password is not available in cleartext to + the RADIUS server then the server MUST send an Access-Reject to the + client. + +2.3. Proxy + + With proxy RADIUS, one RADIUS server receives an authentication (or + accounting) request from a RADIUS client (such as a NAS), forwards + the request to a remote RADIUS server, receives the reply from the + remote server, and sends that reply to the client, possibly with + changes to reflect local administrative policy. A common use for + proxy RADIUS is roaming. Roaming permits two or more administrative + entities to allow each other's users to dial in to either entity's + network for service. + + + +Rigney, et al. Standards Track [Page 8] + +RFC 2865 RADIUS June 2000 + + + The NAS sends its RADIUS access-request to the "forwarding server" + which forwards it to the "remote server". The remote server sends a + response (Access-Accept, Access-Reject, or Access-Challenge) back to + the forwarding server, which sends it back to the NAS. The User-Name + attribute MAY contain a Network Access Identifier [8] for RADIUS + Proxy operations. The choice of which server receives the forwarded + request SHOULD be based on the authentication "realm". The + authentication realm MAY be the realm part of a Network Access + Identifier (a "named realm"). Alternatively, the choice of which + server receives the forwarded request MAY be based on whatever other + criteria the forwarding server is configured to use, such as Called- + Station-Id (a "numbered realm"). + + A RADIUS server can function as both a forwarding server and a remote + server, serving as a forwarding server for some realms and a remote + server for other realms. One forwarding server can act as a + forwarder for any number of remote servers. A remote server can have + any number of servers forwarding to it and can provide authentication + for any number of realms. One forwarding server can forward to + another forwarding server to create a chain of proxies, although care + must be taken to avoid introducing loops. + + The following scenario illustrates a proxy RADIUS communication + between a NAS and the forwarding and remote RADIUS servers: + + 1. A NAS sends its access-request to the forwarding server. + + 2. The forwarding server forwards the access-request to the remote + server. + + 3. The remote server sends an access-accept, access-reject or + access-challenge back to the forwarding server. For this example, + an access-accept is sent. + + 4. The forwarding server sends the access-accept to the NAS. + + The forwarding server MUST treat any Proxy-State attributes already + in the packet as opaque data. Its operation MUST NOT depend on the + content of Proxy-State attributes added by previous servers. + + If there are any Proxy-State attributes in the request received from + the client, the forwarding server MUST include those Proxy-State + attributes in its reply to the client. The forwarding server MAY + include the Proxy-State attributes in the access-request when it + forwards the request, or MAY omit them in the forwarded request. If + the forwarding server omits the Proxy-State attributes in the + forwarded access-request, it MUST attach them to the response before + sending it to the client. + + + +Rigney, et al. Standards Track [Page 9] + +RFC 2865 RADIUS June 2000 + + + We now examine each step in more detail. + + 1. A NAS sends its access-request to the forwarding server. The + forwarding server decrypts the User-Password, if present, using + the shared secret it knows for the NAS. If a CHAP-Password + attribute is present in the packet and no CHAP-Challenge attribute + is present, the forwarding server MUST leave the Request- + Authenticator untouched or copy it to a CHAP-Challenge attribute. + + '' The forwarding server MAY add one Proxy-State attribute to the + packet. (It MUST NOT add more than one.) If it adds a Proxy- + State, the Proxy-State MUST appear after any other Proxy-States in + the packet. The forwarding server MUST NOT modify any other + Proxy-States that were in the packet (it may choose not to forward + them, but it MUST NOT change their contents). The forwarding + server MUST NOT change the order of any attributes of the same + type, including Proxy-State. + + 2. The forwarding server encrypts the User-Password, if present, + using the secret it shares with the remote server, sets the + Identifier as needed, and forwards the access-request to the + remote server. + + 3. The remote server (if the final destination) verifies the user + using User-Password, CHAP-Password, or such method as future + extensions may dictate, and returns an access-accept, access- + reject or access-challenge back to the forwarding server. For + this example, an access-accept is sent. The remote server MUST + copy all Proxy-State attributes (and only the Proxy-State + attributes) in order from the access-request to the response + packet, without modifying them. + + 4. The forwarding server verifies the Response Authenticator using + the secret it shares with the remote server, and silently discards + the packet if it fails verification. If the packet passes + verification, the forwarding server removes the last Proxy-State + (if it attached one), signs the Response Authenticator using the + secret it shares with the NAS, restores the Identifier to match + the one in the original request by the NAS, and sends the access- + accept to the NAS. + + A forwarding server MAY need to modify attributes to enforce local + policy. Such policy is outside the scope of this document, with the + following restrictions. A forwarding server MUST not modify existing + Proxy-State, State, or Class attributes present in the packet. + + + + + + +Rigney, et al. Standards Track [Page 10] + +RFC 2865 RADIUS June 2000 + + + Implementers of forwarding servers should consider carefully which + values it is willing to accept for Service-Type. Careful + consideration must be given to the effects of passing along Service- + Types of NAS-Prompt or Administrative in a proxied Access-Accept, and + implementers may wish to provide mechanisms to block those or other + service types, or other attributes. Such mechanisms are outside the + scope of this document. + +2.4. Why UDP? + + A frequently asked question is why RADIUS uses UDP instead of TCP as + a transport protocol. UDP was chosen for strictly technical reasons. + + There are a number of issues which must be understood. RADIUS is a + transaction based protocol which has several interesting + characteristics: + + 1. If the request to a primary Authentication server fails, a + secondary server must be queried. + + To meet this requirement, a copy of the request must be kept above + the transport layer to allow for alternate transmission. This + means that retransmission timers are still required. + + 2. The timing requirements of this particular protocol are + significantly different than TCP provides. + + At one extreme, RADIUS does not require a "responsive" detection + of lost data. The user is willing to wait several seconds for the + authentication to complete. The generally aggressive TCP + retransmission (based on average round trip time) is not required, + nor is the acknowledgement overhead of TCP. + + At the other extreme, the user is not willing to wait several + minutes for authentication. Therefore the reliable delivery of + TCP data two minutes later is not useful. The faster use of an + alternate server allows the user to gain access before giving up. + + 3. The stateless nature of this protocol simplifies the use of UDP. + + Clients and servers come and go. Systems are rebooted, or are + power cycled independently. Generally this does not cause a + problem and with creative timeouts and detection of lost TCP + connections, code can be written to handle anomalous events. UDP + however completely eliminates any of this special handling. Each + client and server can open their UDP transport just once and leave + it open through all types of failure events on the network. + + + + +Rigney, et al. Standards Track [Page 11] + +RFC 2865 RADIUS June 2000 + + + 4. UDP simplifies the server implementation. + + In the earliest implementations of RADIUS, the server was single + threaded. This means that a single request was received, + processed, and returned. This was found to be unmanageable in + environments where the back-end security mechanism took real time + (1 or more seconds). The server request queue would fill and in + environments where hundreds of people were being authenticated + every minute, the request turn-around time increased to longer + than users were willing to wait (this was especially severe when a + specific lookup in a database or over DNS took 30 or more + seconds). The obvious solution was to make the server multi- + threaded. Achieving this was simple with UDP. Separate processes + were spawned to serve each request and these processes could + respond directly to the client NAS with a simple UDP packet to the + original transport of the client. + + It's not all a panacea. As noted, using UDP requires one thing which + is built into TCP: with UDP we must artificially manage + retransmission timers to the same server, although they don't require + the same attention to timing provided by TCP. This one penalty is a + small price to pay for the advantages of UDP in this protocol. + + Without TCP we would still probably be using tin cans connected by + string. But for this particular protocol, UDP is a better choice. + +2.5. Retransmission Hints + + If the RADIUS server and alternate RADIUS server share the same + shared secret, it is OK to retransmit the packet to the alternate + RADIUS server with the same ID and Request Authenticator, because the + content of the attributes haven't changed. If you want to use a new + Request Authenticator when sending to the alternate server, you may. + + If you change the contents of the User-Password attribute (or any + other attribute), you need a new Request Authenticator and therefore + a new ID. + + If the NAS is retransmitting a RADIUS request to the same server as + before, and the attributes haven't changed, you MUST use the same + Request Authenticator, ID, and source port. If any attributes have + changed, you MUST use a new Request Authenticator and ID. + + A NAS MAY use the same ID across all servers, or MAY keep track of + IDs separately for each server, it is up to the implementer. If a + NAS needs more than 256 IDs for outstanding requests, it MAY use + + + + + +Rigney, et al. Standards Track [Page 12] + +RFC 2865 RADIUS June 2000 + + + additional source ports to send requests from, and keep track of IDs + for each source port. This allows up to 16 million or so outstanding + requests at one time to a single server. + +2.6. Keep-Alives Considered Harmful + + Some implementers have adopted the practice of sending test RADIUS + requests to see if a server is alive. This practice is strongly + discouraged, since it adds to load and harms scalability without + providing any additional useful information. Since a RADIUS request + is contained in a single datagram, in the time it would take you to + send a ping you could just send the RADIUS request, and getting a + reply tells you that the RADIUS server is up. If you do not have a + RADIUS request to send, it does not matter if the server is up or + not, because you are not using it. + + If you want to monitor your RADIUS server, use SNMP. That's what + SNMP is for. + +3. Packet Format + + Exactly one RADIUS packet is encapsulated in the UDP Data field [4], + where the UDP Destination Port field indicates 1812 (decimal). + + When a reply is generated, the source and destination ports are + reversed. + + This memo documents the RADIUS protocol. The early deployment of + RADIUS was done using UDP port number 1645, which conflicts with the + "datametrics" service. The officially assigned port number for + RADIUS is 1812. + + + + + + + + + + + + + + + + + + + + +Rigney, et al. Standards Track [Page 13] + +RFC 2865 RADIUS June 2000 + + + A summary of the RADIUS data format is shown below. The fields are + transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Code | Identifier | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + | Authenticator | + | | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Attributes ... + +-+-+-+-+-+-+-+-+-+-+-+-+- + + Code + + The Code field is one octet, and identifies the type of RADIUS + packet. When a packet is received with an invalid Code field, it + is silently discarded. + + RADIUS Codes (decimal) are assigned as follows: + + 1 Access-Request + 2 Access-Accept + 3 Access-Reject + 4 Accounting-Request + 5 Accounting-Response + 11 Access-Challenge + 12 Status-Server (experimental) + 13 Status-Client (experimental) + 255 Reserved + + Codes 4 and 5 are covered in the RADIUS Accounting document [5]. + Codes 12 and 13 are reserved for possible use, but are not further + mentioned here. + + Identifier + + The Identifier field is one octet, and aids in matching requests + and replies. The RADIUS server can detect a duplicate request if + it has the same client source IP address and source UDP port and + Identifier within a short span of time. + + + + + + + +Rigney, et al. Standards Track [Page 14] + +RFC 2865 RADIUS June 2000 + + + Length + + The Length field is two octets. It indicates the length of the + packet including the Code, Identifier, Length, Authenticator and + Attribute fields. Octets outside the range of the Length field + MUST be treated as padding and ignored on reception. If the + packet is shorter than the Length field indicates, it MUST be + silently discarded. The minimum length is 20 and maximum length + is 4096. + + Authenticator + + The Authenticator field is sixteen (16) octets. The most + significant octet is transmitted first. This value is used to + authenticate the reply from the RADIUS server, and is used in the + password hiding algorithm. + + Request Authenticator + + In Access-Request Packets, the Authenticator value is a 16 + octet random number, called the Request Authenticator. The + value SHOULD be unpredictable and unique over the lifetime of a + secret (the password shared between the client and the RADIUS + server), since repetition of a request value in conjunction + with the same secret would permit an attacker to reply with a + previously intercepted response. Since it is expected that the + same secret MAY be used to authenticate with servers in + disparate geographic regions, the Request Authenticator field + SHOULD exhibit global and temporal uniqueness. + + The Request Authenticator value in an Access-Request packet + SHOULD also be unpredictable, lest an attacker trick a server + into responding to a predicted future request, and then use the + response to masquerade as that server to a future Access- + Request. + + Although protocols such as RADIUS are incapable of protecting + against theft of an authenticated session via realtime active + wiretapping attacks, generation of unique unpredictable + requests can protect against a wide range of active attacks + against authentication. + + The NAS and RADIUS server share a secret. That shared secret + followed by the Request Authenticator is put through a one-way + MD5 hash to create a 16 octet digest value which is xored with + the password entered by the user, and the xored result placed + + + + + +Rigney, et al. Standards Track [Page 15] + +RFC 2865 RADIUS June 2000 + + + in the User-Password attribute in the Access-Request packet. + See the entry for User-Password in the section on Attributes + for a more detailed description. + + Response Authenticator + + The value of the Authenticator field in Access-Accept, Access- + Reject, and Access-Challenge packets is called the Response + Authenticator, and contains a one-way MD5 hash calculated over + a stream of octets consisting of: the RADIUS packet, beginning + with the Code field, including the Identifier, the Length, the + Request Authenticator field from the Access-Request packet, and + the response Attributes, followed by the shared secret. That + is, ResponseAuth = + MD5(Code+ID+Length+RequestAuth+Attributes+Secret) where + + denotes concatenation. + + Administrative Note + + The secret (password shared between the client and the RADIUS + server) SHOULD be at least as large and unguessable as a well- + chosen password. It is preferred that the secret be at least 16 + octets. This is to ensure a sufficiently large range for the + secret to provide protection against exhaustive search attacks. + The secret MUST NOT be empty (length 0) since this would allow + packets to be trivially forged. + + A RADIUS server MUST use the source IP address of the RADIUS UDP + packet to decide which shared secret to use, so that RADIUS + requests can be proxied. + + When using a forwarding proxy, the proxy must be able to alter the + packet as it passes through in each direction - when the proxy + forwards the request, the proxy MAY add a Proxy-State Attribute, + and when the proxy forwards a response, it MUST remove its Proxy- + State Attribute if it added one. Proxy-State is always added or + removed after any other Proxy-States, but no other assumptions + regarding its location within the list of attributes can be made. + Since Access-Accept and Access-Reject replies are authenticated on + the entire packet contents, the stripping of the Proxy-State + attribute invalidates the signature in the packet - so the proxy + has to re-sign it. + + Further details of RADIUS proxy implementation are outside the + scope of this document. + + + + + + +Rigney, et al. Standards Track [Page 16] + +RFC 2865 RADIUS June 2000 + + +4. Packet Types + + The RADIUS Packet type is determined by the Code field in the first + octet of the Packet. + +4.1. Access-Request + + Description + + Access-Request packets are sent to a RADIUS server, and convey + information used to determine whether a user is allowed access to + a specific NAS, and any special services requested for that user. + An implementation wishing to authenticate a user MUST transmit a + RADIUS packet with the Code field set to 1 (Access-Request). + + Upon receipt of an Access-Request from a valid client, an + appropriate reply MUST be transmitted. + + An Access-Request SHOULD contain a User-Name attribute. It MUST + contain either a NAS-IP-Address attribute or a NAS-Identifier + attribute (or both). + + An Access-Request MUST contain either a User-Password or a CHAP- + Password or a State. An Access-Request MUST NOT contain both a + User-Password and a CHAP-Password. If future extensions allow + other kinds of authentication information to be conveyed, the + attribute for that can be used in an Access-Request instead of + User-Password or CHAP-Password. + + An Access-Request SHOULD contain a NAS-Port or NAS-Port-Type + attribute or both unless the type of access being requested does + not involve a port or the NAS does not distinguish among its + ports. + + An Access-Request MAY contain additional attributes as a hint to + the server, but the server is not required to honor the hint. + + When a User-Password is present, it is hidden using a method based + on the RSA Message Digest Algorithm MD5 [3]. + + + + + + + + + + + + +Rigney, et al. Standards Track [Page 17] + +RFC 2865 RADIUS June 2000 + + + A summary of the Access-Request packet format is shown below. The + fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Code | Identifier | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + | Request Authenticator | + | | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Attributes ... + +-+-+-+-+-+-+-+-+-+-+-+-+- + + Code + + 1 for Access-Request. + + Identifier + + The Identifier field MUST be changed whenever the content of the + Attributes field changes, and whenever a valid reply has been + received for a previous request. For retransmissions, the + Identifier MUST remain unchanged. + + Request Authenticator + + The Request Authenticator value MUST be changed each time a new + Identifier is used. + + Attributes + + The Attribute field is variable in length, and contains the list + of Attributes that are required for the type of service, as well + as any desired optional Attributes. + +4.2. Access-Accept + + Description + + Access-Accept packets are sent by the RADIUS server, and provide + specific configuration information necessary to begin delivery of + service to the user. If all Attribute values received in an + Access-Request are acceptable then the RADIUS implementation MUST + transmit a packet with the Code field set to 2 (Access-Accept). + + + + +Rigney, et al. Standards Track [Page 18] + +RFC 2865 RADIUS June 2000 + + + On reception of an Access-Accept, the Identifier field is matched + with a pending Access-Request. The Response Authenticator field + MUST contain the correct response for the pending Access-Request. + Invalid packets are silently discarded. + + A summary of the Access-Accept packet format is shown below. The + fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Code | Identifier | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + | Response Authenticator | + | | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Attributes ... + +-+-+-+-+-+-+-+-+-+-+-+-+- + + Code + + 2 for Access-Accept. + + Identifier + + The Identifier field is a copy of the Identifier field of the + Access-Request which caused this Access-Accept. + + Response Authenticator + + The Response Authenticator value is calculated from the Access- + Request value, as described earlier. + + Attributes + + The Attribute field is variable in length, and contains a list of + zero or more Attributes. + + + + + + + + + + + + +Rigney, et al. Standards Track [Page 19] + +RFC 2865 RADIUS June 2000 + + +4.3. Access-Reject + + Description + + If any value of the received Attributes is not acceptable, then + the RADIUS server MUST transmit a packet with the Code field set + to 3 (Access-Reject). It MAY include one or more Reply-Message + Attributes with a text message which the NAS MAY display to the + user. + + A summary of the Access-Reject packet format is shown below. The + fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Code | Identifier | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + | Response Authenticator | + | | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Attributes ... + +-+-+-+-+-+-+-+-+-+-+-+-+- + + Code + + 3 for Access-Reject. + + Identifier + + The Identifier field is a copy of the Identifier field of the + Access-Request which caused this Access-Reject. + + Response Authenticator + + The Response Authenticator value is calculated from the Access- + Request value, as described earlier. + + Attributes + + The Attribute field is variable in length, and contains a list of + zero or more Attributes. + + + + + + + +Rigney, et al. Standards Track [Page 20] + +RFC 2865 RADIUS June 2000 + + +4.4. Access-Challenge + + Description + + If the RADIUS server desires to send the user a challenge + requiring a response, then the RADIUS server MUST respond to the + Access-Request by transmitting a packet with the Code field set to + 11 (Access-Challenge). + + The Attributes field MAY have one or more Reply-Message + Attributes, and MAY have a single State Attribute, or none. + Vendor-Specific, Idle-Timeout, Session-Timeout and Proxy-State + attributes MAY also be included. No other Attributes defined in + this document are permitted in an Access-Challenge. + + On receipt of an Access-Challenge, the Identifier field is matched + with a pending Access-Request. Additionally, the Response + Authenticator field MUST contain the correct response for the + pending Access-Request. Invalid packets are silently discarded. + + If the NAS does not support challenge/response, it MUST treat an + Access-Challenge as though it had received an Access-Reject + instead. + + If the NAS supports challenge/response, receipt of a valid + Access-Challenge indicates that a new Access-Request SHOULD be + sent. The NAS MAY display the text message, if any, to the user, + and then prompt the user for a response. It then sends its + original Access-Request with a new request ID and Request + Authenticator, with the User-Password Attribute replaced by the + user's response (encrypted), and including the State Attribute + from the Access-Challenge, if any. Only 0 or 1 instances of the + State Attribute can be present in an Access-Request. + + A NAS which supports PAP MAY forward the Reply-Message to the + dialing client and accept a PAP response which it can use as + though the user had entered the response. If the NAS cannot do + so, it MUST treat the Access-Challenge as though it had received + an Access-Reject instead. + + + + + + + + + + + + +Rigney, et al. Standards Track [Page 21] + +RFC 2865 RADIUS June 2000 + + + A summary of the Access-Challenge packet format is shown below. The + fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Code | Identifier | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + | Response Authenticator | + | | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Attributes ... + +-+-+-+-+-+-+-+-+-+-+-+-+- + + Code + + 11 for Access-Challenge. + + Identifier + + The Identifier field is a copy of the Identifier field of the + Access-Request which caused this Access-Challenge. + + Response Authenticator + + The Response Authenticator value is calculated from the Access- + Request value, as described earlier. + + Attributes + + The Attributes field is variable in length, and contains a list of + zero or more Attributes. + +5. Attributes + + RADIUS Attributes carry the specific authentication, authorization, + information and configuration details for the request and reply. + + The end of the list of Attributes is indicated by the Length of the + RADIUS packet. + + Some Attributes MAY be included more than once. The effect of this + is Attribute specific, and is specified in each Attribute + description. A summary table is provided at the end of the + "Attributes" section. + + + + +Rigney, et al. Standards Track [Page 22] + +RFC 2865 RADIUS June 2000 + + + If multiple Attributes with the same Type are present, the order of + Attributes with the same Type MUST be preserved by any proxies. The + order of Attributes of different Types is not required to be + preserved. A RADIUS server or client MUST NOT have any dependencies + on the order of attributes of different types. A RADIUS server or + client MUST NOT require attributes of the same type to be contiguous. + + Where an Attribute's description limits which kinds of packet it can + be contained in, this applies only to the packet types defined in + this document, namely Access-Request, Access-Accept, Access-Reject + and Access-Challenge (Codes 1, 2, 3, and 11). Other documents + defining other packet types may also use Attributes described here. + To determine which Attributes are allowed in Accounting-Request and + Accounting-Response packets (Codes 4 and 5) refer to the RADIUS + Accounting document [5]. + + Likewise where packet types defined here state that only certain + Attributes are permissible in them, future memos defining new + Attributes should indicate which packet types the new Attributes may + be present in. + + A summary of the Attribute format is shown below. The fields are + transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + | Type | Length | Value ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + Type + + The Type field is one octet. Up-to-date values of the RADIUS Type + field are specified in the most recent "Assigned Numbers" RFC [6]. + Values 192-223 are reserved for experimental use, values 224-240 + are reserved for implementation-specific use, and values 241-255 + are reserved and should not be used. + + A RADIUS server MAY ignore Attributes with an unknown Type. + + A RADIUS client MAY ignore Attributes with an unknown Type. + + + + + + + + + + +Rigney, et al. Standards Track [Page 23] + +RFC 2865 RADIUS June 2000 + + + This specification concerns the following values: + + 1 User-Name + 2 User-Password + 3 CHAP-Password + 4 NAS-IP-Address + 5 NAS-Port + 6 Service-Type + 7 Framed-Protocol + 8 Framed-IP-Address + 9 Framed-IP-Netmask + 10 Framed-Routing + 11 Filter-Id + 12 Framed-MTU + 13 Framed-Compression + 14 Login-IP-Host + 15 Login-Service + 16 Login-TCP-Port + 17 (unassigned) + 18 Reply-Message + 19 Callback-Number + 20 Callback-Id + 21 (unassigned) + 22 Framed-Route + 23 Framed-IPX-Network + 24 State + 25 Class + 26 Vendor-Specific + 27 Session-Timeout + 28 Idle-Timeout + 29 Termination-Action + 30 Called-Station-Id + 31 Calling-Station-Id + 32 NAS-Identifier + 33 Proxy-State + 34 Login-LAT-Service + 35 Login-LAT-Node + 36 Login-LAT-Group + 37 Framed-AppleTalk-Link + 38 Framed-AppleTalk-Network + 39 Framed-AppleTalk-Zone + 40-59 (reserved for accounting) + 60 CHAP-Challenge + 61 NAS-Port-Type + 62 Port-Limit + 63 Login-LAT-Port + + + + + +Rigney, et al. Standards Track [Page 24] + +RFC 2865 RADIUS June 2000 + + + Length + + The Length field is one octet, and indicates the length of this + Attribute including the Type, Length and Value fields. If an + Attribute is received in an Access-Request but with an invalid + Length, an Access-Reject SHOULD be transmitted. If an Attribute + is received in an Access-Accept, Access-Reject or Access-Challenge + packet with an invalid length, the packet MUST either be treated + as an Access-Reject or else silently discarded. + + Value + + The Value field is zero or more octets and contains information + specific to the Attribute. The format and length of the Value + field is determined by the Type and Length fields. + + Note that none of the types in RADIUS terminate with a NUL (hex + 00). In particular, types "text" and "string" in RADIUS do not + terminate with a NUL (hex 00). The Attribute has a length field + and does not use a terminator. Text contains UTF-8 encoded 10646 + [7] characters and String contains 8-bit binary data. Servers and + servers and clients MUST be able to deal with embedded nulls. + RADIUS implementers using C are cautioned not to use strcpy() when + handling strings. + + The format of the value field is one of five data types. Note + that type "text" is a subset of type "string". + + text 1-253 octets containing UTF-8 encoded 10646 [7] + characters. Text of length zero (0) MUST NOT be sent; + omit the entire attribute instead. + + string 1-253 octets containing binary data (values 0 through + 255 decimal, inclusive). Strings of length zero (0) + MUST NOT be sent; omit the entire attribute instead. + + address 32 bit value, most significant octet first. + + integer 32 bit unsigned value, most significant octet first. + + time 32 bit unsigned value, most significant octet first -- + seconds since 00:00:00 UTC, January 1, 1970. The + standard Attributes do not use this data type but it is + presented here for possible use in future attributes. + + + + + + + +Rigney, et al. Standards Track [Page 25] + +RFC 2865 RADIUS June 2000 + + +5.1. User-Name + + Description + + This Attribute indicates the name of the user to be authenticated. + It MUST be sent in Access-Request packets if available. + + It MAY be sent in an Access-Accept packet, in which case the + client SHOULD use the name returned in the Access-Accept packet in + all Accounting-Request packets for this session. If the Access- + Accept includes Service-Type = Rlogin and the User-Name attribute, + a NAS MAY use the returned User-Name when performing the Rlogin + function. + + A summary of the User-Name Attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + | Type | Length | String ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + Type + + 1 for User-Name. + + Length + + >= 3 + + String + + The String field is one or more octets. The NAS may limit the + maximum length of the User-Name but the ability to handle at least + 63 octets is recommended. + + The format of the username MAY be one of several forms: + + text Consisting only of UTF-8 encoded 10646 [7] characters. + + network access identifier + A Network Access Identifier as described in RFC 2486 + [8]. + + distinguished name + A name in ASN.1 form used in Public Key authentication + systems. + + + +Rigney, et al. Standards Track [Page 26] + +RFC 2865 RADIUS June 2000 + + +5.2. User-Password + + Description + + This Attribute indicates the password of the user to be + authenticated, or the user's input following an Access-Challenge. + It is only used in Access-Request packets. + + On transmission, the password is hidden. The password is first + padded at the end with nulls to a multiple of 16 octets. A one- + way MD5 hash is calculated over a stream of octets consisting of + the shared secret followed by the Request Authenticator. This + value is XORed with the first 16 octet segment of the password and + placed in the first 16 octets of the String field of the User- + Password Attribute. + + If the password is longer than 16 characters, a second one-way MD5 + hash is calculated over a stream of octets consisting of the + shared secret followed by the result of the first xor. That hash + is XORed with the second 16 octet segment of the password and + placed in the second 16 octets of the String field of the User- + Password Attribute. + + If necessary, this operation is repeated, with each xor result + being used along with the shared secret to generate the next hash + to xor the next segment of the password, to no more than 128 + characters. + + The method is taken from the book "Network Security" by Kaufman, + Perlman and Speciner [9] pages 109-110. A more precise + explanation of the method follows: + + Call the shared secret S and the pseudo-random 128-bit Request + Authenticator RA. Break the password into 16-octet chunks p1, p2, + etc. with the last one padded at the end with nulls to a 16-octet + boundary. Call the ciphertext blocks c(1), c(2), etc. We'll need + intermediate values b1, b2, etc. + + b1 = MD5(S + RA) c(1) = p1 xor b1 + b2 = MD5(S + c(1)) c(2) = p2 xor b2 + . . + . . + . . + bi = MD5(S + c(i-1)) c(i) = pi xor bi + + The String will contain c(1)+c(2)+...+c(i) where + denotes + concatenation. + + + + +Rigney, et al. Standards Track [Page 27] + +RFC 2865 RADIUS June 2000 + + + On receipt, the process is reversed to yield the original + password. + + A summary of the User-Password Attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + | Type | Length | String ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + Type + + 2 for User-Password. + + Length + + At least 18 and no larger than 130. + + String + + The String field is between 16 and 128 octets long, inclusive. + +5.3. CHAP-Password + + Description + + This Attribute indicates the response value provided by a PPP + Challenge-Handshake Authentication Protocol (CHAP) user in + response to the challenge. It is only used in Access-Request + packets. + + The CHAP challenge value is found in the CHAP-Challenge Attribute + (60) if present in the packet, otherwise in the Request + Authenticator field. + + A summary of the CHAP-Password Attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + | Type | Length | CHAP Ident | String ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + + + + + +Rigney, et al. Standards Track [Page 28] + +RFC 2865 RADIUS June 2000 + + + Type + + 3 for CHAP-Password. + + Length + + 19 + + CHAP Ident + + This field is one octet, and contains the CHAP Identifier from the + user's CHAP Response. + + String + + The String field is 16 octets, and contains the CHAP Response from + the user. + +5.4. NAS-IP-Address + + Description + + This Attribute indicates the identifying IP Address of the NAS + which is requesting authentication of the user, and SHOULD be + unique to the NAS within the scope of the RADIUS server. NAS-IP- + Address is only used in Access-Request packets. Either NAS-IP- + Address or NAS-Identifier MUST be present in an Access-Request + packet. + + Note that NAS-IP-Address MUST NOT be used to select the shared + secret used to authenticate the request. The source IP address of + the Access-Request packet MUST be used to select the shared + secret. + + A summary of the NAS-IP-Address Attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Address + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Address (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 4 for NAS-IP-Address. + + + +Rigney, et al. Standards Track [Page 29] + +RFC 2865 RADIUS June 2000 + + + Length + + 6 + + Address + + The Address field is four octets. + +5.5. NAS-Port + + Description + + This Attribute indicates the physical port number of the NAS which + is authenticating the user. It is only used in Access-Request + packets. Note that this is using "port" in its sense of a + physical connection on the NAS, not in the sense of a TCP or UDP + port number. Either NAS-Port or NAS-Port-Type (61) or both SHOULD + be present in an Access-Request packet, if the NAS differentiates + among its ports. + + A summary of the NAS-Port Attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 5 for NAS-Port. + + Length + + 6 + + Value + + The Value field is four octets. + + + + + + + + + +Rigney, et al. Standards Track [Page 30] + +RFC 2865 RADIUS June 2000 + + +5.6. Service-Type + + Description + + This Attribute indicates the type of service the user has + requested, or the type of service to be provided. It MAY be used + in both Access-Request and Access-Accept packets. A NAS is not + required to implement all of these service types, and MUST treat + unknown or unsupported Service-Types as though an Access-Reject + had been received instead. + + A summary of the Service-Type Attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 6 for Service-Type. + + Length + + 6 + + Value + + The Value field is four octets. + + 1 Login + 2 Framed + 3 Callback Login + 4 Callback Framed + 5 Outbound + 6 Administrative + 7 NAS Prompt + 8 Authenticate Only + 9 Callback NAS Prompt + 10 Call Check + 11 Callback Administrative + + + + + + +Rigney, et al. Standards Track [Page 31] + +RFC 2865 RADIUS June 2000 + + + The service types are defined as follows when used in an Access- + Accept. When used in an Access-Request, they MAY be considered to + be a hint to the RADIUS server that the NAS has reason to believe + the user would prefer the kind of service indicated, but the + server is not required to honor the hint. + + Login The user should be connected to a host. + + Framed A Framed Protocol should be started for the + User, such as PPP or SLIP. + + Callback Login The user should be disconnected and called + back, then connected to a host. + + Callback Framed The user should be disconnected and called + back, then a Framed Protocol should be started + for the User, such as PPP or SLIP. + + Outbound The user should be granted access to outgoing + devices. + + Administrative The user should be granted access to the + administrative interface to the NAS from which + privileged commands can be executed. + + NAS Prompt The user should be provided a command prompt + on the NAS from which non-privileged commands + can be executed. + + Authenticate Only Only Authentication is requested, and no + authorization information needs to be returned + in the Access-Accept (typically used by proxy + servers rather than the NAS itself). + + Callback NAS Prompt The user should be disconnected and called + back, then provided a command prompt on the + NAS from which non-privileged commands can be + executed. + + Call Check Used by the NAS in an Access-Request packet to + indicate that a call is being received and + that the RADIUS server should send back an + Access-Accept to answer the call, or an + Access-Reject to not accept the call, + typically based on the Called-Station-Id or + Calling-Station-Id attributes. It is + + + + + +Rigney, et al. Standards Track [Page 32] + +RFC 2865 RADIUS June 2000 + + + recommended that such Access-Requests use the + value of Calling-Station-Id as the value of + the User-Name. + + Callback Administrative + The user should be disconnected and called + back, then granted access to the + administrative interface to the NAS from which + privileged commands can be executed. + +5.7. Framed-Protocol + + Description + + This Attribute indicates the framing to be used for framed access. + It MAY be used in both Access-Request and Access-Accept packets. + + A summary of the Framed-Protocol Attribute format is shown below. + The fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 7 for Framed-Protocol. + + Length + + 6 + + Value + + The Value field is four octets. + + 1 PPP + 2 SLIP + 3 AppleTalk Remote Access Protocol (ARAP) + 4 Gandalf proprietary SingleLink/MultiLink protocol + 5 Xylogics proprietary IPX/SLIP + 6 X.75 Synchronous + + + + + +Rigney, et al. Standards Track [Page 33] + +RFC 2865 RADIUS June 2000 + + +5.8. Framed-IP-Address + + Description + + This Attribute indicates the address to be configured for the + user. It MAY be used in Access-Accept packets. It MAY be used in + an Access-Request packet as a hint by the NAS to the server that + it would prefer that address, but the server is not required to + honor the hint. + + A summary of the Framed-IP-Address Attribute format is shown below. + The fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Address + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Address (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 8 for Framed-IP-Address. + + Length + + 6 + + Address + + The Address field is four octets. The value 0xFFFFFFFF indicates + that the NAS Should allow the user to select an address (e.g. + Negotiated). The value 0xFFFFFFFE indicates that the NAS should + select an address for the user (e.g. Assigned from a pool of + addresses kept by the NAS). Other valid values indicate that the + NAS should use that value as the user's IP address. + +5.9. Framed-IP-Netmask + + Description + + This Attribute indicates the IP netmask to be configured for the + user when the user is a router to a network. It MAY be used in + Access-Accept packets. It MAY be used in an Access-Request packet + as a hint by the NAS to the server that it would prefer that + netmask, but the server is not required to honor the hint. + + + + +Rigney, et al. Standards Track [Page 34] + +RFC 2865 RADIUS June 2000 + + + A summary of the Framed-IP-Netmask Attribute format is shown below. + The fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Address + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Address (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 9 for Framed-IP-Netmask. + + Length + + 6 + + Address + + The Address field is four octets specifying the IP netmask of the + user. + +5.10. Framed-Routing + + Description + + This Attribute indicates the routing method for the user, when the + user is a router to a network. It is only used in Access-Accept + packets. + + A summary of the Framed-Routing Attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 10 for Framed-Routing. + + + + + +Rigney, et al. Standards Track [Page 35] + +RFC 2865 RADIUS June 2000 + + + Length + + 6 + + Value + + The Value field is four octets. + + 0 None + 1 Send routing packets + 2 Listen for routing packets + 3 Send and Listen + +5.11. Filter-Id + + Description + + This Attribute indicates the name of the filter list for this + user. Zero or more Filter-Id attributes MAY be sent in an + Access-Accept packet. + + Identifying a filter list by name allows the filter to be used on + different NASes without regard to filter-list implementation + details. + + A summary of the Filter-Id Attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + | Type | Length | Text ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + Type + + 11 for Filter-Id. + + Length + + >= 3 + + Text + + The Text field is one or more octets, and its contents are + implementation dependent. It is intended to be human readable and + MUST NOT affect operation of the protocol. It is recommended that + the message contain UTF-8 encoded 10646 [7] characters. + + + +Rigney, et al. Standards Track [Page 36] + +RFC 2865 RADIUS June 2000 + + +5.12. Framed-MTU + + Description + + This Attribute indicates the Maximum Transmission Unit to be + configured for the user, when it is not negotiated by some other + means (such as PPP). It MAY be used in Access-Accept packets. It + MAY be used in an Access-Request packet as a hint by the NAS to + the server that it would prefer that value, but the server is not + required to honor the hint. + + A summary of the Framed-MTU Attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 12 for Framed-MTU. + + Length + + 6 + + Value + + The Value field is four octets. Despite the size of the field, + values range from 64 to 65535. + +5.13. Framed-Compression + + Description + + This Attribute indicates a compression protocol to be used for the + link. It MAY be used in Access-Accept packets. It MAY be used in + an Access-Request packet as a hint to the server that the NAS + would prefer to use that compression, but the server is not + required to honor the hint. + + More than one compression protocol Attribute MAY be sent. It is + the responsibility of the NAS to apply the proper compression + protocol to appropriate link traffic. + + + +Rigney, et al. Standards Track [Page 37] + +RFC 2865 RADIUS June 2000 + + + A summary of the Framed-Compression Attribute format is shown below. + The fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 13 for Framed-Compression. + + Length + + 6 + + Value + + The Value field is four octets. + + 0 None + 1 VJ TCP/IP header compression [10] + 2 IPX header compression + 3 Stac-LZS compression + +5.14. Login-IP-Host + + Description + + This Attribute indicates the system with which to connect the user, + when the Login-Service Attribute is included. It MAY be used in + Access-Accept packets. It MAY be used in an Access-Request packet as + a hint to the server that the NAS would prefer to use that host, but + the server is not required to honor the hint. + + A summary of the Login-IP-Host Attribute format is shown below. The + fields are transmitted from left to right. + + + + + + + + + + + +Rigney, et al. Standards Track [Page 38] + +RFC 2865 RADIUS June 2000 + + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Address + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Address (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 14 for Login-IP-Host. + + Length + + 6 + + Address + + The Address field is four octets. The value 0xFFFFFFFF indicates + that the NAS SHOULD allow the user to select an address. The + value 0 indicates that the NAS SHOULD select a host to connect the + user to. Other values indicate the address the NAS SHOULD connect + the user to. + +5.15. Login-Service + + Description + + This Attribute indicates the service to use to connect the user to + the login host. It is only used in Access-Accept packets. + + A summary of the Login-Service Attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 15 for Login-Service. + + + + + + +Rigney, et al. Standards Track [Page 39] + +RFC 2865 RADIUS June 2000 + + + Length + + 6 + + Value + + The Value field is four octets. + + 0 Telnet + 1 Rlogin + 2 TCP Clear + 3 PortMaster (proprietary) + 4 LAT + 5 X25-PAD + 6 X25-T3POS + 8 TCP Clear Quiet (suppresses any NAS-generated connect string) + +5.16. Login-TCP-Port + + Description + + This Attribute indicates the TCP port with which the user is to be + connected, when the Login-Service Attribute is also present. It + is only used in Access-Accept packets. + + A summary of the Login-TCP-Port Attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 16 for Login-TCP-Port. + + Length + + 6 + + Value + + The Value field is four octets. Despite the size of the field, + values range from 0 to 65535. + + + +Rigney, et al. Standards Track [Page 40] + +RFC 2865 RADIUS June 2000 + + +5.17. (unassigned) + + Description + + ATTRIBUTE TYPE 17 HAS NOT BEEN ASSIGNED. + +5.18. Reply-Message + + Description + + This Attribute indicates text which MAY be displayed to the user. + + When used in an Access-Accept, it is the success message. + + When used in an Access-Reject, it is the failure message. It MAY + indicate a dialog message to prompt the user before another + Access-Request attempt. + + When used in an Access-Challenge, it MAY indicate a dialog message + to prompt the user for a response. + + Multiple Reply-Message's MAY be included and if any are displayed, + they MUST be displayed in the same order as they appear in the + packet. + + A summary of the Reply-Message Attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + | Type | Length | Text ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + Type + + 18 for Reply-Message. + + Length + + >= 3 + + Text + + The Text field is one or more octets, and its contents are + implementation dependent. It is intended to be human readable, + and MUST NOT affect operation of the protocol. It is recommended + that the message contain UTF-8 encoded 10646 [7] characters. + + + +Rigney, et al. Standards Track [Page 41] + +RFC 2865 RADIUS June 2000 + + +5.19. Callback-Number + + Description + + This Attribute indicates a dialing string to be used for callback. + It MAY be used in Access-Accept packets. It MAY be used in an + Access-Request packet as a hint to the server that a Callback + service is desired, but the server is not required to honor the + hint. + + A summary of the Callback-Number Attribute format is shown below. + The fields are transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + | Type | Length | String ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + Type + + 19 for Callback-Number. + + Length + + >= 3 + + String + + The String field is one or more octets. The actual format of the + information is site or application specific, and a robust + implementation SHOULD support the field as undistinguished octets. + + The codification of the range of allowed usage of this field is + outside the scope of this specification. + +5.20. Callback-Id + + Description + + This Attribute indicates the name of a place to be called, to be + interpreted by the NAS. It MAY be used in Access-Accept packets. + + + + + + + + + +Rigney, et al. Standards Track [Page 42] + +RFC 2865 RADIUS June 2000 + + + A summary of the Callback-Id Attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + | Type | Length | String ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + Type + + 20 for Callback-Id. + + Length + + >= 3 + + String + + The String field is one or more octets. The actual format of the + information is site or application specific, and a robust + implementation SHOULD support the field as undistinguished octets. + + The codification of the range of allowed usage of this field is + outside the scope of this specification. + +5.21. (unassigned) + + Description + + ATTRIBUTE TYPE 21 HAS NOT BEEN ASSIGNED. + +5.22. Framed-Route + + Description + + This Attribute provides routing information to be configured for + the user on the NAS. It is used in the Access-Accept packet and + can appear multiple times. + + A summary of the Framed-Route Attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + | Type | Length | Text ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + + +Rigney, et al. Standards Track [Page 43] + +RFC 2865 RADIUS June 2000 + + + Type + + 22 for Framed-Route. + + Length + + >= 3 + + Text + + The Text field is one or more octets, and its contents are + implementation dependent. It is intended to be human readable and + MUST NOT affect operation of the protocol. It is recommended that + the message contain UTF-8 encoded 10646 [7] characters. + + For IP routes, it SHOULD contain a destination prefix in dotted + quad form optionally followed by a slash and a decimal length + specifier stating how many high order bits of the prefix to use. + That is followed by a space, a gateway address in dotted quad + form, a space, and one or more metrics separated by spaces. For + example, "192.168.1.0/24 192.168.1.1 1 2 -1 3 400". The length + specifier may be omitted, in which case it defaults to 8 bits for + class A prefixes, 16 bits for class B prefixes, and 24 bits for + class C prefixes. For example, "192.168.1.0 192.168.1.1 1". + + Whenever the gateway address is specified as "0.0.0.0" the IP + address of the user SHOULD be used as the gateway address. + +5.23. Framed-IPX-Network + + Description + + This Attribute indicates the IPX Network number to be configured + for the user. It is used in Access-Accept packets. + + A summary of the Framed-IPX-Network Attribute format is shown below. + The fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + + + + + +Rigney, et al. Standards Track [Page 44] + +RFC 2865 RADIUS June 2000 + + + Type + + 23 for Framed-IPX-Network. + + Length + + 6 + + Value + + The Value field is four octets. The value 0xFFFFFFFE indicates + that the NAS should select an IPX network for the user (e.g. + assigned from a pool of one or more IPX networks kept by the NAS). + Other values should be used as the IPX network for the link to the + user. + +5.24. State + + Description + + This Attribute is available to be sent by the server to the client + in an Access-Challenge and MUST be sent unmodified from the client + to the server in the new Access-Request reply to that challenge, + if any. + + This Attribute is available to be sent by the server to the client + in an Access-Accept that also includes a Termination-Action + Attribute with the value of RADIUS-Request. If the NAS performs + the Termination-Action by sending a new Access-Request upon + termination of the current session, it MUST include the State + attribute unchanged in that Access-Request. + + In either usage, the client MUST NOT interpret the attribute + locally. A packet must have only zero or one State Attribute. + Usage of the State Attribute is implementation dependent. + + A summary of the State Attribute format is shown below. The fields + are transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + | Type | Length | String ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + Type + + 24 for State. + + + +Rigney, et al. Standards Track [Page 45] + +RFC 2865 RADIUS June 2000 + + + Length + + >= 3 + + String + + The String field is one or more octets. The actual format of the + information is site or application specific, and a robust + implementation SHOULD support the field as undistinguished octets. + + The codification of the range of allowed usage of this field is + outside the scope of this specification. + +5.25. Class + + Description + + This Attribute is available to be sent by the server to the client + in an Access-Accept and SHOULD be sent unmodified by the client to + the accounting server as part of the Accounting-Request packet if + accounting is supported. The client MUST NOT interpret the + attribute locally. + + A summary of the Class Attribute format is shown below. The fields + are transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + | Type | Length | String ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + Type + + 25 for Class. + + Length + + >= 3 + + String + + The String field is one or more octets. The actual format of the + information is site or application specific, and a robust + implementation SHOULD support the field as undistinguished octets. + + The codification of the range of allowed usage of this field is + outside the scope of this specification. + + + +Rigney, et al. Standards Track [Page 46] + +RFC 2865 RADIUS June 2000 + + +5.26. Vendor-Specific + + Description + + This Attribute is available to allow vendors to support their own + extended Attributes not suitable for general usage. It MUST not + affect the operation of the RADIUS protocol. + + Servers not equipped to interpret the vendor-specific information + sent by a client MUST ignore it (although it may be reported). + Clients which do not receive desired vendor-specific information + SHOULD make an attempt to operate without it, although they may do + so (and report they are doing so) in a degraded mode. + + A summary of the Vendor-Specific Attribute format is shown below. + The fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Vendor-Id + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Vendor-Id (cont) | String... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + Type + + 26 for Vendor-Specific. + + Length + + >= 7 + + Vendor-Id + + The high-order octet is 0 and the low-order 3 octets are the SMI + Network Management Private Enterprise Code of the Vendor in + network byte order, as defined in the "Assigned Numbers" RFC [6]. + + String + + The String field is one or more octets. The actual format of the + information is site or application specific, and a robust + implementation SHOULD support the field as undistinguished octets. + + The codification of the range of allowed usage of this field is + outside the scope of this specification. + + + + +Rigney, et al. Standards Track [Page 47] + +RFC 2865 RADIUS June 2000 + + + It SHOULD be encoded as a sequence of vendor type / vendor length + / value fields, as follows. The Attribute-Specific field is + dependent on the vendor's definition of that attribute. An + example encoding of the Vendor-Specific attribute using this + method follows: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Vendor-Id + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Vendor-Id (cont) | Vendor type | Vendor length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Attribute-Specific... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + Multiple subattributes MAY be encoded within a single Vendor- + Specific attribute, although they do not have to be. + +5.27. Session-Timeout + + Description + + This Attribute sets the maximum number of seconds of service to be + provided to the user before termination of the session or prompt. + This Attribute is available to be sent by the server to the client + in an Access-Accept or Access-Challenge. + + A summary of the Session-Timeout Attribute format is shown below. + The fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 27 for Session-Timeout. + + Length + + 6 + + + + + +Rigney, et al. Standards Track [Page 48] + +RFC 2865 RADIUS June 2000 + + + Value + + The field is 4 octets, containing a 32-bit unsigned integer with + the maximum number of seconds this user should be allowed to + remain connected by the NAS. + +5.28. Idle-Timeout + + Description + + This Attribute sets the maximum number of consecutive seconds of + idle connection allowed to the user before termination of the + session or prompt. This Attribute is available to be sent by the + server to the client in an Access-Accept or Access-Challenge. + + A summary of the Idle-Timeout Attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 28 for Idle-Timeout. + + Length + + 6 + + Value + + The field is 4 octets, containing a 32-bit unsigned integer with + the maximum number of consecutive seconds of idle time this user + should be permitted before being disconnected by the NAS. + +5.29. Termination-Action + + Description + + This Attribute indicates what action the NAS should take when the + specified service is completed. It is only used in Access-Accept + packets. + + + + +Rigney, et al. Standards Track [Page 49] + +RFC 2865 RADIUS June 2000 + + + A summary of the Termination-Action Attribute format is shown below. + The fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 29 for Termination-Action. + + Length + + 6 + + Value + + The Value field is four octets. + + 0 Default + 1 RADIUS-Request + + If the Value is set to RADIUS-Request, upon termination of the + specified service the NAS MAY send a new Access-Request to the + RADIUS server, including the State attribute if any. + +5.30. Called-Station-Id + + Description + + This Attribute allows the NAS to send in the Access-Request packet + the phone number that the user called, using Dialed Number + Identification (DNIS) or similar technology. Note that this may + be different from the phone number the call comes in on. It is + only used in Access-Request packets. + + A summary of the Called-Station-Id Attribute format is shown below. + The fields are transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + | Type | Length | String ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + + +Rigney, et al. Standards Track [Page 50] + +RFC 2865 RADIUS June 2000 + + + Type + + 30 for Called-Station-Id. + + Length + + >= 3 + + String + + The String field is one or more octets, containing the phone + number that the user's call came in on. + + The actual format of the information is site or application + specific. UTF-8 encoded 10646 [7] characters are recommended, but + a robust implementation SHOULD support the field as + undistinguished octets. + + The codification of the range of allowed usage of this field is + outside the scope of this specification. + +5.31. Calling-Station-Id + + Description + + This Attribute allows the NAS to send in the Access-Request packet + the phone number that the call came from, using Automatic Number + Identification (ANI) or similar technology. It is only used in + Access-Request packets. + + A summary of the Calling-Station-Id Attribute format is shown below. + The fields are transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + | Type | Length | String ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + Type + + 31 for Calling-Station-Id. + + Length + + >= 3 + + + + + +Rigney, et al. Standards Track [Page 51] + +RFC 2865 RADIUS June 2000 + + + String + + The String field is one or more octets, containing the phone + number that the user placed the call from. + + The actual format of the information is site or application + specific. UTF-8 encoded 10646 [7] characters are recommended, but + a robust implementation SHOULD support the field as + undistinguished octets. + + The codification of the range of allowed usage of this field is + outside the scope of this specification. + +5.32. NAS-Identifier + + Description + + This Attribute contains a string identifying the NAS originating + the Access-Request. It is only used in Access-Request packets. + Either NAS-IP-Address or NAS-Identifier MUST be present in an + Access-Request packet. + + Note that NAS-Identifier MUST NOT be used to select the shared + secret used to authenticate the request. The source IP address of + the Access-Request packet MUST be used to select the shared + secret. + + A summary of the NAS-Identifier Attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + | Type | Length | String ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + Type + + 32 for NAS-Identifier. + + Length + + >= 3 + + + + + + + + +Rigney, et al. Standards Track [Page 52] + +RFC 2865 RADIUS June 2000 + + + String + + The String field is one or more octets, and should be unique to + the NAS within the scope of the RADIUS server. For example, a + fully qualified domain name would be suitable as a NAS-Identifier. + + The actual format of the information is site or application + specific, and a robust implementation SHOULD support the field as + undistinguished octets. + + The codification of the range of allowed usage of this field is + outside the scope of this specification. + +5.33. Proxy-State + + Description + + This Attribute is available to be sent by a proxy server to + another server when forwarding an Access-Request and MUST be + returned unmodified in the Access-Accept, Access-Reject or + Access-Challenge. When the proxy server receives the response to + its request, it MUST remove its own Proxy-State (the last Proxy- + State in the packet) before forwarding the response to the NAS. + + If a Proxy-State Attribute is added to a packet when forwarding + the packet, the Proxy-State Attribute MUST be added after any + existing Proxy-State attributes. + + The content of any Proxy-State other than the one added by the + current server should be treated as opaque octets and MUST NOT + affect operation of the protocol. + + Usage of the Proxy-State Attribute is implementation dependent. A + description of its function is outside the scope of this + specification. + + A summary of the Proxy-State Attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + | Type | Length | String ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + Type + + 33 for Proxy-State. + + + +Rigney, et al. Standards Track [Page 53] + +RFC 2865 RADIUS June 2000 + + + Length + + >= 3 + + String + + The String field is one or more octets. The actual format of the + information is site or application specific, and a robust + implementation SHOULD support the field as undistinguished octets. + + The codification of the range of allowed usage of this field is + outside the scope of this specification. + +5.34. Login-LAT-Service + + Description + + This Attribute indicates the system with which the user is to be + connected by LAT. It MAY be used in Access-Accept packets, but + only when LAT is specified as the Login-Service. It MAY be used + in an Access-Request packet as a hint to the server, but the + server is not required to honor the hint. + + Administrators use the service attribute when dealing with + clustered systems, such as a VAX or Alpha cluster. In such an + environment several different time sharing hosts share the same + resources (disks, printers, etc.), and administrators often + configure each to offer access (service) to each of the shared + resources. In this case, each host in the cluster advertises its + services through LAT broadcasts. + + Sophisticated users often know which service providers (machines) + are faster and tend to use a node name when initiating a LAT + connection. Alternately, some administrators want particular + users to use certain machines as a primitive form of load + balancing (although LAT knows how to do load balancing itself). + + A summary of the Login-LAT-Service Attribute format is shown below. + The fields are transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + | Type | Length | String ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + + + + + +Rigney, et al. Standards Track [Page 54] + +RFC 2865 RADIUS June 2000 + + + Type + + 34 for Login-LAT-Service. + + Length + + >= 3 + + String + + The String field is one or more octets, and contains the identity + of the LAT service to use. The LAT Architecture allows this + string to contain $ (dollar), - (hyphen), . (period), _ + (underscore), numerics, upper and lower case alphabetics, and the + ISO Latin-1 character set extension [11]. All LAT string + comparisons are case insensitive. + +5.35. Login-LAT-Node + + Description + + This Attribute indicates the Node with which the user is to be + automatically connected by LAT. It MAY be used in Access-Accept + packets, but only when LAT is specified as the Login-Service. It + MAY be used in an Access-Request packet as a hint to the server, + but the server is not required to honor the hint. + + A summary of the Login-LAT-Node Attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + | Type | Length | String ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + Type + + 35 for Login-LAT-Node. + + Length + + >= 3 + + + + + + + + +Rigney, et al. Standards Track [Page 55] + +RFC 2865 RADIUS June 2000 + + + String + + The String field is one or more octets, and contains the identity + of the LAT Node to connect the user to. The LAT Architecture + allows this string to contain $ (dollar), - (hyphen), . (period), + _ (underscore), numerics, upper and lower case alphabetics, and + the ISO Latin-1 character set extension. All LAT string + comparisons are case insensitive. + +5.36. Login-LAT-Group + + Description + + This Attribute contains a string identifying the LAT group codes + which this user is authorized to use. It MAY be used in Access- + Accept packets, but only when LAT is specified as the Login- + Service. It MAY be used in an Access-Request packet as a hint to + the server, but the server is not required to honor the hint. + + LAT supports 256 different group codes, which LAT uses as a form + of access rights. LAT encodes the group codes as a 256 bit + bitmap. + + Administrators can assign one or more of the group code bits at + the LAT service provider; it will only accept LAT connections that + have these group codes set in the bit map. The administrators + assign a bitmap of authorized group codes to each user; LAT gets + these from the operating system, and uses these in its requests to + the service providers. + + A summary of the Login-LAT-Group Attribute format is shown below. + The fields are transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + | Type | Length | String ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + Type + + 36 for Login-LAT-Group. + + Length + + 34 + + + + + +Rigney, et al. Standards Track [Page 56] + +RFC 2865 RADIUS June 2000 + + + String + + The String field is a 32 octet bit map, most significant octet + first. A robust implementation SHOULD support the field as + undistinguished octets. + + The codification of the range of allowed usage of this field is + outside the scope of this specification. + +5.37. Framed-AppleTalk-Link + + Description + + This Attribute indicates the AppleTalk network number which should + be used for the serial link to the user, which is another + AppleTalk router. It is only used in Access-Accept packets. It + is never used when the user is not another router. + + A summary of the Framed-AppleTalk-Link Attribute format is shown + below. The fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 37 for Framed-AppleTalk-Link. + + Length + + 6 + + Value + + The Value field is four octets. Despite the size of the field, + values range from 0 to 65535. The special value of 0 indicates + that this is an unnumbered serial link. A value of 1-65535 means + that the serial line between the NAS and the user should be + assigned that value as an AppleTalk network number. + + + + + + + +Rigney, et al. Standards Track [Page 57] + +RFC 2865 RADIUS June 2000 + + +5.38. Framed-AppleTalk-Network + + Description + + This Attribute indicates the AppleTalk Network number which the + NAS should probe to allocate an AppleTalk node for the user. It + is only used in Access-Accept packets. It is never used when the + user is another router. Multiple instances of this Attribute + indicate that the NAS may probe using any of the network numbers + specified. + + A summary of the Framed-AppleTalk-Network Attribute format is shown + below. The fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 38 for Framed-AppleTalk-Network. + + Length + + 6 + + Value + + The Value field is four octets. Despite the size of the field, + values range from 0 to 65535. The special value 0 indicates that + the NAS should assign a network for the user, using its default + cable range. A value between 1 and 65535 (inclusive) indicates + the AppleTalk Network the NAS should probe to find an address for + the user. + +5.39. Framed-AppleTalk-Zone + + Description + + This Attribute indicates the AppleTalk Default Zone to be used for + this user. It is only used in Access-Accept packets. Multiple + instances of this attribute in the same packet are not allowed. + + + + + +Rigney, et al. Standards Track [Page 58] + +RFC 2865 RADIUS June 2000 + + + A summary of the Framed-AppleTalk-Zone Attribute format is shown + below. The fields are transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + | Type | Length | String ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + Type + + 39 for Framed-AppleTalk-Zone. + + Length + + >= 3 + + String + + The name of the Default AppleTalk Zone to be used for this user. + A robust implementation SHOULD support the field as + undistinguished octets. + + The codification of the range of allowed usage of this field is + outside the scope of this specification. + +5.40. CHAP-Challenge + + Description + + This Attribute contains the CHAP Challenge sent by the NAS to a + PPP Challenge-Handshake Authentication Protocol (CHAP) user. It + is only used in Access-Request packets. + + If the CHAP challenge value is 16 octets long it MAY be placed in + the Request Authenticator field instead of using this attribute. + + A summary of the CHAP-Challenge Attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + | Type | Length | String... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + + + + + +Rigney, et al. Standards Track [Page 59] + +RFC 2865 RADIUS June 2000 + + + Type + + 60 for CHAP-Challenge. + + Length + + >= 7 + + String + + The String field contains the CHAP Challenge. + +5.41. NAS-Port-Type + + Description + + This Attribute indicates the type of the physical port of the NAS + which is authenticating the user. It can be used instead of or in + addition to the NAS-Port (5) attribute. It is only used in + Access-Request packets. Either NAS-Port (5) or NAS-Port-Type or + both SHOULD be present in an Access-Request packet, if the NAS + differentiates among its ports. + + A summary of the NAS-Port-Type Attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 61 for NAS-Port-Type. + + Length + + 6 + + Value + + The Value field is four octets. "Virtual" refers to a connection + to the NAS via some transport protocol, instead of through a + physical port. For example, if a user telnetted into a NAS to + + + + +Rigney, et al. Standards Track [Page 60] + +RFC 2865 RADIUS June 2000 + + + authenticate himself as an Outbound-User, the Access-Request might + include NAS-Port-Type = Virtual as a hint to the RADIUS server + that the user was not on a physical port. + + 0 Async + 1 Sync + 2 ISDN Sync + 3 ISDN Async V.120 + 4 ISDN Async V.110 + 5 Virtual + 6 PIAFS + 7 HDLC Clear Channel + 8 X.25 + 9 X.75 + 10 G.3 Fax + 11 SDSL - Symmetric DSL + 12 ADSL-CAP - Asymmetric DSL, Carrierless Amplitude Phase + Modulation + 13 ADSL-DMT - Asymmetric DSL, Discrete Multi-Tone + 14 IDSL - ISDN Digital Subscriber Line + 15 Ethernet + 16 xDSL - Digital Subscriber Line of unknown type + 17 Cable + 18 Wireless - Other + 19 Wireless - IEEE 802.11 + + PIAFS is a form of wireless ISDN commonly used in Japan, and + stands for PHS (Personal Handyphone System) Internet Access Forum + Standard (PIAFS). + +5.42. Port-Limit + + Description + + This Attribute sets the maximum number of ports to be provided to + the user by the NAS. This Attribute MAY be sent by the server to + the client in an Access-Accept packet. It is intended for use in + conjunction with Multilink PPP [12] or similar uses. It MAY also + be sent by the NAS to the server as a hint that that many ports + are desired for use, but the server is not required to honor the + hint. + + A summary of the Port-Limit Attribute format is shown below. The + fields are transmitted from left to right. + + + + + + + +Rigney, et al. Standards Track [Page 61] + +RFC 2865 RADIUS June 2000 + + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 62 for Port-Limit. + + Length + + 6 + + Value + + The field is 4 octets, containing a 32-bit unsigned integer with + the maximum number of ports this user should be allowed to connect + to on the NAS. + +5.43. Login-LAT-Port + + Description + + This Attribute indicates the Port with which the user is to be + connected by LAT. It MAY be used in Access-Accept packets, but + only when LAT is specified as the Login-Service. It MAY be used + in an Access-Request packet as a hint to the server, but the + server is not required to honor the hint. + + A summary of the Login-LAT-Port Attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + | Type | Length | String ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- + + Type + + 63 for Login-LAT-Port. + + Length + + >= 3 + + + +Rigney, et al. Standards Track [Page 62] + +RFC 2865 RADIUS June 2000 + + + String + + The String field is one or more octets, and contains the identity + of the LAT port to use. The LAT Architecture allows this string + to contain $ (dollar), - (hyphen), . (period), _ (underscore), + numerics, upper and lower case alphabetics, and the ISO Latin-1 + character set extension. All LAT string comparisons are case + insensitive. + +5.44. Table of Attributes + + The following table provides a guide to which attributes may be found + in which kinds of packets, and in what quantity. + + Request Accept Reject Challenge # Attribute + 0-1 0-1 0 0 1 User-Name + 0-1 0 0 0 2 User-Password [Note 1] + 0-1 0 0 0 3 CHAP-Password [Note 1] + 0-1 0 0 0 4 NAS-IP-Address [Note 2] + 0-1 0 0 0 5 NAS-Port + 0-1 0-1 0 0 6 Service-Type + 0-1 0-1 0 0 7 Framed-Protocol + 0-1 0-1 0 0 8 Framed-IP-Address + 0-1 0-1 0 0 9 Framed-IP-Netmask + 0 0-1 0 0 10 Framed-Routing + 0 0+ 0 0 11 Filter-Id + 0-1 0-1 0 0 12 Framed-MTU + 0+ 0+ 0 0 13 Framed-Compression + 0+ 0+ 0 0 14 Login-IP-Host + 0 0-1 0 0 15 Login-Service + 0 0-1 0 0 16 Login-TCP-Port + 0 0+ 0+ 0+ 18 Reply-Message + 0-1 0-1 0 0 19 Callback-Number + 0 0-1 0 0 20 Callback-Id + 0 0+ 0 0 22 Framed-Route + 0 0-1 0 0 23 Framed-IPX-Network + 0-1 0-1 0 0-1 24 State [Note 1] + 0 0+ 0 0 25 Class + 0+ 0+ 0 0+ 26 Vendor-Specific + 0 0-1 0 0-1 27 Session-Timeout + 0 0-1 0 0-1 28 Idle-Timeout + 0 0-1 0 0 29 Termination-Action + 0-1 0 0 0 30 Called-Station-Id + 0-1 0 0 0 31 Calling-Station-Id + 0-1 0 0 0 32 NAS-Identifier [Note 2] + 0+ 0+ 0+ 0+ 33 Proxy-State + 0-1 0-1 0 0 34 Login-LAT-Service + 0-1 0-1 0 0 35 Login-LAT-Node + + + +Rigney, et al. Standards Track [Page 63] + +RFC 2865 RADIUS June 2000 + + + 0-1 0-1 0 0 36 Login-LAT-Group + 0 0-1 0 0 37 Framed-AppleTalk-Link + 0 0+ 0 0 38 Framed-AppleTalk-Network + 0 0-1 0 0 39 Framed-AppleTalk-Zone + 0-1 0 0 0 60 CHAP-Challenge + 0-1 0 0 0 61 NAS-Port-Type + 0-1 0-1 0 0 62 Port-Limit + 0-1 0-1 0 0 63 Login-LAT-Port + Request Accept Reject Challenge # Attribute + + [Note 1] An Access-Request MUST contain either a User-Password or a + CHAP-Password or State. An Access-Request MUST NOT contain both a + User-Password and a CHAP-Password. If future extensions allow other + kinds of authentication information to be conveyed, the attribute for + that can be used in an Access-Request instead of User-Password or + CHAP-Password. + + [Note 2] An Access-Request MUST contain either a NAS-IP-Address or a + NAS-Identifier (or both). + + The following table defines the meaning of the above table entries. + +0 This attribute MUST NOT be present in packet. +0+ Zero or more instances of this attribute MAY be present in packet. +0-1 Zero or one instance of this attribute MAY be present in packet. +1 Exactly one instance of this attribute MUST be present in packet. + +6. IANA Considerations + + This section provides guidance to the Internet Assigned Numbers + Authority (IANA) regarding registration of values related to the + RADIUS protocol, in accordance with BCP 26 [13]. + + There are three name spaces in RADIUS that require registration: + Packet Type Codes, Attribute Types, and Attribute Values (for certain + Attributes). + + RADIUS is not intended as a general-purpose Network Access Server + (NAS) management protocol, and allocations should not be made for + purposes unrelated to Authentication, Authorization or Accounting. + +6.1. Definition of Terms + + The following terms are used here with the meanings defined in + BCP 26: "name space", "assigned value", "registration". + + + + + + +Rigney, et al. Standards Track [Page 64] + +RFC 2865 RADIUS June 2000 + + + The following policies are used here with the meanings defined in + BCP 26: "Private Use", "First Come First Served", "Expert Review", + "Specification Required", "IETF Consensus", "Standards Action". + +6.2. Recommended Registration Policies + + For registration requests where a Designated Expert should be + consulted, the IESG Area Director for Operations should appoint the + Designated Expert. + + For registration requests requiring Expert Review, the ietf-radius + mailing list should be consulted. + + Packet Type Codes have a range from 1 to 254, of which 1-5,11-13 have + been allocated. Because a new Packet Type has considerable impact on + interoperability, a new Packet Type Code requires Standards Action, + and should be allocated starting at 14. + + Attribute Types have a range from 1 to 255, and are the scarcest + resource in RADIUS, thus must be allocated with care. Attributes + 1-53,55,60-88,90-91 have been allocated, with 17 and 21 available for + re-use. Attributes 17, 21, 54, 56-59, 89, 92-191 may be allocated + following Expert Review, with Specification Required. Release of + blocks of Attribute Types (more than 3 at a time for a given purpose) + should require IETF Consensus. It is recommended that attributes 17 + and 21 be used only after all others are exhausted. + + Note that RADIUS defines a mechanism for Vendor-Specific extensions + (Attribute 26) and the use of that should be encouraged instead of + allocation of global attribute types, for functions specific only to + one vendor's implementation of RADIUS, where no interoperability is + deemed useful. + + As stated in the "Attributes" section above: + + "[Attribute Type] Values 192-223 are reserved for experimental + use, values 224-240 are reserved for implementation-specific use, + and values 241-255 are reserved and should not be used." + + Therefore Attribute values 192-240 are considered Private Use, and + values 241-255 require Standards Action. + + Certain attributes (for example, NAS-Port-Type) in RADIUS define a + list of values to correspond with various meanings. There can be 4 + billion (2^32) values for each attribute. Adding additional values to + the list can be done on a First Come, First Served basis by the IANA. + + + + + +Rigney, et al. Standards Track [Page 65] + +RFC 2865 RADIUS June 2000 + + +7. Examples + + A few examples are presented to illustrate the flow of packets and + use of typical attributes. These examples are not intended to be + exhaustive, many others are possible. Hexadecimal dumps of the + example packets are given in network byte order, using the shared + secret "xyzzy5461". + +7.1. User Telnet to Specified Host + + The NAS at 192.168.1.16 sends an Access-Request UDP packet to the + RADIUS Server for a user named nemo logging in on port 3 with + password "arctangent". + + The Request Authenticator is a 16 octet random number generated by + the NAS. + + The User-Password is 16 octets of password padded at end with nulls, + XORed with MD5(shared secret|Request Authenticator). + + 01 00 00 38 0f 40 3f 94 73 97 80 57 bd 83 d5 cb + 98 f4 22 7a 01 06 6e 65 6d 6f 02 12 0d be 70 8d + 93 d4 13 ce 31 96 e4 3f 78 2a 0a ee 04 06 c0 a8 + 01 10 05 06 00 00 00 03 + + 1 Code = Access-Request (1) + 1 ID = 0 + 2 Length = 56 + 16 Request Authenticator + + Attributes: + 6 User-Name = "nemo" + 18 User-Password + 6 NAS-IP-Address = 192.168.1.16 + 6 NAS-Port = 3 + + The RADIUS server authenticates nemo, and sends an Access-Accept UDP + packet to the NAS telling it to telnet nemo to host 192.168.1.3. + + The Response Authenticator is a 16-octet MD5 checksum of the code + (2), id (0), Length (38), the Request Authenticator from above, the + attributes in this reply, and the shared secret. + + + + + + + + + +Rigney, et al. Standards Track [Page 66] + +RFC 2865 RADIUS June 2000 + + + 02 00 00 26 86 fe 22 0e 76 24 ba 2a 10 05 f6 bf + 9b 55 e0 b2 06 06 00 00 00 01 0f 06 00 00 00 00 + 0e 06 c0 a8 01 03 + + 1 Code = Access-Accept (2) + 1 ID = 0 (same as in Access-Request) + 2 Length = 38 + 16 Response Authenticator + + Attributes: + 6 Service-Type (6) = Login (1) + 6 Login-Service (15) = Telnet (0) + 6 Login-IP-Host (14) = 192.168.1.3 + +7.2. Framed User Authenticating with CHAP + + The NAS at 192.168.1.16 sends an Access-Request UDP packet to the + RADIUS Server for a user named flopsy logging in on port 20 with PPP, + authenticating using CHAP. The NAS sends along the Service-Type and + Framed-Protocol attributes as a hint to the RADIUS server that this + user is looking for PPP, although the NAS is not required to do so. + + The Request Authenticator is a 16 octet random number generated by + the NAS, and is also used as the CHAP Challenge. + + The CHAP-Password consists of a 1 octet CHAP ID, in this case 22, + followed by the 16 octet CHAP response. + + 01 01 00 47 2a ee 86 f0 8d 0d 55 96 9c a5 97 8e + 0d 33 67 a2 01 08 66 6c 6f 70 73 79 03 13 16 e9 + 75 57 c3 16 18 58 95 f2 93 ff 63 44 07 72 75 04 + 06 c0 a8 01 10 05 06 00 00 00 14 06 06 00 00 00 + 02 07 06 00 00 00 01 + + 1 Code = 1 (Access-Request) + 1 ID = 1 + 2 Length = 71 + 16 Request Authenticator + + Attributes: + 8 User-Name (1) = "flopsy" + 19 CHAP-Password (3) + 6 NAS-IP-Address (4) = 192.168.1.16 + 6 NAS-Port (5) = 20 + 6 Service-Type (6) = Framed (2) + 6 Framed-Protocol (7) = PPP (1) + + + + + +Rigney, et al. Standards Track [Page 67] + +RFC 2865 RADIUS June 2000 + + + The RADIUS server authenticates flopsy, and sends an Access-Accept + UDP packet to the NAS telling it to start PPP service and assign an + address for the user out of its dynamic address pool. + + The Response Authenticator is a 16-octet MD5 checksum of the code + (2), id (1), Length (56), the Request Authenticator from above, the + attributes in this reply, and the shared secret. + + 02 01 00 38 15 ef bc 7d ab 26 cf a3 dc 34 d9 c0 + 3c 86 01 a4 06 06 00 00 00 02 07 06 00 00 00 01 + 08 06 ff ff ff fe 0a 06 00 00 00 02 0d 06 00 00 + 00 01 0c 06 00 00 05 dc + + 1 Code = Access-Accept (2) + 1 ID = 1 (same as in Access-Request) + 2 Length = 56 + 16 Response Authenticator + + Attributes: + 6 Service-Type (6) = Framed (2) + 6 Framed-Protocol (7) = PPP (1) + 6 Framed-IP-Address (8) = 255.255.255.254 + 6 Framed-Routing (10) = None (0) + 6 Framed-Compression (13) = VJ TCP/IP Header Compression (1) + 6 Framed-MTU (12) = 1500 + +7.3. User with Challenge-Response card + + The NAS at 192.168.1.16 sends an Access-Request UDP packet to the + RADIUS Server for a user named mopsy logging in on port 7. The user + enters the dummy password "challenge" in this example. The challenge + and response generated by the smart card for this example are + "32769430" and "99101462". + + The Request Authenticator is a 16 octet random number generated by + the NAS. + + The User-Password is 16 octets of password, in this case "challenge", + padded at the end with nulls, XORed with MD5(shared secret|Request + Authenticator). + + 01 02 00 39 f3 a4 7a 1f 6a 6d 76 71 0b 94 7a b9 + 30 41 a0 39 01 07 6d 6f 70 73 79 02 12 33 65 75 + 73 77 82 89 b5 70 88 5e 15 08 48 25 c5 04 06 c0 + a8 01 10 05 06 00 00 00 07 + + + + + + +Rigney, et al. Standards Track [Page 68] + +RFC 2865 RADIUS June 2000 + + + 1 Code = Access-Request (1) + 1 ID = 2 + 2 Length = 57 + 16 Request Authenticator + + Attributes: + 7 User-Name (1) = "mopsy" + 18 User-Password (2) + 6 NAS-IP-Address (4) = 192.168.1.16 + 6 NAS-Port (5) = 7 + + The RADIUS server decides to challenge mopsy, sending back a + challenge string and looking for a response. The RADIUS server + therefore and sends an Access-Challenge UDP packet to the NAS. + + The Response Authenticator is a 16-octet MD5 checksum of the code + (11), id (2), length (78), the Request Authenticator from above, the + attributes in this reply, and the shared secret. + + The Reply-Message is "Challenge 32769430. Enter response at prompt." + + The State is a magic cookie to be returned along with user's + response; in this example 8 octets of data (33 32 37 36 39 34 33 30 + in hex). + + 0b 02 00 4e 36 f3 c8 76 4a e8 c7 11 57 40 3c 0c + 71 ff 9c 45 12 30 43 68 61 6c 6c 65 6e 67 65 20 + 33 32 37 36 39 34 33 30 2e 20 20 45 6e 74 65 72 + 20 72 65 73 70 6f 6e 73 65 20 61 74 20 70 72 6f + 6d 70 74 2e 18 0a 33 32 37 36 39 34 33 30 + + 1 Code = Access-Challenge (11) + 1 ID = 2 (same as in Access-Request) + 2 Length = 78 + 16 Response Authenticator + + Attributes: + 48 Reply-Message (18) + 10 State (24) + + The user enters his response, and the NAS send a new Access-Request + with that response, and includes the State Attribute. + + The Request Authenticator is a new 16 octet random number. + + The User-Password is 16 octets of the user's response, in this case + "99101462", padded at the end with nulls, XORed with MD5(shared + secret|Request Authenticator). + + + +Rigney, et al. Standards Track [Page 69] + +RFC 2865 RADIUS June 2000 + + + The state is the magic cookie from the Access-Challenge packet, + unchanged. + + 01 03 00 43 b1 22 55 6d 42 8a 13 d0 d6 25 38 07 + c4 57 ec f0 01 07 6d 6f 70 73 79 02 12 69 2c 1f + 20 5f c0 81 b9 19 b9 51 95 f5 61 a5 81 04 06 c0 + a8 01 10 05 06 00 00 00 07 18 10 33 32 37 36 39 + 34 33 30 + + 1 Code = Access-Request (1) + 1 ID = 3 (Note that this changes.) + 2 Length = 67 + 16 Request Authenticator + + Attributes: + 7 User-Name = "mopsy" + 18 User-Password + 6 NAS-IP-Address (4) = 192.168.1.16 + 6 NAS-Port (5) = 7 + 10 State (24) + + The Response was incorrect (for the sake of example), so the RADIUS + server tells the NAS to reject the login attempt. + + The Response Authenticator is a 16 octet MD5 checksum of the code + (3), id (3), length(20), the Request Authenticator from above, the + attributes in this reply (in this case, none), and the shared secret. + + 03 03 00 14 a4 2f 4f ca 45 91 6c 4e 09 c8 34 0f + 9e 74 6a a0 + + 1 Code = Access-Reject (3) + 1 ID = 3 (same as in Access-Request) + 2 Length = 20 + 16 Response Authenticator + + Attributes: + (none, although a Reply-Message could be sent) + + + + + + + + + + + + + +Rigney, et al. Standards Track [Page 70] + +RFC 2865 RADIUS June 2000 + + +8. Security Considerations + + Security issues are the primary topic of this document. + + In practice, within or associated with each RADIUS server, there is a + database which associates "user" names with authentication + information ("secrets"). It is not anticipated that a particular + named user would be authenticated by multiple methods. This would + make the user vulnerable to attacks which negotiate the least secure + method from among a set. Instead, for each named user there should + be an indication of exactly one method used to authenticate that user + name. If a user needs to make use of different authentication + methods under different circumstances, then distinct user names + SHOULD be employed, each of which identifies exactly one + authentication method. + + Passwords and other secrets should be stored at the respective ends + such that access to them is as limited as possible. Ideally, the + secrets should only be accessible to the process requiring access in + order to perform the authentication. + + The secrets should be distributed with a mechanism that limits the + number of entities that handle (and thus gain knowledge of) the + secret. Ideally, no unauthorized person should ever gain knowledge + of the secrets. It is possible to achieve this with SNMP Security + Protocols [14], but such a mechanism is outside the scope of this + specification. + + Other distribution methods are currently undergoing research and + experimentation. The SNMP Security document [14] also has an + excellent overview of threats to network protocols. + + The User-Password hiding mechanism described in Section 5.2 has not + been subjected to significant amounts of cryptanalysis in the + published literature. Some in the IETF community are concerned that + this method might not provide sufficient confidentiality protection + [15] to passwords transmitted using RADIUS. Users should evaluate + their threat environment and consider whether additional security + mechanisms should be employed. + +9. Change Log + + The following changes have been made from RFC 2138: + + Strings should use UTF-8 instead of US-ASCII and should be handled as + 8-bit data. + + Integers and dates are now defined as 32 bit unsigned values. + + + +Rigney, et al. Standards Track [Page 71] + +RFC 2865 RADIUS June 2000 + + + Updated list of attributes that can be included in Access-Challenge + to be consistent with the table of attributes. + + User-Name mentions Network Access Identifiers. + + User-Name may now be sent in Access-Accept for use with accounting + and Rlogin. + + Values added for Service-Type, Login-Service, Framed-Protocol, + Framed-Compression, and NAS-Port-Type. + + NAS-Port can now use all 32 bits. + + Examples now include hexadecimal displays of the packets. + + Source UDP port must be used in conjunction with the Request + Identifier when identifying duplicates. + + Multiple subattributes may be allowed in a Vendor-Specific attribute. + + An Access-Request is now required to contain either a NAS-IP-Address + or NAS-Identifier (or may contain both). + + Added notes under "Operations" with more information on proxy, + retransmissions, and keep-alives. + + If multiple Attributes with the same Type are present, the order of + Attributes with the same Type MUST be preserved by any proxies. + + Clarified Proxy-State. + + Clarified that Attributes must not depend on position within the + packet, as long as Attributes of the same type are kept in order. + + Added IANA Considerations section. + + Updated section on "Proxy" under "Operations". + + Framed-MTU can now be sent in Access-Request as a hint. + + Updated Security Considerations. + + Text strings identified as a subset of string, to clarify use of + UTF-8. + + + + + + + +Rigney, et al. Standards Track [Page 72] + +RFC 2865 RADIUS June 2000 + + +10. References + + [1] Rigney, C., Rubens, A., Simpson, W. and S. Willens, "Remote + Authentication Dial In User Service (RADIUS)", RFC 2138, April + 1997. + + [2] Bradner, S., "Key words for use in RFCs to Indicate Requirement + Levels", BCP 14, RFC 2119, March, 1997. + + [3] Rivest, R. and S. Dusse, "The MD5 Message-Digest Algorithm", + RFC 1321, April 1992. + + [4] Postel, J., "User Datagram Protocol", STD 6, RFC 768, August + 1980. + + [5] Rigney, C., "RADIUS Accounting", RFC 2866, June 2000. + + [6] Reynolds, J. and J. Postel, "Assigned Numbers", STD 2, RFC + 1700, October 1994. + + [7] Yergeau, F., "UTF-8, a transformation format of ISO 10646", RFC + 2279, January 1998. + + [8] Aboba, B. and M. Beadles, "The Network Access Identifier", RFC + 2486, January 1999. + + [9] Kaufman, C., Perlman, R., and Speciner, M., "Network Security: + Private Communications in a Public World", Prentice Hall, March + 1995, ISBN 0-13-061466-1. + + [10] Jacobson, V., "Compressing TCP/IP headers for low-speed serial + links", RFC 1144, February 1990. + + [11] ISO 8859. International Standard -- Information Processing -- + 8-bit Single-Byte Coded Graphic Character Sets -- Part 1: Latin + Alphabet No. 1, ISO 8859-1:1987. + + [12] Sklower, K., Lloyd, B., McGregor, G., Carr, D. and T. + Coradetti, "The PPP Multilink Protocol (MP)", RFC 1990, August + 1996. + + [13] Alvestrand, H. and T. Narten, "Guidelines for Writing an IANA + Considerations Section in RFCs", BCP 26, RFC 2434, October + 1998. + + [14] Galvin, J., McCloghrie, K. and J. Davin, "SNMP Security + Protocols", RFC 1352, July 1992. + + + + +Rigney, et al. Standards Track [Page 73] + +RFC 2865 RADIUS June 2000 + + + [15] Dobbertin, H., "The Status of MD5 After a Recent Attack", + CryptoBytes Vol.2 No.2, Summer 1996. + +11. Acknowledgements + + RADIUS was originally developed by Steve Willens of Livingston + Enterprises for their PortMaster series of Network Access Servers. + +12. Chair's Address + + The working group can be contacted via the current chair: + + Carl Rigney + Livingston Enterprises + 4464 Willow Road + Pleasanton, California 94588 + + Phone: +1 925 737 2100 + EMail: cdr@telemancy.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Rigney, et al. Standards Track [Page 74] + +RFC 2865 RADIUS June 2000 + + +13. Authors' Addresses + + Questions about this memo can also be directed to: + + Carl Rigney + Livingston Enterprises + 4464 Willow Road + Pleasanton, California 94588 + + Phone: +1 925 737 2100 + EMail: cdr@telemancy.com + + + Allan C. Rubens + Merit Network, Inc. + 4251 Plymouth Road + Ann Arbor, Michigan 48105-2785 + + EMail: acr@merit.edu + + + William Allen Simpson + Daydreamer + Computer Systems Consulting Services + 1384 Fontaine + Madison Heights, Michigan 48071 + + EMail: wsimpson@greendragon.com + + + Steve Willens + Livingston Enterprises + 4464 Willow Road + Pleasanton, California 94588 + + EMail: steve@livingston.com + + + + + + + + + + + + + + + +Rigney, et al. Standards Track [Page 75] + +RFC 2865 RADIUS June 2000 + + +14. Full Copyright Statement + + Copyright (C) The Internet Society (2000). All Rights Reserved. + + This document and translations of it may be copied and furnished to + others, and derivative works that comment on or otherwise explain it + or assist in its implementation may be prepared, copied, published + and distributed, in whole or in part, without restriction of any + kind, provided that the above copyright notice and this paragraph are + included on all such copies and derivative works. However, this + document itself may not be modified in any way, such as by removing + the copyright notice or references to the Internet Society or other + Internet organizations, except as needed for the purpose of + developing Internet standards in which case the procedures for + copyrights defined in the Internet Standards process must be + followed, or as required to translate it into languages other than + English. + + The limited permissions granted above are perpetual and will not be + revoked by the Internet Society or its successors or assigns. + + This document and the information contained herein is provided on an + "AS IS" basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING + TASK FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING + BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION + HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +Acknowledgement + + Funding for the RFC Editor function is currently provided by the + Internet Society. + + + + + + + + + + + + + + + + + + + +Rigney, et al. Standards Track [Page 76] + diff --git a/spec/rfc2866.txt b/spec/rfc2866.txt new file mode 100644 index 0000000..82da1dc --- /dev/null +++ b/spec/rfc2866.txt @@ -0,0 +1,1571 @@ + + + + + + +Network Working Group C. Rigney +Request for Comments: 2866 Livingston +Category: Informational June 2000 +Obsoletes: 2139 + + + RADIUS Accounting + +Status of this Memo + + This memo provides information for the Internet community. It does + not specify an Internet standard of any kind. Distribution of this + memo is unlimited. + +Copyright Notice + + Copyright (C) The Internet Society (2000). All Rights Reserved. + +Abstract + + This document describes a protocol for carrying accounting + information between a Network Access Server and a shared Accounting + Server. + +Implementation Note + + This memo documents the RADIUS Accounting protocol. The early + deployment of RADIUS Accounting was done using UDP port number 1646, + which conflicts with the "sa-msg-port" service. The officially + assigned port number for RADIUS Accounting is 1813. + +Table of Contents + + 1. Introduction .................................... 2 + 1.1 Specification of Requirements ................. 3 + 1.2 Terminology ................................... 3 + 2. Operation ....................................... 4 + 2.1 Proxy ......................................... 4 + 3. Packet Format ................................... 5 + 4. Packet Types ................................... 7 + 4.1 Accounting-Request ............................ 8 + 4.2 Accounting-Response ........................... 9 + 5. Attributes ...................................... 10 + 5.1 Acct-Status-Type .............................. 12 + 5.2 Acct-Delay-Time ............................... 13 + 5.3 Acct-Input-Octets ............................. 14 + 5.4 Acct-Output-Octets ............................ 15 + 5.5 Acct-Session-Id ............................... 15 + + + +Rigney Informational [Page 1] + +RFC 2866 RADIUS Accounting June 2000 + + + 5.6 Acct-Authentic ................................ 16 + 5.7 Acct-Session-Time ............................. 17 + 5.8 Acct-Input-Packets ............................ 18 + 5.9 Acct-Output-Packets ........................... 18 + 5.10 Acct-Terminate-Cause .......................... 19 + 5.11 Acct-Multi-Session-Id ......................... 21 + 5.12 Acct-Link-Count ............................... 22 + 5.13 Table of Attributes ........................... 23 + 6. IANA Considerations ............................. 25 + 7. Security Considerations ......................... 25 + 8. Change Log ...................................... 25 + 9. References ...................................... 26 + 10. Acknowledgements ................................ 26 + 11. Chair's Address ................................. 26 + 12. Author's Address ................................ 27 + 13. Full Copyright Statement ........................ 28 + +1. Introduction + + Managing dispersed serial line and modem pools for large numbers of + users can create the need for significant administrative support. + Since modem pools are by definition a link to the outside world, they + require careful attention to security, authorization and accounting. + This can be best achieved by managing a single "database" of users, + which allows for authentication (verifying user name and password) as + well as configuration information detailing the type of service to + deliver to the user (for example, SLIP, PPP, telnet, rlogin). + + The RADIUS (Remote Authentication Dial In User Service) document [2] + specifies the RADIUS protocol used for Authentication and + Authorization. This memo extends the use of the RADIUS protocol to + cover delivery of accounting information from the Network Access + Server (NAS) to a RADIUS accounting server. + + This document obsoletes RFC 2139 [1]. A summary of the changes + between this document and RFC 2139 is available in the "Change Log" + appendix. + + Key features of RADIUS Accounting are: + + Client/Server Model + + A Network Access Server (NAS) operates as a client of the + RADIUS accounting server. The client is responsible for + passing user accounting information to a designated RADIUS + accounting server. + + + + + +Rigney Informational [Page 2] + +RFC 2866 RADIUS Accounting June 2000 + + + The RADIUS accounting server is responsible for receiving the + accounting request and returning a response to the client + indicating that it has successfully received the request. + + The RADIUS accounting server can act as a proxy client to + other kinds of accounting servers. + + Network Security + + Transactions between the client and RADIUS accounting server + are authenticated through the use of a shared secret, which is + never sent over the network. + + Extensible Protocol + + All transactions are comprised of variable length Attribute- + Length-Value 3-tuples. New attribute values can be added + without disturbing existing implementations of the protocol. + +1.1. Specification of Requirements + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this + document are to be interpreted as described in RFC 2119 [3]. These + key words mean the same thing whether capitalized or not. + +1.2. Terminology + + This document uses the following terms: + + service The NAS provides a service to the dial-in user, such as PPP + or Telnet. + + session Each service provided by the NAS to a dial-in user + constitutes a session, with the beginning of the session + defined as the point where service is first provided and + the end of the session defined as the point where service + is ended. A user may have multiple sessions in parallel or + series if the NAS supports that, with each session + generating a separate start and stop accounting record with + its own Acct-Session-Id. + + silently discard + This means the implementation discards the packet without + further processing. The implementation SHOULD provide the + capability of logging the error, including the contents of + the silently discarded packet, and SHOULD record the event + in a statistics counter. + + + +Rigney Informational [Page 3] + +RFC 2866 RADIUS Accounting June 2000 + + +2. Operation + + When a client is configured to use RADIUS Accounting, at the start of + service delivery it will generate an Accounting Start packet + describing the type of service being delivered and the user it is + being delivered to, and will send that to the RADIUS Accounting + server, which will send back an acknowledgement that the packet has + been received. At the end of service delivery the client will + generate an Accounting Stop packet describing the type of service + that was delivered and optionally statistics such as elapsed time, + input and output octets, or input and output packets. It will send + that to the RADIUS Accounting server, which will send back an + acknowledgement that the packet has been received. + + The Accounting-Request (whether for Start or Stop) is submitted to + the RADIUS accounting server via the network. It is recommended that + the client continue attempting to send the Accounting-Request packet + until it receives an acknowledgement, using some form of backoff. If + no response is returned within a length of time, the request is re- + sent a number of times. The client can also forward requests to an + alternate server or servers in the event that the primary server is + down or unreachable. An alternate server can be used either after a + number of tries to the primary server fail, or in a round-robin + fashion. Retry and fallback algorithms are the topic of current + research and are not specified in detail in this document. + + The RADIUS accounting server MAY make requests of other servers in + order to satisfy the request, in which case it acts as a client. + + If the RADIUS accounting server is unable to successfully record the + accounting packet it MUST NOT send an Accounting-Response + acknowledgment to the client. + +2.1. Proxy + + See the "RADIUS" RFC [2] for information on Proxy RADIUS. Proxy + Accounting RADIUS works the same way, as illustrated by the following + example. + + 1. The NAS sends an accounting-request to the forwarding server. + + 2. The forwarding server logs the accounting-request (if desired), + adds its Proxy-State (if desired) after any other Proxy-State + attributes, updates the Request Authenticator, and forwards the + request to the remote server. + + + + + + +Rigney Informational [Page 4] + +RFC 2866 RADIUS Accounting June 2000 + + + 3. The remote server logs the accounting-request (if desired), + copies all Proxy-State attributes in order and unmodified from + the request to the response packet, and sends the accounting- + response to the forwarding server. + + 4. The forwarding server strips the last Proxy-State (if it added + one in step 2), updates the Response Authenticator and sends + the accounting-response to the NAS. + + A forwarding server MUST not modify existing Proxy-State or Class + attributes present in the packet. + + A forwarding server may either perform its forwarding function in a + pass through manner, where it sends retransmissions on as soon as it + gets them, or it may take responsibility for retransmissions, for + example in cases where the network link between forwarding and remote + server has very different characteristics than the link between NAS + and forwarding server. + + Extreme care should be used when implementing a proxy server that + takes responsibility for retransmissions so that its retransmission + policy is robust and scalable. + +3. Packet Format + + Exactly one RADIUS Accounting packet is encapsulated in the UDP Data + field [4], where the UDP Destination Port field indicates 1813 + (decimal). + + When a reply is generated, the source and destination ports are + reversed. + + This memo documents the RADIUS Accounting protocol. The early + deployment of RADIUS Accounting was done using UDP port number 1646, + which conflicts with the "sa-msg-port" service. The officially + assigned port number for RADIUS Accounting is 1813. + + A summary of the RADIUS data format is shown below. The fields are + transmitted from left to right. + + + + + + + + + + + + +Rigney Informational [Page 5] + +RFC 2866 RADIUS Accounting June 2000 + + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Code | Identifier | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + | Authenticator | + | | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Attributes ... + +-+-+-+-+-+-+-+-+-+-+-+-+- + + + Code + + The Code field is one octet, and identifies the type of RADIUS + packet. When a packet is received with an invalid Code field, it + is silently discarded. + + RADIUS Accounting Codes (decimal) are assigned as follows: + + 4 Accounting-Request + 5 Accounting-Response + + Identifier + + The Identifier field is one octet, and aids in matching requests + and replies. The RADIUS server can detect a duplicate request if + it has the same client source IP address and source UDP port and + Identifier within a short span of time. + + Length + + The Length field is two octets. It indicates the length of the + packet including the Code, Identifier, Length, Authenticator and + Attribute fields. Octets outside the range of the Length field + MUST be treated as padding and ignored on reception. If the + packet is shorter than the Length field indicates, it MUST be + silently discarded. The minimum length is 20 and maximum length + is 4095. + + Authenticator + + The Authenticator field is sixteen (16) octets. The most + significant octet is transmitted first. This value is used to + authenticate the messages between the client and RADIUS accounting + server. + + + +Rigney Informational [Page 6] + +RFC 2866 RADIUS Accounting June 2000 + + + Request Authenticator + + In Accounting-Request Packets, the Authenticator value is a 16 + octet MD5 [5] checksum, called the Request Authenticator. + + The NAS and RADIUS accounting server share a secret. The Request + Authenticator field in Accounting-Request packets contains a one- + way MD5 hash calculated over a stream of octets consisting of the + Code + Identifier + Length + 16 zero octets + request attributes + + shared secret (where + indicates concatenation). The 16 octet MD5 + hash value is stored in the Authenticator field of the + Accounting-Request packet. + + Note that the Request Authenticator of an Accounting-Request can + not be done the same way as the Request Authenticator of a RADIUS + Access-Request, because there is no User-Password attribute in an + Accounting-Request. + + Response Authenticator + + The Authenticator field in an Accounting-Response packet is called + the Response Authenticator, and contains a one-way MD5 hash + calculated over a stream of octets consisting of the Accounting- + Response Code, Identifier, Length, the Request Authenticator field + from the Accounting-Request packet being replied to, and the + response attributes if any, followed by the shared secret. The + resulting 16 octet MD5 hash value is stored in the Authenticator + field of the Accounting-Response packet. + + Attributes + + Attributes may have multiple instances, in such a case the order + of attributes of the same type SHOULD be preserved. The order of + attributes of different types is not required to be preserved. + +4. Packet Types + + The RADIUS packet type is determined by the Code field in the first + octet of the packet. + + + + + + + + + + + + +Rigney Informational [Page 7] + +RFC 2866 RADIUS Accounting June 2000 + + +4.1. Accounting-Request + + Description + + Accounting-Request packets are sent from a client (typically a + Network Access Server or its proxy) to a RADIUS accounting server, + and convey information used to provide accounting for a service + provided to a user. The client transmits a RADIUS packet with the + Code field set to 4 (Accounting-Request). + + Upon receipt of an Accounting-Request, the server MUST transmit an + Accounting-Response reply if it successfully records the + accounting packet, and MUST NOT transmit any reply if it fails to + record the accounting packet. + + Any attribute valid in a RADIUS Access-Request or Access-Accept + packet is valid in a RADIUS Accounting-Request packet, except that + the following attributes MUST NOT be present in an Accounting- + Request: User-Password, CHAP-Password, Reply-Message, State. + Either NAS-IP-Address or NAS-Identifier MUST be present in a + RADIUS Accounting-Request. It SHOULD contain a NAS-Port or NAS- + Port-Type attribute or both unless the service does not involve a + port or the NAS does not distinguish among its ports. + + If the Accounting-Request packet includes a Framed-IP-Address, + that attribute MUST contain the IP address of the user. If the + Access-Accept used the special values for Framed-IP-Address + telling the NAS to assign or negotiate an IP address for the user, + the Framed-IP-Address (if any) in the Accounting-Request MUST + contain the actual IP address assigned or negotiated. + + A summary of the Accounting-Request packet format is shown below. + + The fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Code | Identifier | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + | Request Authenticator | + | | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Attributes ... + +-+-+-+-+-+-+-+-+-+-+-+-+- + + + + +Rigney Informational [Page 8] + +RFC 2866 RADIUS Accounting June 2000 + + + Code + + 4 for Accounting-Request. + + Identifier + + The Identifier field MUST be changed whenever the content of the + Attributes field changes, and whenever a valid reply has been + received for a previous request. For retransmissions where the + contents are identical, the Identifier MUST remain unchanged. + + Note that if Acct-Delay-Time is included in the attributes of an + Accounting-Request then the Acct-Delay-Time value will be updated + when the packet is retransmitted, changing the content of the + Attributes field and requiring a new Identifier and Request + Authenticator. + + Request Authenticator + + The Request Authenticator of an Accounting-Request contains a 16-octet + MD5 hash value calculated according to the method described in + "Request Authenticator" above. + + Attributes + + The Attributes field is variable in length, and contains a list of + Attributes. + +4.2. Accounting-Response + + Description + + Accounting-Response packets are sent by the RADIUS accounting + server to the client to acknowledge that the Accounting-Request + has been received and recorded successfully. If the Accounting- + Request was recorded successfully then the RADIUS accounting + server MUST transmit a packet with the Code field set to 5 + (Accounting-Response). On reception of an Accounting-Response by + the client, the Identifier field is matched with a pending + Accounting-Request. The Response Authenticator field MUST contain + the correct response for the pending Accounting-Request. Invalid + packets are silently discarded. + + A RADIUS Accounting-Response is not required to have any + attributes in it. + + A summary of the Accounting-Response packet format is shown below. + The fields are transmitted from left to right. + + + +Rigney Informational [Page 9] + +RFC 2866 RADIUS Accounting June 2000 + + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Code | Identifier | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + | Response Authenticator | + | | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Attributes ... + +-+-+-+-+-+-+-+-+-+-+-+-+- + + Code + + 5 for Accounting-Response. + + Identifier + + The Identifier field is a copy of the Identifier field of the + Accounting-Request which caused this Accounting-Response. + + Response Authenticator + + The Response Authenticator of an Accounting-Response contains a + 16-octet MD5 hash value calculated according to the method + described in "Response Authenticator" above. + + Attributes + + The Attributes field is variable in length, and contains a list of + zero or more Attributes. + +5. Attributes + + RADIUS Attributes carry the specific authentication, authorization + and accounting details for the request and response. + + Some attributes MAY be included more than once. The effect of this + is attribute specific, and is specified in each attribute + description. + + The end of the list of attributes is indicated by the Length of the + RADIUS packet. + + A summary of the attribute format is shown below. The fields are + transmitted from left to right. + + + + +Rigney Informational [Page 10] + +RFC 2866 RADIUS Accounting June 2000 + + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + + Type + + The Type field is one octet. Up-to-date values of the RADIUS Type + field are specified in the most recent "Assigned Numbers" RFC [6]. + Values 192-223 are reserved for experimental use, values 224-240 + are reserved for implementation-specific use, and values 241-255 + are reserved and should not be used. This specification concerns + the following values: + + 1-39 (refer to RADIUS document [2]) + 40 Acct-Status-Type + 41 Acct-Delay-Time + 42 Acct-Input-Octets + 43 Acct-Output-Octets + 44 Acct-Session-Id + 45 Acct-Authentic + 46 Acct-Session-Time + 47 Acct-Input-Packets + 48 Acct-Output-Packets + 49 Acct-Terminate-Cause + 50 Acct-Multi-Session-Id + 51 Acct-Link-Count + 60+ (refer to RADIUS document [2]) + + Length + + The Length field is one octet, and indicates the length of this + attribute including the Type, Length and Value fields. If an + attribute is received in an Accounting-Request with an invalid + Length, the entire request MUST be silently discarded. + + Value + + The Value field is zero or more octets and contains information + specific to the attribute. The format and length of the Value + field is determined by the Type and Length fields. + + Note that none of the types in RADIUS terminate with a NUL (hex + 00). In particular, types "text" and "string" in RADIUS do not + terminate with a NUL (hex 00). The Attribute has a length field + and does not use a terminator. Text contains UTF-8 encoded 10646 + + + +Rigney Informational [Page 11] + +RFC 2866 RADIUS Accounting June 2000 + + + [7] characters and String contains 8-bit binary data. Servers and + servers and clients MUST be able to deal with embedded nulls. + RADIUS implementers using C are cautioned not to use strcpy() when + handling strings. + + The format of the value field is one of five data types. Note + that type "text" is a subset of type "string." + + text 1-253 octets containing UTF-8 encoded 10646 [7] + characters. Text of length zero (0) MUST NOT be sent; + omit the entire attribute instead. + + string 1-253 octets containing binary data (values 0 through 255 + decimal, inclusive). Strings of length zero (0) MUST NOT + be sent; omit the entire attribute instead. + + address 32 bit value, most significant octet first. + + integer 32 bit unsigned value, most significant octet first. + + time 32 bit unsigned value, most significant octet first -- + seconds since 00:00:00 UTC, January 1, 1970. The + standard Attributes do not use this data type but it is + presented here for possible use in future attributes. + +5.1. Acct-Status-Type + + Description + + This attribute indicates whether this Accounting-Request marks the + beginning of the user service (Start) or the end (Stop). + + It MAY be used by the client to mark the start of accounting (for + example, upon booting) by specifying Accounting-On and to mark the + end of accounting (for example, just before a scheduled reboot) by + specifying Accounting-Off. + + A summary of the Acct-Status-Type attribute format is shown below. + The fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + + + +Rigney Informational [Page 12] + +RFC 2866 RADIUS Accounting June 2000 + + + Type + + 40 for Acct-Status-Type. + + Length + + 6 + + Value + + The Value field is four octets. + + 1 Start + 2 Stop + 3 Interim-Update + 7 Accounting-On + 8 Accounting-Off + 9-14 Reserved for Tunnel Accounting + 15 Reserved for Failed + +5.2. Acct-Delay-Time + + Description + + This attribute indicates how many seconds the client has been + trying to send this record for, and can be subtracted from the + time of arrival on the server to find the approximate time of the + event generating this Accounting-Request. (Network transit time + is ignored.) + + Note that changing the Acct-Delay-Time causes the Identifier to + change; see the discussion under Identifier above. + + A summary of the Acct-Delay-Time attribute format is shown below. + The fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + + + + + + + +Rigney Informational [Page 13] + +RFC 2866 RADIUS Accounting June 2000 + + + Type + + 41 for Acct-Delay-Time. + + Length + + 6 + + Value + + The Value field is four octets. + +5.3. Acct-Input-Octets + + Description + + This attribute indicates how many octets have been received from + the port over the course of this service being provided, and can + only be present in Accounting-Request records where the Acct- + Status-Type is set to Stop. + + A summary of the Acct-Input-Octets attribute format is shown below. + The fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 42 for Acct-Input-Octets. + + Length + + 6 + + Value + + The Value field is four octets. + + + + + + + + +Rigney Informational [Page 14] + +RFC 2866 RADIUS Accounting June 2000 + + +5.4. Acct-Output-Octets + + Description + + This attribute indicates how many octets have been sent to the + port in the course of delivering this service, and can only be + present in Accounting-Request records where the Acct-Status-Type + is set to Stop. + + A summary of the Acct-Output-Octets attribute format is shown below. + The fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 43 for Acct-Output-Octets. + + Length + + 6 + + Value + + The Value field is four octets. + +5.5. Acct-Session-Id + + Description + + This attribute is a unique Accounting ID to make it easy to match + start and stop records in a log file. The start and stop records + for a given session MUST have the same Acct-Session-Id. An + Accounting-Request packet MUST have an Acct-Session-Id. An + Access-Request packet MAY have an Acct-Session-Id; if it does, + then the NAS MUST use the same Acct-Session-Id in the Accounting- + Request packets for that session. + + The Acct-Session-Id SHOULD contain UTF-8 encoded 10646 [7] + characters. + + + + + +Rigney Informational [Page 15] + +RFC 2866 RADIUS Accounting June 2000 + + + For example, one implementation uses a string with an 8-digit + upper case hexadecimal number, the first two digits increment on + each reboot (wrapping every 256 reboots) and the next 6 digits + counting from 0 for the first person logging in after a reboot up + to 2^24-1, about 16 million. Other encodings are possible. + + A summary of the Acct-Session-Id attribute format is shown below. + The fields are transmitted from left to right. + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Text ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 44 for Acct-Session-Id. + + Length + + >= 3 + + String + + The String field SHOULD be a string of UTF-8 encoded 10646 [7] + characters. + +5.6. Acct-Authentic + + Description + + This attribute MAY be included in an Accounting-Request to + indicate how the user was authenticated, whether by RADIUS, the + NAS itself, or another remote authentication protocol. Users who + are delivered service without being authenticated SHOULD NOT + generate Accounting records. + + A summary of the Acct-Authentic attribute format is shown below. The + fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + + +Rigney Informational [Page 16] + +RFC 2866 RADIUS Accounting June 2000 + + + Type + + 45 for Acct-Authentic. + + Length + + 6 + + Value + + The Value field is four octets. + + 1 RADIUS + 2 Local + 3 Remote + +5.7. Acct-Session-Time + + Description + + This attribute indicates how many seconds the user has received + service for, and can only be present in Accounting-Request records + where the Acct-Status-Type is set to Stop. + + A summary of the Acct-Session-Time attribute format is shown below. + The fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 46 for Acct-Session-Time. + + Length + + 6 + + Value + + The Value field is four octets. + + + + + +Rigney Informational [Page 17] + +RFC 2866 RADIUS Accounting June 2000 + + +5.8. Acct-Input-Packets + + Description + + This attribute indicates how many packets have been received from + the port over the course of this service being provided to a + Framed User, and can only be present in Accounting-Request records + where the Acct-Status-Type is set to Stop. + + A summary of the Acct-Input-packets attribute format is shown below. + The fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 47 for Acct-Input-Packets. + + Length + + 6 + + Value + + The Value field is four octets. + +5.9. Acct-Output-Packets + + Description + + This attribute indicates how many packets have been sent to the + port in the course of delivering this service to a Framed User, + and can only be present in Accounting-Request records where the + Acct-Status-Type is set to Stop. + + A summary of the Acct-Output-Packets attribute format is shown below. + The fields are transmitted from left to right. + + + + + + + + +Rigney Informational [Page 18] + +RFC 2866 RADIUS Accounting June 2000 + + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 48 for Acct-Output-Packets. + + Length + + 6 + + Value + + The Value field is four octets. + +5.10. Acct-Terminate-Cause + + Description + + This attribute indicates how the session was terminated, and can + only be present in Accounting-Request records where the Acct- + Status-Type is set to Stop. + + A summary of the Acct-Terminate-Cause attribute format is shown + below. The fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + + + + + + + + + + + + +Rigney Informational [Page 19] + +RFC 2866 RADIUS Accounting June 2000 + + + Type + + 49 for Acct-Terminate-Cause + + Length + + 6 + + Value + + The Value field is four octets, containing an integer specifying + the cause of session termination, as follows: + + 1 User Request + 2 Lost Carrier + 3 Lost Service + 4 Idle Timeout + 5 Session Timeout + 6 Admin Reset + 7 Admin Reboot + 8 Port Error + 9 NAS Error + 10 NAS Request + 11 NAS Reboot + 12 Port Unneeded + 13 Port Preempted + 14 Port Suspended + 15 Service Unavailable + 16 Callback + 17 User Error + 18 Host Request + + The termination causes are as follows: + + User Request User requested termination of service, for + example with LCP Terminate or by logging out. + + Lost Carrier DCD was dropped on the port. + + Lost Service Service can no longer be provided; for + example, user's connection to a host was + interrupted. + + Idle Timeout Idle timer expired. + + Session Timeout Maximum session length timer expired. + + Admin Reset Administrator reset the port or session. + + + +Rigney Informational [Page 20] + +RFC 2866 RADIUS Accounting June 2000 + + + Admin Reboot Administrator is ending service on the NAS, + for example prior to rebooting the NAS. + + Port Error NAS detected an error on the port which + required ending the session. + + NAS Error NAS detected some error (other than on the + port) which required ending the session. + + NAS Request NAS ended session for a non-error reason not + otherwise listed here. + + NAS Reboot The NAS ended the session in order to reboot + non-administratively ("crash"). + + Port Unneeded NAS ended session because resource usage fell + below low-water mark (for example, if a + bandwidth-on-demand algorithm decided that + the port was no longer needed). + + Port Preempted NAS ended session in order to allocate the + port to a higher priority use. + + Port Suspended NAS ended session to suspend a virtual + session. + + Service Unavailable NAS was unable to provide requested service. + + Callback NAS is terminating current session in order + to perform callback for a new session. + + User Error Input from user is in error, causing + termination of session. + + Host Request Login Host terminated session normally. + +5.11. Acct-Multi-Session-Id + + Description + + This attribute is a unique Accounting ID to make it easy to link + together multiple related sessions in a log file. Each session + linked together would have a unique Acct-Session-Id but the same + Acct-Multi-Session-Id. It is strongly recommended that the Acct- + Multi-Session-Id contain UTF-8 encoded 10646 [7] characters. + + A summary of the Acct-Session-Id attribute format is shown below. + The fields are transmitted from left to right. + + + +Rigney Informational [Page 21] + +RFC 2866 RADIUS Accounting June 2000 + + + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | String ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Type + + 50 for Acct-Multi-Session-Id. + + Length + + >= 3 + + String + + The String field SHOULD contain UTF-8 encoded 10646 [7] characters. + +5.12. Acct-Link-Count + + Description + + This attribute gives the count of links which are known to have been + in a given multilink session at the time the accounting record is + generated. The NAS MAY include the Acct-Link-Count attribute in any + Accounting-Request which might have multiple links. + + A summary of the Acct-Link-Count attribute format is show below. The + fields are transmitted from left to right. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + Value (cont) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + + + + + + + + + + + + + +Rigney Informational [Page 22] + +RFC 2866 RADIUS Accounting June 2000 + + + Type + + 51 for Acct-Link-Count. + + Length + + 6 + + Value + + The Value field is four octets, and contains the number of links + seen so far in this Multilink Session. + + It may be used to make it easier for an accounting server to know + when it has all the records for a given Multilink session. When + the number of Accounting-Requests received with Acct-Status-Type = + Stop and the same Acct-Multi-Session-Id and unique Acct-Session- + Id's equals the largest value of Acct-Link-Count seen in those + Accounting-Requests, all Stop Accounting-Requests for that + Multilink Session have been received. + + An example showing 8 Accounting-Requests should make things + clearer. For clarity only the relevant attributes are shown, but + additional attributes containing accounting information will also + be present in the Accounting-Request. + + Multi-Session-Id Session-Id Status-Type Link-Count + "10" "10" Start 1 + "10" "11" Start 2 + "10" "11" Stop 2 + "10" "12" Start 3 + "10" "13" Start 4 + "10" "12" Stop 4 + "10" "13" Stop 4 + "10" "10" Stop 4 + +5.13. Table of Attributes + + The following table provides a guide to which attributes may be found + in Accounting-Request packets. No attributes should be found in + Accounting-Response packets except Proxy-State and possibly Vendor- + Specific. + + + # Attribute + 0-1 User-Name + 0 User-Password + 0 CHAP-Password + + + +Rigney Informational [Page 23] + +RFC 2866 RADIUS Accounting June 2000 + + + 0-1 NAS-IP-Address [Note 1] + 0-1 NAS-Port + 0-1 Service-Type + 0-1 Framed-Protocol + 0-1 Framed-IP-Address + 0-1 Framed-IP-Netmask + 0-1 Framed-Routing + 0+ Filter-Id + 0-1 Framed-MTU + 0+ Framed-Compression + 0+ Login-IP-Host + 0-1 Login-Service + 0-1 Login-TCP-Port + 0 Reply-Message + 0-1 Callback-Number + 0-1 Callback-Id + 0+ Framed-Route + 0-1 Framed-IPX-Network + 0 State + 0+ Class + 0+ Vendor-Specific + 0-1 Session-Timeout + 0-1 Idle-Timeout + 0-1 Termination-Action + 0-1 Called-Station-Id + 0-1 Calling-Station-Id + 0-1 NAS-Identifier [Note 1] + 0+ Proxy-State + 0-1 Login-LAT-Service + 0-1 Login-LAT-Node + 0-1 Login-LAT-Group + 0-1 Framed-AppleTalk-Link + 0-1 Framed-AppleTalk-Network + 0-1 Framed-AppleTalk-Zone + 1 Acct-Status-Type + 0-1 Acct-Delay-Time + 0-1 Acct-Input-Octets + 0-1 Acct-Output-Octets + 1 Acct-Session-Id + 0-1 Acct-Authentic + 0-1 Acct-Session-Time + 0-1 Acct-Input-Packets + 0-1 Acct-Output-Packets + 0-1 Acct-Terminate-Cause + 0+ Acct-Multi-Session-Id + 0+ Acct-Link-Count + 0 CHAP-Challenge + + + + +Rigney Informational [Page 24] + +RFC 2866 RADIUS Accounting June 2000 + + + 0-1 NAS-Port-Type + 0-1 Port-Limit + 0-1 Login-LAT-Port + + [Note 1] An Accounting-Request MUST contain either a NAS-IP-Address + or a NAS-Identifier (or both). + + The following table defines the above table entries. + + 0 This attribute MUST NOT be present + 0+ Zero or more instances of this attribute MAY be present. + 0-1 Zero or one instance of this attribute MAY be present. + 1 Exactly one instance of this attribute MUST be present. + +6. IANA Considerations + + The Packet Type Codes, Attribute Types, and Attribute Values defined + in this document are registered by the Internet Assigned Numbers + Authority (IANA) from the RADIUS name spaces as described in the + "IANA Considerations" section of RFC 2865 [2], in accordance with BCP + 26 [8]. + +7. Security Considerations + + Security issues are discussed in sections concerning the + authenticator included in accounting requests and responses, using a + shared secret which is never sent over the network. + +8. Change Log + + US-ASCII replaced by UTF-8. + + Added notes on Proxy. + + Framed-IP-Address should contain the actual IP address of the user. + + If Acct-Session-ID was sent in an access-request, it must be used in + the accounting-request for that session. + + New values added to Acct-Status-Type. + + Added an IANA Considerations section. + + Updated references. + + Text strings identified as a subset of string, to clarify use of + UTF-8. + + + + +Rigney Informational [Page 25] + +RFC 2866 RADIUS Accounting June 2000 + + +9. References + + [1] Rigney, C., "RADIUS Accounting", RFC 2139, April 1997. + + [2] Rigney, C., Willens, S., Rubens, A. and W. Simpson, "Remote + Authentication Dial In User Service (RADIUS)", RFC 2865, June + 2000. + + [3] Bradner, S., "Key words for use in RFCs to Indicate Requirement + Levels", BCP 14, RFC 2119, March, 1997. + + [4] Postel, J., "User Datagram Protocol", STD 6, RFC 768, August + 1980. + + [5] Rivest, R. and S. Dusse, "The MD5 Message-Digest Algorithm", RFC + 1321, April 1992. + + [6] Reynolds, J. and J. Postel, "Assigned Numbers", STD 2, RFC 1700, + October 1994. + + [7] Yergeau, F., "UTF-8, a transformation format of ISO 10646", RFC + 2279, January 1998. + + [8] Alvestrand, H. and T. Narten, "Guidelines for Writing an IANA + Considerations Section in RFCs", BCP 26, RFC 2434, October 1998. + +10. Acknowledgements + + RADIUS and RADIUS Accounting were originally developed by Steve + Willens of Livingston Enterprises for their PortMaster series of + Network Access Servers. + +11. Chair's Address + + The RADIUS working group can be contacted via the current chair: + + Carl Rigney + Livingston Enterprises + 4464 Willow Road + Pleasanton, California 94588 + + Phone: +1 925 737 2100 + EMail: cdr@telemancy.com + + + + + + + + +Rigney Informational [Page 26] + +RFC 2866 RADIUS Accounting June 2000 + + +12. Author's Address + + Questions about this memo can also be directed to: + + Carl Rigney + Livingston Enterprises + 4464 Willow Road + Pleasanton, California 94588 + + EMail: cdr@telemancy.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Rigney Informational [Page 27] + +RFC 2866 RADIUS Accounting June 2000 + + +13. Full Copyright Statement + + Copyright (C) The Internet Society (2000). All Rights Reserved. + + This document and translations of it may be copied and furnished to + others, and derivative works that comment on or otherwise explain it + or assist in its implementation may be prepared, copied, published + and distributed, in whole or in part, without restriction of any + kind, provided that the above copyright notice and this paragraph are + included on all such copies and derivative works. However, this + document itself may not be modified in any way, such as by removing + the copyright notice or references to the Internet Society or other + Internet organizations, except as needed for the purpose of + developing Internet standards in which case the procedures for + copyrights defined in the Internet Standards process must be + followed, or as required to translate it into languages other than + English. + + The limited permissions granted above are perpetual and will not be + revoked by the Internet Society or its successors or assigns. + + This document and the information contained herein is provided on an + "AS IS" basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING + TASK FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING + BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION + HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +Acknowledgement + + Funding for the RFC Editor function is currently provided by the + Internet Society. + + + + + + + + + + + + + + + + + + + +Rigney Informational [Page 28] + diff --git a/test/client/test.client.ts b/test/client/test.client.ts new file mode 100644 index 0000000..3e87c73 --- /dev/null +++ b/test/client/test.client.ts @@ -0,0 +1,167 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import { RadiusClient } from '../../ts_client/index.js'; +import { + RadiusServer, + ERadiusCode, + EAcctStatusType, +} from '../../ts_server/index.js'; + +// Test server and client instances +let server: RadiusServer; +let client: RadiusClient; +const TEST_SECRET = 'testing123'; +const TEST_AUTH_PORT = 18120; +const TEST_ACCT_PORT = 18130; + +tap.test('setup - create server and client', async () => { + // Create server with authentication handler + server = new RadiusServer({ + authPort: TEST_AUTH_PORT, + acctPort: TEST_ACCT_PORT, + defaultSecret: TEST_SECRET, + authenticationHandler: async (request) => { + // Simple handler: accept user/password, reject others + if (request.username === 'testuser' && request.password === 'testpass') { + return { + code: ERadiusCode.AccessAccept, + replyMessage: 'Welcome!', + sessionTimeout: 3600, + }; + } + + // Test CHAP + if (request.username === 'chapuser' && request.chapPassword && request.chapChallenge) { + const { RadiusAuthenticator } = await import('../../ts_server/index.js'); + const isValid = RadiusAuthenticator.verifyChapResponse( + request.chapPassword, + request.chapChallenge, + 'chappass' + ); + if (isValid) { + return { + code: ERadiusCode.AccessAccept, + replyMessage: 'CHAP OK', + }; + } + } + + return { + code: ERadiusCode.AccessReject, + replyMessage: 'Invalid credentials', + }; + }, + accountingHandler: async (request) => { + return { success: true }; + }, + }); + + await server.start(); + + // Create client + client = new RadiusClient({ + host: '127.0.0.1', + authPort: TEST_AUTH_PORT, + acctPort: TEST_ACCT_PORT, + secret: TEST_SECRET, + timeout: 2000, + retries: 2, + }); + + await client.connect(); +}); + +tap.test('should authenticate with PAP - valid credentials', async () => { + const response = await client.authenticatePap('testuser', 'testpass'); + + expect(response.accepted).toBeTruthy(); + expect(response.rejected).toBeFalsy(); + expect(response.code).toEqual(ERadiusCode.AccessAccept); + expect(response.replyMessage).toEqual('Welcome!'); + expect(response.sessionTimeout).toEqual(3600); +}); + +tap.test('should reject PAP - invalid credentials', async () => { + const response = await client.authenticatePap('testuser', 'wrongpass'); + + expect(response.accepted).toBeFalsy(); + expect(response.rejected).toBeTruthy(); + expect(response.code).toEqual(ERadiusCode.AccessReject); + expect(response.replyMessage).toEqual('Invalid credentials'); +}); + +tap.test('should authenticate with CHAP - valid credentials', async () => { + const response = await client.authenticateChap('chapuser', 'chappass'); + + expect(response.accepted).toBeTruthy(); + expect(response.rejected).toBeFalsy(); + expect(response.code).toEqual(ERadiusCode.AccessAccept); +}); + +tap.test('should reject CHAP - invalid credentials', async () => { + const response = await client.authenticateChap('chapuser', 'wrongpass'); + + expect(response.accepted).toBeFalsy(); + expect(response.rejected).toBeTruthy(); + expect(response.code).toEqual(ERadiusCode.AccessReject); +}); + +tap.test('should send accounting start', async () => { + const response = await client.accountingStart('session-001', 'testuser'); + + expect(response.success).toBeTruthy(); +}); + +tap.test('should send accounting update', async () => { + const response = await client.accountingUpdate('session-001', { + username: 'testuser', + sessionTime: 1800, + inputOctets: 512000, + outputOctets: 1024000, + }); + + expect(response.success).toBeTruthy(); +}); + +tap.test('should send accounting stop', async () => { + const response = await client.accountingStop('session-001', { + username: 'testuser', + sessionTime: 3600, + inputOctets: 1024000, + outputOctets: 2048000, + terminateCause: 1, // User-Request + }); + + expect(response.success).toBeTruthy(); +}); + +tap.test('should send custom accounting request', async () => { + const response = await client.accounting({ + statusType: EAcctStatusType.Start, + sessionId: 'custom-session', + username: 'customuser', + nasPort: 5060, + calledStationId: 'called-001', + callingStationId: 'calling-002', + }); + + expect(response.success).toBeTruthy(); +}); + +tap.test('should handle authentication with custom attributes', async () => { + const response = await client.authenticate({ + username: 'testuser', + password: 'testpass', + nasPort: 1, + calledStationId: 'test-station', + callingStationId: '192.168.1.100', + }); + + expect(response.accepted).toBeTruthy(); +}); + +tap.test('teardown - cleanup server and client', async () => { + await client.disconnect(); + await server.stop(); +}); + +export default tap.start(); diff --git a/test/client/test.integration.ts b/test/client/test.integration.ts new file mode 100644 index 0000000..284a5db --- /dev/null +++ b/test/client/test.integration.ts @@ -0,0 +1,304 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import { RadiusClient } from '../../ts_client/index.js'; +import { + RadiusServer, + ERadiusCode, + EAcctStatusType, + EAcctTerminateCause, + RadiusAuthenticator, +} from '../../ts_server/index.js'; + +// Integration test: full client-server communication +let server: RadiusServer; +const TEST_SECRET = 'integration-secret'; +const TEST_AUTH_PORT = 18200; +const TEST_ACCT_PORT = 18210; + +// Track accounting records +const accountingRecords: any[] = []; + +// User database +const users: Record = { + alice: { password: 'alice123', sessionTimeout: 7200 }, + bob: { password: 'bob456', sessionTimeout: 3600 }, + charlie: { password: 'charlie789' }, +}; + +tap.test('setup - start integration test server', async () => { + server = new RadiusServer({ + authPort: TEST_AUTH_PORT, + acctPort: TEST_ACCT_PORT, + defaultSecret: TEST_SECRET, + authenticationHandler: async (request) => { + const user = users[request.username]; + if (!user) { + return { + code: ERadiusCode.AccessReject, + replyMessage: 'Unknown user', + }; + } + + // PAP authentication + if (request.password !== undefined) { + if (request.password === user.password) { + return { + code: ERadiusCode.AccessAccept, + replyMessage: `Welcome, ${request.username}!`, + sessionTimeout: user.sessionTimeout, + framedIpAddress: '10.0.0.' + (Object.keys(users).indexOf(request.username) + 10), + }; + } + return { + code: ERadiusCode.AccessReject, + replyMessage: 'Invalid password', + }; + } + + // CHAP authentication + if (request.chapPassword && request.chapChallenge) { + const isValid = RadiusAuthenticator.verifyChapResponse( + request.chapPassword, + request.chapChallenge, + user.password + ); + if (isValid) { + return { + code: ERadiusCode.AccessAccept, + replyMessage: `Welcome, ${request.username}! (CHAP)`, + sessionTimeout: user.sessionTimeout, + }; + } + return { + code: ERadiusCode.AccessReject, + replyMessage: 'CHAP authentication failed', + }; + } + + return { + code: ERadiusCode.AccessReject, + replyMessage: 'No authentication method provided', + }; + }, + accountingHandler: async (request) => { + accountingRecords.push({ + statusType: request.statusType, + sessionId: request.sessionId, + username: request.username, + sessionTime: request.sessionTime, + inputOctets: request.inputOctets, + outputOctets: request.outputOctets, + terminateCause: request.terminateCause, + timestamp: Date.now(), + }); + return { success: true }; + }, + }); + + await server.start(); +}); + +tap.test('integration: PAP authentication for multiple users', async () => { + const client = new RadiusClient({ + host: '127.0.0.1', + authPort: TEST_AUTH_PORT, + acctPort: TEST_ACCT_PORT, + secret: TEST_SECRET, + timeout: 2000, + }); + await client.connect(); + + // Alice + let response = await client.authenticatePap('alice', 'alice123'); + expect(response.accepted).toBeTruthy(); + expect(response.replyMessage).toInclude('alice'); + expect(response.sessionTimeout).toEqual(7200); + expect(response.framedIpAddress).toEqual('10.0.0.10'); + + // Bob + response = await client.authenticatePap('bob', 'bob456'); + expect(response.accepted).toBeTruthy(); + expect(response.replyMessage).toInclude('bob'); + expect(response.sessionTimeout).toEqual(3600); + + // Charlie (no session timeout) + response = await client.authenticatePap('charlie', 'charlie789'); + expect(response.accepted).toBeTruthy(); + expect(response.replyMessage).toInclude('charlie'); + expect(response.sessionTimeout).toBeUndefined(); + + // Unknown user + response = await client.authenticatePap('unknown', 'password'); + expect(response.rejected).toBeTruthy(); + expect(response.replyMessage).toEqual('Unknown user'); + + // Wrong password + response = await client.authenticatePap('alice', 'wrongpassword'); + expect(response.rejected).toBeTruthy(); + expect(response.replyMessage).toEqual('Invalid password'); + + await client.disconnect(); +}); + +tap.test('integration: CHAP authentication', async () => { + const client = new RadiusClient({ + host: '127.0.0.1', + authPort: TEST_AUTH_PORT, + acctPort: TEST_ACCT_PORT, + secret: TEST_SECRET, + timeout: 2000, + }); + await client.connect(); + + // CHAP with correct password + let response = await client.authenticateChap('bob', 'bob456'); + expect(response.accepted).toBeTruthy(); + expect(response.replyMessage).toInclude('CHAP'); + + // CHAP with wrong password + response = await client.authenticateChap('bob', 'wrongpass'); + expect(response.rejected).toBeTruthy(); + + await client.disconnect(); +}); + +tap.test('integration: full session lifecycle with accounting', async () => { + const client = new RadiusClient({ + host: '127.0.0.1', + authPort: TEST_AUTH_PORT, + acctPort: TEST_ACCT_PORT, + secret: TEST_SECRET, + timeout: 2000, + }); + await client.connect(); + + const sessionId = `session-${Date.now()}`; + + // 1. Authenticate + const authResponse = await client.authenticatePap('alice', 'alice123'); + expect(authResponse.accepted).toBeTruthy(); + + // 2. Accounting Start + let acctResponse = await client.accountingStart(sessionId, 'alice'); + expect(acctResponse.success).toBeTruthy(); + + // 3. Interim Update + acctResponse = await client.accountingUpdate(sessionId, { + username: 'alice', + sessionTime: 300, + inputOctets: 100000, + outputOctets: 200000, + }); + expect(acctResponse.success).toBeTruthy(); + + // 4. Accounting Stop + acctResponse = await client.accountingStop(sessionId, { + username: 'alice', + sessionTime: 600, + inputOctets: 250000, + outputOctets: 500000, + terminateCause: EAcctTerminateCause.UserRequest, + }); + expect(acctResponse.success).toBeTruthy(); + + // Verify accounting records + const sessionRecords = accountingRecords.filter((r) => r.sessionId === sessionId); + expect(sessionRecords.length).toEqual(3); + + const startRecord = sessionRecords.find((r) => r.statusType === EAcctStatusType.Start); + expect(startRecord).toBeDefined(); + expect(startRecord!.username).toEqual('alice'); + + const updateRecord = sessionRecords.find((r) => r.statusType === EAcctStatusType.InterimUpdate); + expect(updateRecord).toBeDefined(); + expect(updateRecord!.sessionTime).toEqual(300); + + const stopRecord = sessionRecords.find((r) => r.statusType === EAcctStatusType.Stop); + expect(stopRecord).toBeDefined(); + expect(stopRecord!.sessionTime).toEqual(600); + expect(stopRecord!.terminateCause).toEqual(EAcctTerminateCause.UserRequest); + + await client.disconnect(); +}); + +tap.test('integration: multiple concurrent clients', async () => { + const clients = []; + const results = []; + + // Create 5 clients + for (let i = 0; i < 5; i++) { + const client = new RadiusClient({ + host: '127.0.0.1', + authPort: TEST_AUTH_PORT, + acctPort: TEST_ACCT_PORT, + secret: TEST_SECRET, + timeout: 2000, + }); + await client.connect(); + clients.push(client); + } + + // Send authentication requests concurrently + const promises = clients.map((client, i) => + client.authenticatePap(i % 2 === 0 ? 'alice' : 'bob', i % 2 === 0 ? 'alice123' : 'bob456') + ); + + const responses = await Promise.all(promises); + + // All should succeed + for (const response of responses) { + expect(response.accepted).toBeTruthy(); + } + + // Cleanup + for (const client of clients) { + await client.disconnect(); + } +}); + +tap.test('integration: server statistics', async () => { + const stats = server.getStats(); + + // Should have recorded some requests + expect(stats.authRequests).toBeGreaterThan(0); + expect(stats.authAccepts).toBeGreaterThan(0); + expect(stats.authRejects).toBeGreaterThan(0); + expect(stats.acctRequests).toBeGreaterThan(0); + expect(stats.acctResponses).toBeGreaterThan(0); + + // Should have no invalid packets (we sent valid ones) + expect(stats.authInvalidPackets).toEqual(0); + expect(stats.acctInvalidPackets).toEqual(0); +}); + +tap.test('integration: duplicate request handling', async () => { + // This tests that the server handles duplicate requests correctly + // by caching responses within the detection window + const client = new RadiusClient({ + host: '127.0.0.1', + authPort: TEST_AUTH_PORT, + acctPort: TEST_ACCT_PORT, + secret: TEST_SECRET, + timeout: 2000, + }); + await client.connect(); + + // Send same request multiple times quickly + const responses = await Promise.all([ + client.authenticatePap('alice', 'alice123'), + client.authenticatePap('alice', 'alice123'), + client.authenticatePap('alice', 'alice123'), + ]); + + // All should succeed with same result + for (const response of responses) { + expect(response.accepted).toBeTruthy(); + } + + await client.disconnect(); +}); + +tap.test('teardown - stop integration test server', async () => { + await server.stop(); +}); + +export default tap.start(); diff --git a/test/client/test.timeout.ts b/test/client/test.timeout.ts new file mode 100644 index 0000000..d574546 --- /dev/null +++ b/test/client/test.timeout.ts @@ -0,0 +1,149 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import { RadiusClient } from '../../ts_client/index.js'; + +tap.test('should timeout when server is not reachable', async () => { + // Connect to a port where no server is running + const client = new RadiusClient({ + host: '127.0.0.1', + authPort: 19999, // Unlikely to have a server + acctPort: 19998, + secret: 'testing123', + timeout: 500, // Short timeout + retries: 1, // Minimal retries + }); + + await client.connect(); + + let error: Error | undefined; + try { + await client.authenticatePap('user', 'pass'); + } catch (e) { + error = e as Error; + } + + expect(error).toBeDefined(); + expect(error!.message).toInclude('timed out'); + + await client.disconnect(); +}); + +tap.test('should retry on timeout', async () => { + const startTime = Date.now(); + + const client = new RadiusClient({ + host: '127.0.0.1', + authPort: 19997, + acctPort: 19996, + secret: 'testing123', + timeout: 200, // 200ms timeout + retries: 3, // 3 retries + retryDelay: 100, // 100ms initial delay + }); + + await client.connect(); + + let error: Error | undefined; + try { + await client.authenticatePap('user', 'pass'); + } catch (e) { + error = e as Error; + } + + const elapsed = Date.now() - startTime; + + expect(error).toBeDefined(); + // With 3 retries and exponential backoff, should take at least: + // Initial timeout (200) + retry 1 delay (100) + timeout (200) + retry 2 delay (200) + timeout (200) + retry 3 delay (400) + timeout (200) + // = 200 + 100 + 200 + 200 + 200 + 400 + 200 = ~1500ms minimum + expect(elapsed).toBeGreaterThanOrEqual(500); + + await client.disconnect(); +}); + +tap.test('should handle disconnect during request', async () => { + const client = new RadiusClient({ + host: '127.0.0.1', + authPort: 19995, + acctPort: 19994, + secret: 'testing123', + timeout: 5000, + retries: 3, + }); + + await client.connect(); + + // Start a request (will never complete because no server) + const requestPromise = client.authenticatePap('user', 'pass'); + + // Disconnect immediately + await client.disconnect(); + + let error: Error | undefined; + try { + await requestPromise; + } catch (e) { + error = e as Error; + } + + // When the client disconnects, pending requests are rejected + // The error can be either "Client disconnected" or other disconnect-related messages + expect(error).toBeDefined(); + // Just verify we got an error - the specific message may vary +}); + +tap.test('should handle multiple concurrent requests', async () => { + // This test just verifies we can make multiple requests + // They will all timeout since no server, but should handle correctly + const client = new RadiusClient({ + host: '127.0.0.1', + authPort: 19993, + acctPort: 19992, + secret: 'testing123', + timeout: 200, + retries: 1, + }); + + await client.connect(); + + const requests = [ + client.authenticatePap('user1', 'pass1').catch((e) => e), + client.authenticatePap('user2', 'pass2').catch((e) => e), + client.authenticatePap('user3', 'pass3').catch((e) => e), + ]; + + const results = await Promise.all(requests); + + // All should be errors (timeout) + for (const result of results) { + expect(result).toBeInstanceOf(Error); + } + + await client.disconnect(); +}); + +tap.test('should auto-connect if not connected', async () => { + const client = new RadiusClient({ + host: '127.0.0.1', + authPort: 19991, + acctPort: 19990, + secret: 'testing123', + timeout: 200, + retries: 1, + }); + + // Don't call connect() - should auto-connect + let error: Error | undefined; + try { + await client.authenticatePap('user', 'pass'); + } catch (e) { + error = e as Error; + } + + // Should timeout, not connection error + expect(error).toBeDefined(); + expect(error!.message).toInclude('timed out'); + + await client.disconnect(); +}); + +export default tap.start(); diff --git a/test/server/test.accounting.ts b/test/server/test.accounting.ts new file mode 100644 index 0000000..3ed9756 --- /dev/null +++ b/test/server/test.accounting.ts @@ -0,0 +1,246 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import { + RadiusPacket, + RadiusAuthenticator, + ERadiusCode, + ERadiusAttributeType, + EAcctStatusType, + EAcctTerminateCause, + EAcctAuthentic, +} from '../../ts_server/index.js'; + +tap.test('should create Accounting-Request packet with Start status', async () => { + const identifier = 1; + const secret = 'testing123'; + + const packet = RadiusPacket.createAccountingRequest(identifier, secret, [ + { type: ERadiusAttributeType.AcctStatusType, value: EAcctStatusType.Start }, + { type: ERadiusAttributeType.AcctSessionId, value: 'session-001' }, + { type: ERadiusAttributeType.UserName, value: 'testuser' }, + { type: ERadiusAttributeType.NasIpAddress, value: '192.168.1.1' }, + ]); + + const decoded = RadiusPacket.decode(packet); + expect(decoded.code).toEqual(ERadiusCode.AccountingRequest); + expect(decoded.identifier).toEqual(identifier); +}); + +tap.test('should create Accounting-Request packet with Stop status', async () => { + const identifier = 2; + const secret = 'testing123'; + + const packet = RadiusPacket.createAccountingRequest(identifier, secret, [ + { type: ERadiusAttributeType.AcctStatusType, value: EAcctStatusType.Stop }, + { type: ERadiusAttributeType.AcctSessionId, value: 'session-001' }, + { type: ERadiusAttributeType.UserName, value: 'testuser' }, + { type: ERadiusAttributeType.AcctSessionTime, value: 3600 }, // 1 hour + { type: ERadiusAttributeType.AcctInputOctets, value: 1024000 }, + { type: ERadiusAttributeType.AcctOutputOctets, value: 2048000 }, + { type: ERadiusAttributeType.AcctInputPackets, value: 1000 }, + { type: ERadiusAttributeType.AcctOutputPackets, value: 2000 }, + { type: ERadiusAttributeType.AcctTerminateCause, value: EAcctTerminateCause.UserRequest }, + ]); + + const decoded = RadiusPacket.decodeAndParse(packet); + expect(decoded.code).toEqual(ERadiusCode.AccountingRequest); + + const statusType = decoded.parsedAttributes.find( + (a) => a.type === ERadiusAttributeType.AcctStatusType + ); + expect(statusType?.value).toEqual(EAcctStatusType.Stop); +}); + +tap.test('should create Accounting-Request packet with Interim-Update status', async () => { + const identifier = 3; + const secret = 'testing123'; + + const packet = RadiusPacket.createAccountingRequest(identifier, secret, [ + { type: ERadiusAttributeType.AcctStatusType, value: EAcctStatusType.InterimUpdate }, + { type: ERadiusAttributeType.AcctSessionId, value: 'session-001' }, + { type: ERadiusAttributeType.UserName, value: 'testuser' }, + { type: ERadiusAttributeType.AcctSessionTime, value: 1800 }, // 30 min + { type: ERadiusAttributeType.AcctInputOctets, value: 512000 }, + { type: ERadiusAttributeType.AcctOutputOctets, value: 1024000 }, + ]); + + const decoded = RadiusPacket.decodeAndParse(packet); + expect(decoded.code).toEqual(ERadiusCode.AccountingRequest); + + const statusType = decoded.parsedAttributes.find( + (a) => a.type === ERadiusAttributeType.AcctStatusType + ); + expect(statusType?.value).toEqual(EAcctStatusType.InterimUpdate); +}); + +tap.test('should verify Accounting-Request authenticator', async () => { + const identifier = 1; + const secret = 'testing123'; + + const packet = RadiusPacket.createAccountingRequest(identifier, secret, [ + { type: ERadiusAttributeType.AcctStatusType, value: EAcctStatusType.Start }, + { type: ERadiusAttributeType.AcctSessionId, value: 'session-001' }, + ]); + + // Verify the authenticator + const isValid = RadiusAuthenticator.verifyAccountingRequestAuthenticator(packet, secret); + expect(isValid).toBeTruthy(); + + // Should fail with wrong secret + const isInvalid = RadiusAuthenticator.verifyAccountingRequestAuthenticator(packet, 'wrongsecret'); + expect(isInvalid).toBeFalsy(); +}); + +tap.test('should create Accounting-Response packet', async () => { + const identifier = 1; + const requestAuthenticator = Buffer.alloc(16, 0x42); + const secret = 'testing123'; + + const packet = RadiusPacket.createAccountingResponse( + identifier, + requestAuthenticator, + secret, + [] // Usually no attributes in response + ); + + const decoded = RadiusPacket.decode(packet); + expect(decoded.code).toEqual(ERadiusCode.AccountingResponse); + expect(decoded.identifier).toEqual(identifier); +}); + +tap.test('should verify Accounting-Response authenticator', async () => { + const identifier = 1; + const requestAuthenticator = Buffer.alloc(16, 0x42); + const secret = 'testing123'; + + const response = RadiusPacket.createAccountingResponse( + identifier, + requestAuthenticator, + secret, + [] + ); + + // Verify response authenticator + const isValid = RadiusAuthenticator.verifyResponseAuthenticator( + response, + requestAuthenticator, + secret + ); + expect(isValid).toBeTruthy(); +}); + +tap.test('should parse all accounting attributes correctly', async () => { + const identifier = 1; + const secret = 'testing123'; + + const packet = RadiusPacket.createAccountingRequest(identifier, secret, [ + { type: ERadiusAttributeType.AcctStatusType, value: EAcctStatusType.Stop }, + { type: ERadiusAttributeType.AcctDelayTime, value: 5 }, + { type: ERadiusAttributeType.AcctInputOctets, value: 1000000 }, + { type: ERadiusAttributeType.AcctOutputOctets, value: 2000000 }, + { type: ERadiusAttributeType.AcctSessionId, value: 'sess-123' }, + { type: ERadiusAttributeType.AcctAuthentic, value: EAcctAuthentic.Radius }, + { type: ERadiusAttributeType.AcctSessionTime, value: 7200 }, + { type: ERadiusAttributeType.AcctInputPackets, value: 5000 }, + { type: ERadiusAttributeType.AcctOutputPackets, value: 10000 }, + { type: ERadiusAttributeType.AcctTerminateCause, value: EAcctTerminateCause.SessionTimeout }, + { type: ERadiusAttributeType.AcctMultiSessionId, value: 'multi-sess-456' }, + { type: ERadiusAttributeType.AcctLinkCount, value: 2 }, + ]); + + const decoded = RadiusPacket.decodeAndParse(packet); + + // Check each attribute + const attrs = decoded.parsedAttributes; + + const statusType = attrs.find((a) => a.type === ERadiusAttributeType.AcctStatusType); + expect(statusType?.value).toEqual(EAcctStatusType.Stop); + + const delayTime = attrs.find((a) => a.type === ERadiusAttributeType.AcctDelayTime); + expect(delayTime?.value).toEqual(5); + + const inputOctets = attrs.find((a) => a.type === ERadiusAttributeType.AcctInputOctets); + expect(inputOctets?.value).toEqual(1000000); + + const outputOctets = attrs.find((a) => a.type === ERadiusAttributeType.AcctOutputOctets); + expect(outputOctets?.value).toEqual(2000000); + + const sessionId = attrs.find((a) => a.type === ERadiusAttributeType.AcctSessionId); + expect(sessionId?.value).toEqual('sess-123'); + + const authentic = attrs.find((a) => a.type === ERadiusAttributeType.AcctAuthentic); + expect(authentic?.value).toEqual(EAcctAuthentic.Radius); + + const sessionTime = attrs.find((a) => a.type === ERadiusAttributeType.AcctSessionTime); + expect(sessionTime?.value).toEqual(7200); + + const terminateCause = attrs.find((a) => a.type === ERadiusAttributeType.AcctTerminateCause); + expect(terminateCause?.value).toEqual(EAcctTerminateCause.SessionTimeout); +}); + +tap.test('should handle Accounting-On/Off status types', async () => { + const secret = 'testing123'; + + // Accounting-On (NAS restart/reboot notification) + const acctOnPacket = RadiusPacket.createAccountingRequest(1, secret, [ + { type: ERadiusAttributeType.AcctStatusType, value: EAcctStatusType.AccountingOn }, + { type: ERadiusAttributeType.NasIpAddress, value: '192.168.1.1' }, + ]); + + let decoded = RadiusPacket.decodeAndParse(acctOnPacket); + let statusType = decoded.parsedAttributes.find( + (a) => a.type === ERadiusAttributeType.AcctStatusType + ); + expect(statusType?.value).toEqual(EAcctStatusType.AccountingOn); + + // Accounting-Off + const acctOffPacket = RadiusPacket.createAccountingRequest(2, secret, [ + { type: ERadiusAttributeType.AcctStatusType, value: EAcctStatusType.AccountingOff }, + { type: ERadiusAttributeType.NasIpAddress, value: '192.168.1.1' }, + ]); + + decoded = RadiusPacket.decodeAndParse(acctOffPacket); + statusType = decoded.parsedAttributes.find( + (a) => a.type === ERadiusAttributeType.AcctStatusType + ); + expect(statusType?.value).toEqual(EAcctStatusType.AccountingOff); +}); + +tap.test('should handle all termination causes', async () => { + const secret = 'testing123'; + const terminationCauses = [ + EAcctTerminateCause.UserRequest, + EAcctTerminateCause.LostCarrier, + EAcctTerminateCause.LostService, + EAcctTerminateCause.IdleTimeout, + EAcctTerminateCause.SessionTimeout, + EAcctTerminateCause.AdminReset, + EAcctTerminateCause.AdminReboot, + EAcctTerminateCause.PortError, + EAcctTerminateCause.NasError, + EAcctTerminateCause.NasRequest, + EAcctTerminateCause.NasReboot, + EAcctTerminateCause.PortUnneeded, + EAcctTerminateCause.PortPreempted, + EAcctTerminateCause.PortSuspended, + EAcctTerminateCause.ServiceUnavailable, + EAcctTerminateCause.Callback, + EAcctTerminateCause.UserError, + EAcctTerminateCause.HostRequest, + ]; + + for (const cause of terminationCauses) { + const packet = RadiusPacket.createAccountingRequest(1, secret, [ + { type: ERadiusAttributeType.AcctStatusType, value: EAcctStatusType.Stop }, + { type: ERadiusAttributeType.AcctSessionId, value: 'session-001' }, + { type: ERadiusAttributeType.AcctTerminateCause, value: cause }, + ]); + + const decoded = RadiusPacket.decodeAndParse(packet); + const termCause = decoded.parsedAttributes.find( + (a) => a.type === ERadiusAttributeType.AcctTerminateCause + ); + expect(termCause?.value).toEqual(cause); + } +}); + +export default tap.start(); diff --git a/test/server/test.attributes.ts b/test/server/test.attributes.ts new file mode 100644 index 0000000..852a9d2 --- /dev/null +++ b/test/server/test.attributes.ts @@ -0,0 +1,211 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import { + RadiusAttributes, + ERadiusAttributeType, +} from '../../ts_server/index.js'; + +tap.test('should get attribute definitions by type', async () => { + const userNameDef = RadiusAttributes.getDefinition(ERadiusAttributeType.UserName); + expect(userNameDef).toBeDefined(); + expect(userNameDef!.name).toEqual('User-Name'); + expect(userNameDef!.valueType).toEqual('text'); + + const nasIpDef = RadiusAttributes.getDefinition(ERadiusAttributeType.NasIpAddress); + expect(nasIpDef).toBeDefined(); + expect(nasIpDef!.name).toEqual('NAS-IP-Address'); + expect(nasIpDef!.valueType).toEqual('address'); + + const nasPortDef = RadiusAttributes.getDefinition(ERadiusAttributeType.NasPort); + expect(nasPortDef).toBeDefined(); + expect(nasPortDef!.name).toEqual('NAS-Port'); + expect(nasPortDef!.valueType).toEqual('integer'); +}); + +tap.test('should get attribute type by name', async () => { + expect(RadiusAttributes.getTypeByName('User-Name')).toEqual(ERadiusAttributeType.UserName); + expect(RadiusAttributes.getTypeByName('NAS-IP-Address')).toEqual(ERadiusAttributeType.NasIpAddress); + expect(RadiusAttributes.getTypeByName('NAS-Port')).toEqual(ERadiusAttributeType.NasPort); + expect(RadiusAttributes.getTypeByName('Unknown-Attribute')).toBeUndefined(); +}); + +tap.test('should get attribute name by type', async () => { + expect(RadiusAttributes.getNameByType(ERadiusAttributeType.UserName)).toEqual('User-Name'); + expect(RadiusAttributes.getNameByType(ERadiusAttributeType.UserPassword)).toEqual('User-Password'); + expect(RadiusAttributes.getNameByType(255)).toInclude('Unknown-Attribute'); +}); + +tap.test('should parse text attributes', async () => { + const textBuffer = Buffer.from('testuser', 'utf8'); + const parsed = RadiusAttributes.parseValue(ERadiusAttributeType.UserName, textBuffer); + expect(parsed).toEqual('testuser'); +}); + +tap.test('should parse address attributes', async () => { + const addressBuffer = Buffer.from([192, 168, 1, 100]); + const parsed = RadiusAttributes.parseValue(ERadiusAttributeType.NasIpAddress, addressBuffer); + expect(parsed).toEqual('192.168.1.100'); +}); + +tap.test('should parse integer attributes', async () => { + const intBuffer = Buffer.allocUnsafe(4); + intBuffer.writeUInt32BE(12345, 0); + const parsed = RadiusAttributes.parseValue(ERadiusAttributeType.NasPort, intBuffer); + expect(parsed).toEqual(12345); +}); + +tap.test('should encode text attributes', async () => { + const encoded = RadiusAttributes.encodeValue(ERadiusAttributeType.UserName, 'testuser'); + expect(encoded.toString('utf8')).toEqual('testuser'); +}); + +tap.test('should encode address attributes', async () => { + const encoded = RadiusAttributes.encodeValue(ERadiusAttributeType.NasIpAddress, '10.20.30.40'); + expect(encoded.length).toEqual(4); + expect(encoded[0]).toEqual(10); + expect(encoded[1]).toEqual(20); + expect(encoded[2]).toEqual(30); + expect(encoded[3]).toEqual(40); +}); + +tap.test('should encode integer attributes', async () => { + const encoded = RadiusAttributes.encodeValue(ERadiusAttributeType.NasPort, 65535); + expect(encoded.length).toEqual(4); + expect(encoded.readUInt32BE(0)).toEqual(65535); +}); + +tap.test('should encode complete attribute with TLV format', async () => { + const encoded = RadiusAttributes.encodeAttribute(ERadiusAttributeType.UserName, 'test'); + expect(encoded[0]).toEqual(ERadiusAttributeType.UserName); // Type + expect(encoded[1]).toEqual(6); // Length (2 + 4) + expect(encoded.subarray(2).toString('utf8')).toEqual('test'); +}); + +tap.test('should handle attribute value too long', async () => { + const longValue = Buffer.alloc(254); // Max is 253 bytes + let error: Error | undefined; + try { + RadiusAttributes.encodeAttribute(ERadiusAttributeType.UserName, longValue); + } catch (e) { + error = e as Error; + } + expect(error).toBeDefined(); + expect(error!.message).toInclude('too long'); +}); + +tap.test('should parse raw attribute', async () => { + const rawAttr = { + type: ERadiusAttributeType.UserName, + value: Buffer.from('john.doe', 'utf8'), + }; + const parsed = RadiusAttributes.parseAttribute(rawAttr); + expect(parsed.type).toEqual(ERadiusAttributeType.UserName); + expect(parsed.name).toEqual('User-Name'); + expect(parsed.value).toEqual('john.doe'); +}); + +tap.test('should handle Vendor-Specific Attributes', async () => { + // Vendor-Id: 9 (Cisco), Vendor-Type: 1, Value: 'test' + const vendorId = 9; + const vendorType = 1; + const vendorValue = Buffer.from('cisco-av-pair', 'utf8'); + + const vsaBuffer = RadiusAttributes.encodeVSA({ vendorId, vendorType, vendorValue }); + + // Parse it back + const parsed = RadiusAttributes.parseVSA(vsaBuffer); + expect(parsed).toBeDefined(); + expect(parsed!.vendorId).toEqual(vendorId); + expect(parsed!.vendorType).toEqual(vendorType); + expect(parsed!.vendorValue.toString('utf8')).toEqual('cisco-av-pair'); +}); + +tap.test('should create complete Vendor-Specific attribute', async () => { + const vendorId = 311; // Microsoft + const vendorType = 1; + const vendorValue = Buffer.from('test-value', 'utf8'); + + const attr = RadiusAttributes.createVendorAttribute(vendorId, vendorType, vendorValue); + + // First byte should be type 26 (Vendor-Specific) + expect(attr[0]).toEqual(ERadiusAttributeType.VendorSpecific); + + // Second byte is total length + expect(attr[1]).toBeGreaterThan(6); +}); + +tap.test('should check if attribute is encrypted', async () => { + expect(RadiusAttributes.isEncrypted(ERadiusAttributeType.UserPassword)).toBeTruthy(); + expect(RadiusAttributes.isEncrypted(ERadiusAttributeType.UserName)).toBeFalsy(); +}); + +tap.test('should handle all standard RFC 2865 attributes', async () => { + // Test that all standard attributes have definitions + const standardAttributes = [ + ERadiusAttributeType.UserName, + ERadiusAttributeType.UserPassword, + ERadiusAttributeType.ChapPassword, + ERadiusAttributeType.NasIpAddress, + ERadiusAttributeType.NasPort, + ERadiusAttributeType.ServiceType, + ERadiusAttributeType.FramedProtocol, + ERadiusAttributeType.FramedIpAddress, + ERadiusAttributeType.FramedIpNetmask, + ERadiusAttributeType.FramedRouting, + ERadiusAttributeType.FilterId, + ERadiusAttributeType.FramedMtu, + ERadiusAttributeType.FramedCompression, + ERadiusAttributeType.LoginIpHost, + ERadiusAttributeType.LoginService, + ERadiusAttributeType.LoginTcpPort, + ERadiusAttributeType.ReplyMessage, + ERadiusAttributeType.CallbackNumber, + ERadiusAttributeType.CallbackId, + ERadiusAttributeType.FramedRoute, + ERadiusAttributeType.FramedIpxNetwork, + ERadiusAttributeType.State, + ERadiusAttributeType.Class, + ERadiusAttributeType.VendorSpecific, + ERadiusAttributeType.SessionTimeout, + ERadiusAttributeType.IdleTimeout, + ERadiusAttributeType.TerminationAction, + ERadiusAttributeType.CalledStationId, + ERadiusAttributeType.CallingStationId, + ERadiusAttributeType.NasIdentifier, + ERadiusAttributeType.ProxyState, + ERadiusAttributeType.ChapChallenge, + ERadiusAttributeType.NasPortType, + ERadiusAttributeType.PortLimit, + ]; + + for (const attrType of standardAttributes) { + const def = RadiusAttributes.getDefinition(attrType); + expect(def).toBeDefined(); + expect(def!.name).toBeDefined(); + expect(def!.valueType).toBeDefined(); + } +}); + +tap.test('should handle all RFC 2866 accounting attributes', async () => { + const accountingAttributes = [ + ERadiusAttributeType.AcctStatusType, + ERadiusAttributeType.AcctDelayTime, + ERadiusAttributeType.AcctInputOctets, + ERadiusAttributeType.AcctOutputOctets, + ERadiusAttributeType.AcctSessionId, + ERadiusAttributeType.AcctAuthentic, + ERadiusAttributeType.AcctSessionTime, + ERadiusAttributeType.AcctInputPackets, + ERadiusAttributeType.AcctOutputPackets, + ERadiusAttributeType.AcctTerminateCause, + ERadiusAttributeType.AcctMultiSessionId, + ERadiusAttributeType.AcctLinkCount, + ]; + + for (const attrType of accountingAttributes) { + const def = RadiusAttributes.getDefinition(attrType); + expect(def).toBeDefined(); + expect(def!.name).toBeDefined(); + } +}); + +export default tap.start(); diff --git a/test/server/test.authenticator.ts b/test/server/test.authenticator.ts new file mode 100644 index 0000000..c075801 --- /dev/null +++ b/test/server/test.authenticator.ts @@ -0,0 +1,205 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import * as crypto from 'crypto'; +import { + RadiusAuthenticator, + RadiusPacket, + ERadiusCode, + ERadiusAttributeType, +} from '../../ts_server/index.js'; + +tap.test('should generate 16-byte random request authenticator', async () => { + const auth1 = RadiusAuthenticator.generateRequestAuthenticator(); + const auth2 = RadiusAuthenticator.generateRequestAuthenticator(); + + expect(auth1.length).toEqual(16); + expect(auth2.length).toEqual(16); + + // Should be random (different each time) + expect(auth1.equals(auth2)).toBeFalsy(); +}); + +tap.test('should calculate response authenticator correctly', async () => { + const code = ERadiusCode.AccessAccept; + const identifier = 1; + const requestAuthenticator = crypto.randomBytes(16); + const attributes = Buffer.from([]); + const secret = 'testing123'; + + const responseAuth = RadiusAuthenticator.calculateResponseAuthenticator( + code, + identifier, + requestAuthenticator, + attributes, + secret + ); + + expect(responseAuth.length).toEqual(16); + + // Verify by recalculating + const verified = RadiusAuthenticator.calculateResponseAuthenticator( + code, + identifier, + requestAuthenticator, + attributes, + secret + ); + + expect(responseAuth.equals(verified)).toBeTruthy(); +}); + +tap.test('should calculate accounting request authenticator', async () => { + const code = ERadiusCode.AccountingRequest; + const identifier = 1; + const attributes = Buffer.from([]); + const secret = 'testing123'; + + const acctAuth = RadiusAuthenticator.calculateAccountingRequestAuthenticator( + code, + identifier, + attributes, + secret + ); + + expect(acctAuth.length).toEqual(16); +}); + +tap.test('should verify accounting request authenticator', async () => { + const secret = 'testing123'; + + // Create an accounting request packet + const packet = RadiusPacket.createAccountingRequest(1, secret, [ + { type: ERadiusAttributeType.AcctStatusType, value: 1 }, // Start + { type: ERadiusAttributeType.AcctSessionId, value: 'session-123' }, + ]); + + // Verify the authenticator + const isValid = RadiusAuthenticator.verifyAccountingRequestAuthenticator(packet, secret); + expect(isValid).toBeTruthy(); + + // Should fail with wrong secret + const isInvalid = RadiusAuthenticator.verifyAccountingRequestAuthenticator(packet, 'wrongsecret'); + expect(isInvalid).toBeFalsy(); +}); + +tap.test('should verify response authenticator', async () => { + const identifier = 1; + const requestAuthenticator = crypto.randomBytes(16); + const secret = 'testing123'; + + // Create a response packet + const responsePacket = RadiusPacket.createAccessAccept( + identifier, + requestAuthenticator, + secret, + [{ type: ERadiusAttributeType.ReplyMessage, value: 'Welcome' }] + ); + + // Verify the authenticator + const isValid = RadiusAuthenticator.verifyResponseAuthenticator( + responsePacket, + requestAuthenticator, + secret + ); + expect(isValid).toBeTruthy(); + + // Should fail with wrong request authenticator + const wrongRequestAuth = crypto.randomBytes(16); + const isInvalid = RadiusAuthenticator.verifyResponseAuthenticator( + responsePacket, + wrongRequestAuth, + secret + ); + expect(isInvalid).toBeFalsy(); + + // Should fail with wrong secret + const isInvalid2 = RadiusAuthenticator.verifyResponseAuthenticator( + responsePacket, + requestAuthenticator, + 'wrongsecret' + ); + expect(isInvalid2).toBeFalsy(); +}); + +tap.test('should create packet header', async () => { + const code = ERadiusCode.AccessRequest; + const identifier = 42; + const authenticator = crypto.randomBytes(16); + const attributesLength = 50; + + const header = RadiusAuthenticator.createPacketHeader( + code, + identifier, + authenticator, + attributesLength + ); + + expect(header.length).toEqual(20); + expect(header[0]).toEqual(code); + expect(header[1]).toEqual(identifier); + expect(header.readUInt16BE(2)).toEqual(20 + attributesLength); + expect(header.subarray(4, 20).equals(authenticator)).toBeTruthy(); +}); + +tap.test('should calculate CHAP response', async () => { + const chapId = 1; + const password = 'testpassword'; + const challenge = crypto.randomBytes(16); + + const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge); + + expect(response.length).toEqual(16); + + // Verify the calculation: MD5(CHAP-ID + Password + Challenge) + const md5 = crypto.createHash('md5'); + md5.update(Buffer.from([chapId])); + md5.update(Buffer.from(password, 'utf8')); + md5.update(challenge); + const expected = md5.digest(); + + expect(response.equals(expected)).toBeTruthy(); +}); + +tap.test('should verify CHAP response', async () => { + const chapId = 1; + const password = 'testpassword'; + const challenge = crypto.randomBytes(16); + + // Calculate the response + const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge); + + // Create CHAP-Password attribute format: CHAP-ID (1 byte) + Response (16 bytes) + const chapPassword = Buffer.allocUnsafe(17); + chapPassword.writeUInt8(chapId, 0); + response.copy(chapPassword, 1); + + // Verify with correct password + const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password); + expect(isValid).toBeTruthy(); + + // Verify with wrong password + const isInvalid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, 'wrongpassword'); + expect(isInvalid).toBeFalsy(); +}); + +tap.test('should calculate Message-Authenticator (HMAC-MD5)', async () => { + const packet = Buffer.alloc(50); + packet[0] = ERadiusCode.AccessRequest; + packet[1] = 1; + packet.writeUInt16BE(50, 2); + crypto.randomBytes(16).copy(packet, 4); + + const secret = 'testing123'; + + const msgAuth = RadiusAuthenticator.calculateMessageAuthenticator(packet, secret); + + expect(msgAuth.length).toEqual(16); + + // Verify it's HMAC-MD5 + const hmac = crypto.createHmac('md5', Buffer.from(secret, 'utf8')); + hmac.update(packet); + const expected = hmac.digest(); + + expect(msgAuth.equals(expected)).toBeTruthy(); +}); + +export default tap.start(); diff --git a/test/server/test.chap.ts b/test/server/test.chap.ts new file mode 100644 index 0000000..5204a5c --- /dev/null +++ b/test/server/test.chap.ts @@ -0,0 +1,209 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import * as crypto from 'crypto'; +import { RadiusAuthenticator } from '../../ts_server/index.js'; + +tap.test('should calculate CHAP response per RFC', async () => { + // CHAP-Response = MD5(CHAP-ID + Password + Challenge) + const chapId = 42; + const password = 'mypassword'; + const challenge = crypto.randomBytes(16); + + const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge); + + // Verify manually + const md5 = crypto.createHash('md5'); + md5.update(Buffer.from([chapId])); + md5.update(Buffer.from(password, 'utf8')); + md5.update(challenge); + const expected = md5.digest(); + + expect(response.length).toEqual(16); + expect(response.equals(expected)).toBeTruthy(); +}); + +tap.test('should verify valid CHAP response', async () => { + const chapId = 1; + const password = 'correctpassword'; + const challenge = crypto.randomBytes(16); + + // Calculate the expected response + const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge); + + // Build CHAP-Password attribute: CHAP Ident (1 byte) + Response (16 bytes) + const chapPassword = Buffer.allocUnsafe(17); + chapPassword.writeUInt8(chapId, 0); + response.copy(chapPassword, 1); + + // Should verify with correct password + const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password); + expect(isValid).toBeTruthy(); +}); + +tap.test('should reject CHAP response with wrong password', async () => { + const chapId = 1; + const correctPassword = 'correctpassword'; + const wrongPassword = 'wrongpassword'; + const challenge = crypto.randomBytes(16); + + // Calculate response with correct password + const response = RadiusAuthenticator.calculateChapResponse(chapId, correctPassword, challenge); + + // Build CHAP-Password attribute + const chapPassword = Buffer.allocUnsafe(17); + chapPassword.writeUInt8(chapId, 0); + response.copy(chapPassword, 1); + + // Should NOT verify with wrong password + const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, wrongPassword); + expect(isValid).toBeFalsy(); +}); + +tap.test('should reject CHAP response with wrong challenge', async () => { + const chapId = 1; + const password = 'mypassword'; + const correctChallenge = crypto.randomBytes(16); + const wrongChallenge = crypto.randomBytes(16); + + // Calculate response with correct challenge + const response = RadiusAuthenticator.calculateChapResponse(chapId, password, correctChallenge); + + // Build CHAP-Password attribute + const chapPassword = Buffer.allocUnsafe(17); + chapPassword.writeUInt8(chapId, 0); + response.copy(chapPassword, 1); + + // Should NOT verify with wrong challenge + const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, wrongChallenge, password); + expect(isValid).toBeFalsy(); +}); + +tap.test('should reject CHAP response with wrong identifier', async () => { + const correctChapId = 1; + const wrongChapId = 2; + const password = 'mypassword'; + const challenge = crypto.randomBytes(16); + + // Calculate response with correct CHAP ID + const response = RadiusAuthenticator.calculateChapResponse(correctChapId, password, challenge); + + // Build CHAP-Password with WRONG CHAP ID + const chapPassword = Buffer.allocUnsafe(17); + chapPassword.writeUInt8(wrongChapId, 0); // Wrong ID + response.copy(chapPassword, 1); + + // Should NOT verify + const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password); + expect(isValid).toBeFalsy(); +}); + +tap.test('should reject invalid CHAP-Password length', async () => { + const challenge = crypto.randomBytes(16); + const password = 'mypassword'; + + // CHAP-Password must be exactly 17 bytes (1 + 16) + const invalidChapPassword = Buffer.alloc(16); // Too short + + const isValid = RadiusAuthenticator.verifyChapResponse(invalidChapPassword, challenge, password); + expect(isValid).toBeFalsy(); + + const tooLongChapPassword = Buffer.alloc(18); // Too long + const isValid2 = RadiusAuthenticator.verifyChapResponse(tooLongChapPassword, challenge, password); + expect(isValid2).toBeFalsy(); +}); + +tap.test('should handle all CHAP ID values (0-255)', async () => { + const password = 'testpassword'; + const challenge = crypto.randomBytes(16); + + for (const chapId of [0, 1, 127, 128, 254, 255]) { + const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge); + + const chapPassword = Buffer.allocUnsafe(17); + chapPassword.writeUInt8(chapId, 0); + response.copy(chapPassword, 1); + + const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password); + expect(isValid).toBeTruthy(); + } +}); + +tap.test('should handle special characters in password', async () => { + const chapId = 1; + const password = '!@#$%^&*()_+-=[]{}|;:,.<>?~`'; + const challenge = crypto.randomBytes(16); + + const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge); + + const chapPassword = Buffer.allocUnsafe(17); + chapPassword.writeUInt8(chapId, 0); + response.copy(chapPassword, 1); + + const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password); + expect(isValid).toBeTruthy(); +}); + +tap.test('should handle unicode password', async () => { + const chapId = 1; + const password = 'ๅฏ†็ ใƒ‘ใ‚นใƒฏใƒผใƒ‰'; // Chinese + Japanese + const challenge = crypto.randomBytes(16); + + const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge); + + const chapPassword = Buffer.allocUnsafe(17); + chapPassword.writeUInt8(chapId, 0); + response.copy(chapPassword, 1); + + const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password); + expect(isValid).toBeTruthy(); +}); + +tap.test('should handle empty password', async () => { + const chapId = 1; + const password = ''; + const challenge = crypto.randomBytes(16); + + const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge); + + const chapPassword = Buffer.allocUnsafe(17); + chapPassword.writeUInt8(chapId, 0); + response.copy(chapPassword, 1); + + const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password); + expect(isValid).toBeTruthy(); +}); + +tap.test('should handle very long password', async () => { + const chapId = 1; + const password = 'a'.repeat(1000); // Very long password + const challenge = crypto.randomBytes(16); + + const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge); + + const chapPassword = Buffer.allocUnsafe(17); + chapPassword.writeUInt8(chapId, 0); + response.copy(chapPassword, 1); + + const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password); + expect(isValid).toBeTruthy(); +}); + +tap.test('should handle different challenge lengths', async () => { + const chapId = 1; + const password = 'testpassword'; + + // CHAP challenge can be various lengths + for (const length of [8, 16, 24, 32]) { + const challenge = crypto.randomBytes(length); + + const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge); + + const chapPassword = Buffer.allocUnsafe(17); + chapPassword.writeUInt8(chapId, 0); + response.copy(chapPassword, 1); + + const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password); + expect(isValid).toBeTruthy(); + } +}); + +export default tap.start(); diff --git a/test/server/test.packet.ts b/test/server/test.packet.ts new file mode 100644 index 0000000..7f461eb --- /dev/null +++ b/test/server/test.packet.ts @@ -0,0 +1,190 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import { + RadiusPacket, + ERadiusCode, + ERadiusAttributeType, +} from '../../ts_server/index.js'; + +tap.test('should encode and decode a basic Access-Request packet', async () => { + const identifier = 42; + const secret = 'testing123'; + const attributes = [ + { type: ERadiusAttributeType.UserName, value: 'testuser' }, + { type: ERadiusAttributeType.UserPassword, value: 'testpass' }, + { type: ERadiusAttributeType.NasIpAddress, value: '192.168.1.1' }, + { type: ERadiusAttributeType.NasPort, value: 1 }, + ]; + + const packet = RadiusPacket.createAccessRequest(identifier, secret, attributes); + + // Verify minimum packet size (20 bytes header) + expect(packet.length).toBeGreaterThanOrEqual(RadiusPacket.MIN_PACKET_SIZE); + + // Verify maximum packet size + expect(packet.length).toBeLessThanOrEqual(RadiusPacket.MAX_PACKET_SIZE); + + // Verify header + expect(packet[0]).toEqual(ERadiusCode.AccessRequest); + expect(packet[1]).toEqual(identifier); + + // Decode the packet + const decoded = RadiusPacket.decode(packet); + expect(decoded.code).toEqual(ERadiusCode.AccessRequest); + expect(decoded.identifier).toEqual(identifier); + expect(decoded.authenticator.length).toEqual(16); + expect(decoded.attributes.length).toBeGreaterThan(0); +}); + +tap.test('should handle packet length validation', async () => { + // Packet too short + const shortPacket = Buffer.alloc(19); + let error: Error | undefined; + try { + RadiusPacket.decode(shortPacket); + } catch (e) { + error = e as Error; + } + expect(error).toBeDefined(); + expect(error!.message).toInclude('too short'); +}); + +tap.test('should handle invalid length in header', async () => { + const packet = Buffer.alloc(20); + packet[0] = ERadiusCode.AccessRequest; + packet[1] = 1; // identifier + packet.writeUInt16BE(10, 2); // length too small + + let error: Error | undefined; + try { + RadiusPacket.decode(packet); + } catch (e) { + error = e as Error; + } + expect(error).toBeDefined(); +}); + +tap.test('should handle maximum packet size', async () => { + const secret = 'testing123'; + const identifier = 1; + + // Create a packet that would exceed max size + const hugeAttributes: Array<{ type: number; value: Buffer }> = []; + // Each attribute can be max 255 bytes. Create enough to exceed 4096 + for (let i = 0; i < 20; i++) { + hugeAttributes.push({ + type: ERadiusAttributeType.ReplyMessage, + value: Buffer.alloc(250, 65), // 250 bytes of 'A' + }); + } + + // This should throw because packet would be too large + let error: Error | undefined; + try { + // Manually build the packet to test size limit + const rawAttrs = hugeAttributes.map((a) => ({ + type: a.type, + value: a.value, + })); + + RadiusPacket.encode({ + code: ERadiusCode.AccessRequest, + identifier, + authenticator: Buffer.alloc(16), + attributes: rawAttrs, + }); + } catch (e) { + error = e as Error; + } + expect(error).toBeDefined(); + expect(error!.message).toInclude('too large'); +}); + +tap.test('should parse and encode attributes correctly', async () => { + const secret = 'testing123'; + const identifier = 1; + + // Create packet with various attribute types + const packet = RadiusPacket.createAccessRequest(identifier, secret, [ + { type: 'User-Name', value: 'john.doe' }, // text + { type: 'NAS-IP-Address', value: '10.0.0.1' }, // address + { type: 'NAS-Port', value: 5060 }, // integer + { type: 'NAS-Identifier', value: 'nas01.example.com' }, // text + ]); + + const decoded = RadiusPacket.decodeAndParse(packet); + + // Find username + const usernameAttr = decoded.parsedAttributes.find( + (a) => a.type === ERadiusAttributeType.UserName + ); + expect(usernameAttr).toBeDefined(); + expect(usernameAttr!.value).toEqual('john.doe'); + + // Find NAS-IP-Address + const nasIpAttr = decoded.parsedAttributes.find( + (a) => a.type === ERadiusAttributeType.NasIpAddress + ); + expect(nasIpAttr).toBeDefined(); + expect(nasIpAttr!.value).toEqual('10.0.0.1'); + + // Find NAS-Port + const nasPortAttr = decoded.parsedAttributes.find( + (a) => a.type === ERadiusAttributeType.NasPort + ); + expect(nasPortAttr).toBeDefined(); + expect(nasPortAttr!.value).toEqual(5060); +}); + +tap.test('should create Access-Accept packet', async () => { + const identifier = 1; + const requestAuth = Buffer.alloc(16); + const secret = 'testing123'; + + const packet = RadiusPacket.createAccessAccept(identifier, requestAuth, secret, [ + { type: ERadiusAttributeType.ReplyMessage, value: 'Welcome!' }, + { type: ERadiusAttributeType.SessionTimeout, value: 3600 }, + ]); + + const decoded = RadiusPacket.decode(packet); + expect(decoded.code).toEqual(ERadiusCode.AccessAccept); + expect(decoded.identifier).toEqual(identifier); +}); + +tap.test('should create Access-Reject packet', async () => { + const identifier = 1; + const requestAuth = Buffer.alloc(16); + const secret = 'testing123'; + + const packet = RadiusPacket.createAccessReject(identifier, requestAuth, secret, [ + { type: ERadiusAttributeType.ReplyMessage, value: 'Invalid credentials' }, + ]); + + const decoded = RadiusPacket.decode(packet); + expect(decoded.code).toEqual(ERadiusCode.AccessReject); +}); + +tap.test('should create Access-Challenge packet', async () => { + const identifier = 1; + const requestAuth = Buffer.alloc(16); + const secret = 'testing123'; + const state = Buffer.from('challenge-state-123'); + + const packet = RadiusPacket.createAccessChallenge(identifier, requestAuth, secret, [ + { type: ERadiusAttributeType.ReplyMessage, value: 'Enter OTP' }, + { type: ERadiusAttributeType.State, value: state }, + ]); + + const decoded = RadiusPacket.decode(packet); + expect(decoded.code).toEqual(ERadiusCode.AccessChallenge); +}); + +tap.test('should get code name', async () => { + expect(RadiusPacket.getCodeName(ERadiusCode.AccessRequest)).toEqual('Access-Request'); + expect(RadiusPacket.getCodeName(ERadiusCode.AccessAccept)).toEqual('Access-Accept'); + expect(RadiusPacket.getCodeName(ERadiusCode.AccessReject)).toEqual('Access-Reject'); + expect(RadiusPacket.getCodeName(ERadiusCode.AccountingRequest)).toEqual('Accounting-Request'); + expect(RadiusPacket.getCodeName(ERadiusCode.AccountingResponse)).toEqual('Accounting-Response'); + expect(RadiusPacket.getCodeName(ERadiusCode.AccessChallenge)).toEqual('Access-Challenge'); +}); + +export default tap.start(); diff --git a/test/server/test.pap.ts b/test/server/test.pap.ts new file mode 100644 index 0000000..12bbd42 --- /dev/null +++ b/test/server/test.pap.ts @@ -0,0 +1,282 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import * as crypto from 'crypto'; +import { RadiusAuthenticator } from '../../ts_server/index.js'; + +tap.test('should encrypt and decrypt short password (< 16 chars)', async () => { + const password = 'secret'; + const requestAuthenticator = crypto.randomBytes(16); + const secret = 'testing123'; + + const encrypted = RadiusAuthenticator.encryptPassword( + password, + requestAuthenticator, + secret + ); + + // Encrypted length should be multiple of 16 + expect(encrypted.length).toEqual(16); + + const decrypted = RadiusAuthenticator.decryptPassword( + encrypted, + requestAuthenticator, + secret + ); + + expect(decrypted).toEqual(password); +}); + +tap.test('should encrypt and decrypt exactly 16-char password', async () => { + const password = '1234567890123456'; // Exactly 16 characters + const requestAuthenticator = crypto.randomBytes(16); + const secret = 'testing123'; + + const encrypted = RadiusAuthenticator.encryptPassword( + password, + requestAuthenticator, + secret + ); + + expect(encrypted.length).toEqual(16); + + const decrypted = RadiusAuthenticator.decryptPassword( + encrypted, + requestAuthenticator, + secret + ); + + expect(decrypted).toEqual(password); +}); + +tap.test('should encrypt and decrypt long password (> 16 chars)', async () => { + const password = 'thisisaverylongpasswordthatexceeds16characters'; + const requestAuthenticator = crypto.randomBytes(16); + const secret = 'testing123'; + + const encrypted = RadiusAuthenticator.encryptPassword( + password, + requestAuthenticator, + secret + ); + + // Should be padded to next multiple of 16 + expect(encrypted.length % 16).toEqual(0); + expect(encrypted.length).toBeGreaterThan(16); + + const decrypted = RadiusAuthenticator.decryptPassword( + encrypted, + requestAuthenticator, + secret + ); + + expect(decrypted).toEqual(password); +}); + +tap.test('should encrypt and decrypt password near max length (128 chars)', async () => { + const password = 'a'.repeat(120); // Near max of 128 + const requestAuthenticator = crypto.randomBytes(16); + const secret = 'testing123'; + + const encrypted = RadiusAuthenticator.encryptPassword( + password, + requestAuthenticator, + secret + ); + + // Should be limited to 128 bytes + expect(encrypted.length).toBeLessThanOrEqual(128); + + const decrypted = RadiusAuthenticator.decryptPassword( + encrypted, + requestAuthenticator, + secret + ); + + expect(decrypted).toEqual(password); +}); + +tap.test('should truncate password exceeding 128 chars', async () => { + const password = 'a'.repeat(150); // Exceeds max + const requestAuthenticator = crypto.randomBytes(16); + const secret = 'testing123'; + + const encrypted = RadiusAuthenticator.encryptPassword( + password, + requestAuthenticator, + secret + ); + + // Should be exactly 128 bytes (max) + expect(encrypted.length).toEqual(128); + + // Decrypted will be truncated to 128 chars + const decrypted = RadiusAuthenticator.decryptPassword( + encrypted, + requestAuthenticator, + secret + ); + + expect(decrypted.length).toBeLessThanOrEqual(128); +}); + +tap.test('should handle empty password', async () => { + const password = ''; + const requestAuthenticator = crypto.randomBytes(16); + const secret = 'testing123'; + + const encrypted = RadiusAuthenticator.encryptPassword( + password, + requestAuthenticator, + secret + ); + + // Even empty password is padded to 16 bytes + expect(encrypted.length).toEqual(16); + + const decrypted = RadiusAuthenticator.decryptPassword( + encrypted, + requestAuthenticator, + secret + ); + + expect(decrypted).toEqual(password); +}); + +tap.test('should fail to decrypt with wrong secret', async () => { + const password = 'mypassword'; + const requestAuthenticator = crypto.randomBytes(16); + const secret = 'testing123'; + const wrongSecret = 'wrongsecret'; + + const encrypted = RadiusAuthenticator.encryptPassword( + password, + requestAuthenticator, + secret + ); + + const decrypted = RadiusAuthenticator.decryptPassword( + encrypted, + requestAuthenticator, + wrongSecret + ); + + // Should not match original password + expect(decrypted).not.toEqual(password); +}); + +tap.test('should fail to decrypt with wrong authenticator', async () => { + const password = 'mypassword'; + const requestAuthenticator = crypto.randomBytes(16); + const wrongAuthenticator = crypto.randomBytes(16); + const secret = 'testing123'; + + const encrypted = RadiusAuthenticator.encryptPassword( + password, + requestAuthenticator, + secret + ); + + const decrypted = RadiusAuthenticator.decryptPassword( + encrypted, + wrongAuthenticator, + secret + ); + + // Should not match original password + expect(decrypted).not.toEqual(password); +}); + +tap.test('should handle special characters in password', async () => { + const password = '!@#$%^&*()_+-=[]{}|;:,.<>?'; + const requestAuthenticator = crypto.randomBytes(16); + const secret = 'testing123'; + + const encrypted = RadiusAuthenticator.encryptPassword( + password, + requestAuthenticator, + secret + ); + + const decrypted = RadiusAuthenticator.decryptPassword( + encrypted, + requestAuthenticator, + secret + ); + + expect(decrypted).toEqual(password); +}); + +tap.test('should handle unicode characters in password', async () => { + const password = 'ใƒ‘ใ‚นใƒฏใƒผใƒ‰ๅฏ†็ '; // Japanese + Chinese + const requestAuthenticator = crypto.randomBytes(16); + const secret = 'testing123'; + + const encrypted = RadiusAuthenticator.encryptPassword( + password, + requestAuthenticator, + secret + ); + + const decrypted = RadiusAuthenticator.decryptPassword( + encrypted, + requestAuthenticator, + secret + ); + + expect(decrypted).toEqual(password); +}); + +tap.test('should reject invalid encrypted password length', async () => { + const requestAuthenticator = crypto.randomBytes(16); + const secret = 'testing123'; + + // Not a multiple of 16 + const invalidEncrypted = Buffer.alloc(15); + + let error: Error | undefined; + try { + RadiusAuthenticator.decryptPassword(invalidEncrypted, requestAuthenticator, secret); + } catch (e) { + error = e as Error; + } + + expect(error).toBeDefined(); + expect(error!.message).toInclude('Invalid'); +}); + +tap.test('PAP encryption matches RFC 2865 algorithm', async () => { + // Test that our implementation matches the RFC 2865 algorithm: + // b1 = MD5(S + RA) c(1) = p1 xor b1 + // b2 = MD5(S + c(1)) c(2) = p2 xor b2 + + const password = 'testpassword12345678'; // Longer than 16 to test chaining + const requestAuth = Buffer.alloc(16, 0x42); // Fixed for deterministic test + const secret = 'sharedsecret'; + + // Our implementation + const encrypted = RadiusAuthenticator.encryptPassword(password, requestAuth, secret); + + // Manual calculation per RFC + const paddedLength = Math.ceil(password.length / 16) * 16; + const padded = Buffer.alloc(paddedLength, 0); + Buffer.from(password, 'utf8').copy(padded); + + const expected = Buffer.alloc(paddedLength); + let previousCipher = requestAuth; + + for (let i = 0; i < paddedLength; i += 16) { + const md5 = crypto.createHash('md5'); + md5.update(Buffer.from(secret, 'utf8')); + md5.update(previousCipher); + const b = md5.digest(); + + for (let j = 0; j < 16; j++) { + expected[i + j] = padded[i + j] ^ b[j]; + } + + previousCipher = expected.subarray(i, i + 16); + } + + expect(encrypted.equals(expected)).toBeTruthy(); +}); + +export default tap.start(); diff --git a/test/test.ts b/test/test.ts deleted file mode 100644 index ab71788..0000000 --- a/test/test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import * as smartradius from '../ts/index.js' - -tap.test('first test', async () => { - console.log(smartradius) -}) - -export default tap.start() diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts new file mode 100644 index 0000000..0b57764 --- /dev/null +++ b/ts/00_commitinfo_data.ts @@ -0,0 +1,8 @@ +/** + * autocreated commitinfo by @push.rocks/commitinfo + */ +export const commitinfo = { + name: '@push.rocks/smartradius', + version: '1.1.0', + description: 'A RADIUS server and client implementation for Node.js with full RFC 2865/2866 compliance' +} diff --git a/ts/index.ts b/ts/index.ts index 8f1f224..03b863d 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -1,3 +1,19 @@ -import * as plugins from './plugins.js'; +// @push.rocks/smartradius +// RADIUS Server and Client Library +// Implements RFC 2865 (Authentication) and RFC 2866 (Accounting) -export let demoExport = 'Hi there! :) This is an exported string'; +// Re-export shared protocol definitions +export * from '../ts_shared/index.js'; + +// Re-export server module +export * from '../ts_server/index.js'; + +// Re-export client module +export { RadiusClient } from '../ts_client/index.js'; +export type { + IRadiusClientOptions, + IClientAuthRequest, + IClientAuthResponse, + IClientAccountingRequest, + IClientAccountingResponse, +} from '../ts_client/index.js'; diff --git a/ts/paths.ts b/ts/paths.ts deleted file mode 100644 index 5396408..0000000 --- a/ts/paths.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as plugins from './plugins.js'; -export const packageDir = plugins.path.join( - plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url), - '../' -); \ No newline at end of file diff --git a/ts/plugins.ts b/ts/plugins.ts deleted file mode 100644 index a7c8cf2..0000000 --- a/ts/plugins.ts +++ /dev/null @@ -1,9 +0,0 @@ -// native scope -import * as path from 'path'; - -export { path }; - -// @push.rocks scope -import * as smartpath from '@push.rocks/smartpath'; - -export { smartpath }; diff --git a/ts/readme.md b/ts/readme.md new file mode 100644 index 0000000..104d231 --- /dev/null +++ b/ts/readme.md @@ -0,0 +1,93 @@ +# @push.rocks/smartradius + +> ๐Ÿ” Complete RADIUS Server and Client Library for Node.js + +## Overview + +This is the main entry point for the smartradius library. It re-exports all functionality from the server, client, and shared modules, providing a unified API for RADIUS operations. + +## What Gets Exported + +This module combines exports from: + +- **ts_shared** - Protocol definitions, enums, and core interfaces +- **ts_server** - Server implementation and utilities +- **ts_client** - Client implementation + +## Quick Import + +```typescript +// Everything from one import +import { + // Server + RadiusServer, + RadiusPacket, + RadiusAttributes, + RadiusAuthenticator, + + // Client + RadiusClient, + + // Shared Enums + ERadiusCode, + ERadiusAttributeType, + EAcctStatusType, + EAcctTerminateCause, + + // Shared Interfaces + IRadiusPacket, + IParsedAttribute, + + // Server Interfaces + IRadiusServerOptions, + IAuthenticationRequest, + IAuthenticationResponse, + + // Client Interfaces + IRadiusClientOptions, + IClientAuthRequest, + IClientAuthResponse, +} from '@push.rocks/smartradius'; +``` + +## Sub-modules + +For more targeted imports, see the individual module documentation: + +| Module | Description | +|--------|-------------| +| [ts_shared](../ts_shared/readme.md) | Protocol definitions and enums | +| [ts_server](../ts_server/readme.md) | Server implementation | +| [ts_client](../ts_client/readme.md) | Client implementation | + +## Usage Example + +```typescript +import { RadiusServer, RadiusClient, ERadiusCode } from '@push.rocks/smartradius'; + +// Create server +const server = new RadiusServer({ + defaultSecret: 'testing123', + authenticationHandler: async (req) => ({ + code: req.password === 'secret' + ? ERadiusCode.AccessAccept + : ERadiusCode.AccessReject + }), +}); +await server.start(); + +// Create client +const client = new RadiusClient({ + host: '127.0.0.1', + secret: 'testing123', +}); +await client.connect(); + +// Authenticate +const result = await client.authenticatePap('user', 'secret'); +console.log('Accepted:', result.accepted); + +// Cleanup +await client.disconnect(); +await server.stop(); +``` diff --git a/ts/tspublish.json b/ts/tspublish.json new file mode 100644 index 0000000..199510f --- /dev/null +++ b/ts/tspublish.json @@ -0,0 +1 @@ +{ "order": 4 } diff --git a/ts_client/classes.radiusclient.ts b/ts_client/classes.radiusclient.ts new file mode 100644 index 0000000..1c39b02 --- /dev/null +++ b/ts_client/classes.radiusclient.ts @@ -0,0 +1,531 @@ +import * as plugins from './plugins.js'; +import type { + IRadiusClientOptions, + IClientAuthRequest, + IClientAuthResponse, + IClientAccountingRequest, + IClientAccountingResponse, +} from './interfaces.js'; +import { + RadiusPacket, + RadiusAuthenticator, + RadiusAttributes, + ERadiusCode, + ERadiusAttributeType, +} from '../ts_server/index.js'; + +/** + * Pending request tracking + */ +interface IPendingRequest { + identifier: number; + requestAuthenticator: Buffer; + resolve: (response: Buffer) => void; + reject: (error: Error) => void; + timeoutId?: ReturnType; + retries: number; + packet: Buffer; + port: number; +} + +/** + * RADIUS Client implementation + * Supports PAP, CHAP authentication and accounting + */ +export class RadiusClient { + private socket?: plugins.dgram.Socket; + private readonly options: Required; + private currentIdentifier = 0; + private readonly pendingRequests: Map = new Map(); + private isConnected = false; + + constructor(options: IRadiusClientOptions) { + this.options = { + host: options.host, + authPort: options.authPort ?? 1812, + acctPort: options.acctPort ?? 1813, + secret: options.secret, + timeout: options.timeout ?? 5000, + retries: options.retries ?? 3, + retryDelay: options.retryDelay ?? 1000, + nasIpAddress: options.nasIpAddress ?? '0.0.0.0', + nasIdentifier: options.nasIdentifier ?? 'smartradius-client', + }; + } + + /** + * Connect the client (bind UDP socket) + */ + public async connect(): Promise { + if (this.isConnected) { + return; + } + + return new Promise((resolve, reject) => { + this.socket = plugins.dgram.createSocket('udp4'); + + this.socket.on('error', (err) => { + if (!this.isConnected) { + reject(err); + } + }); + + this.socket.on('message', (msg) => { + this.handleResponse(msg); + }); + + this.socket.bind(0, () => { + this.isConnected = true; + resolve(); + }); + }); + } + + /** + * Disconnect the client + */ + public async disconnect(): Promise { + if (!this.isConnected || !this.socket) { + return; + } + + // Reject all pending requests + for (const [, pending] of this.pendingRequests) { + if (pending.timeoutId) { + clearTimeout(pending.timeoutId); + } + pending.reject(new Error('Client disconnected')); + } + this.pendingRequests.clear(); + + return new Promise((resolve) => { + this.socket!.close(() => { + this.socket = undefined; + this.isConnected = false; + resolve(); + }); + }); + } + + /** + * Authenticate a user using PAP + */ + public async authenticatePap(username: string, password: string): Promise { + return this.authenticate({ + username, + password, + }); + } + + /** + * Authenticate a user using CHAP + */ + public async authenticateChap( + username: string, + password: string, + challenge?: Buffer + ): Promise { + // Generate challenge if not provided + const chapChallenge = challenge || plugins.crypto.randomBytes(16); + const chapId = Math.floor(Math.random() * 256); + + // Calculate CHAP response + const chapResponse = RadiusAuthenticator.calculateChapResponse(chapId, password, chapChallenge); + + // CHAP-Password = CHAP Ident (1 byte) + Response (16 bytes) + const chapPassword = Buffer.allocUnsafe(17); + chapPassword.writeUInt8(chapId, 0); + chapResponse.copy(chapPassword, 1); + + return this.authenticate({ + username, + chapPassword, + chapChallenge, + }); + } + + /** + * Send an authentication request + */ + public async authenticate(request: IClientAuthRequest): Promise { + await this.ensureConnected(); + + const identifier = this.nextIdentifier(); + const attributes: Array<{ type: number | string; value: string | number | Buffer }> = []; + + // Add User-Name + attributes.push({ type: ERadiusAttributeType.UserName, value: request.username }); + + // Add NAS-IP-Address or NAS-Identifier + if (this.options.nasIpAddress && this.options.nasIpAddress !== '0.0.0.0') { + attributes.push({ type: ERadiusAttributeType.NasIpAddress, value: this.options.nasIpAddress }); + } + if (this.options.nasIdentifier) { + attributes.push({ type: ERadiusAttributeType.NasIdentifier, value: this.options.nasIdentifier }); + } + + // Add PAP password or CHAP credentials + if (request.password !== undefined) { + // PAP - password will be encrypted in createAccessRequest + attributes.push({ type: ERadiusAttributeType.UserPassword, value: request.password }); + } else if (request.chapPassword) { + // CHAP + attributes.push({ type: ERadiusAttributeType.ChapPassword, value: request.chapPassword }); + if (request.chapChallenge) { + attributes.push({ type: ERadiusAttributeType.ChapChallenge, value: request.chapChallenge }); + } + } + + // Add optional attributes + if (request.nasPort !== undefined) { + attributes.push({ type: ERadiusAttributeType.NasPort, value: request.nasPort }); + } + if (request.nasPortType !== undefined) { + attributes.push({ type: ERadiusAttributeType.NasPortType, value: request.nasPortType }); + } + if (request.serviceType !== undefined) { + attributes.push({ type: ERadiusAttributeType.ServiceType, value: request.serviceType }); + } + if (request.calledStationId) { + attributes.push({ type: ERadiusAttributeType.CalledStationId, value: request.calledStationId }); + } + if (request.callingStationId) { + attributes.push({ type: ERadiusAttributeType.CallingStationId, value: request.callingStationId }); + } + if (request.state) { + attributes.push({ type: ERadiusAttributeType.State, value: request.state }); + } + + // Add custom attributes + if (request.customAttributes) { + attributes.push(...request.customAttributes); + } + + // Create packet + const packet = RadiusPacket.createAccessRequest(identifier, this.options.secret, attributes); + + // Extract request authenticator from packet (bytes 4-20) + const requestAuthenticator = packet.subarray(4, 20); + + // Send and wait for response + const responseBuffer = await this.sendRequest( + identifier, + packet, + requestAuthenticator, + this.options.authPort + ); + + // Verify response authenticator + if (!RadiusAuthenticator.verifyResponseAuthenticator( + responseBuffer, + requestAuthenticator, + this.options.secret + )) { + throw new Error('Invalid response authenticator'); + } + + // Parse response + const response = RadiusPacket.decodeAndParse(responseBuffer); + + return this.buildAuthResponse(response); + } + + /** + * Send an accounting request + */ + public async accounting(request: IClientAccountingRequest): Promise { + await this.ensureConnected(); + + const identifier = this.nextIdentifier(); + const attributes: Array<{ type: number | string; value: string | number | Buffer }> = []; + + // Add required attributes + attributes.push({ type: ERadiusAttributeType.AcctStatusType, value: request.statusType }); + attributes.push({ type: ERadiusAttributeType.AcctSessionId, value: request.sessionId }); + + // Add NAS identification + if (this.options.nasIpAddress && this.options.nasIpAddress !== '0.0.0.0') { + attributes.push({ type: ERadiusAttributeType.NasIpAddress, value: this.options.nasIpAddress }); + } + if (this.options.nasIdentifier) { + attributes.push({ type: ERadiusAttributeType.NasIdentifier, value: this.options.nasIdentifier }); + } + + // Add optional attributes + if (request.username) { + attributes.push({ type: ERadiusAttributeType.UserName, value: request.username }); + } + if (request.nasPort !== undefined) { + attributes.push({ type: ERadiusAttributeType.NasPort, value: request.nasPort }); + } + if (request.nasPortType !== undefined) { + attributes.push({ type: ERadiusAttributeType.NasPortType, value: request.nasPortType }); + } + if (request.sessionTime !== undefined) { + attributes.push({ type: ERadiusAttributeType.AcctSessionTime, value: request.sessionTime }); + } + if (request.inputOctets !== undefined) { + attributes.push({ type: ERadiusAttributeType.AcctInputOctets, value: request.inputOctets }); + } + if (request.outputOctets !== undefined) { + attributes.push({ type: ERadiusAttributeType.AcctOutputOctets, value: request.outputOctets }); + } + if (request.inputPackets !== undefined) { + attributes.push({ type: ERadiusAttributeType.AcctInputPackets, value: request.inputPackets }); + } + if (request.outputPackets !== undefined) { + attributes.push({ type: ERadiusAttributeType.AcctOutputPackets, value: request.outputPackets }); + } + if (request.terminateCause !== undefined) { + attributes.push({ type: ERadiusAttributeType.AcctTerminateCause, value: request.terminateCause }); + } + if (request.calledStationId) { + attributes.push({ type: ERadiusAttributeType.CalledStationId, value: request.calledStationId }); + } + if (request.callingStationId) { + attributes.push({ type: ERadiusAttributeType.CallingStationId, value: request.callingStationId }); + } + + // Add custom attributes + if (request.customAttributes) { + attributes.push(...request.customAttributes); + } + + // Create packet + const packet = RadiusPacket.createAccountingRequest(identifier, this.options.secret, attributes); + + // Extract request authenticator from packet + const requestAuthenticator = packet.subarray(4, 20); + + // Send and wait for response + const responseBuffer = await this.sendRequest( + identifier, + packet, + requestAuthenticator, + this.options.acctPort + ); + + // Verify response authenticator + if (!RadiusAuthenticator.verifyResponseAuthenticator( + responseBuffer, + requestAuthenticator, + this.options.secret + )) { + throw new Error('Invalid response authenticator'); + } + + // Parse response + const response = RadiusPacket.decodeAndParse(responseBuffer); + + return { + success: response.code === ERadiusCode.AccountingResponse, + attributes: response.parsedAttributes, + rawPacket: response, + }; + } + + /** + * Send accounting start + */ + public async accountingStart(sessionId: string, username?: string): Promise { + const { EAcctStatusType } = await import('../ts_server/index.js'); + return this.accounting({ + statusType: EAcctStatusType.Start, + sessionId, + username, + }); + } + + /** + * Send accounting stop + */ + public async accountingStop( + sessionId: string, + options?: { + username?: string; + sessionTime?: number; + inputOctets?: number; + outputOctets?: number; + terminateCause?: number; + } + ): Promise { + const { EAcctStatusType } = await import('../ts_server/index.js'); + return this.accounting({ + statusType: EAcctStatusType.Stop, + sessionId, + ...options, + }); + } + + /** + * Send accounting interim update + */ + public async accountingUpdate( + sessionId: string, + options?: { + username?: string; + sessionTime?: number; + inputOctets?: number; + outputOctets?: number; + } + ): Promise { + const { EAcctStatusType } = await import('../ts_server/index.js'); + return this.accounting({ + statusType: EAcctStatusType.InterimUpdate, + sessionId, + ...options, + }); + } + + private async ensureConnected(): Promise { + if (!this.isConnected) { + await this.connect(); + } + } + + private nextIdentifier(): number { + this.currentIdentifier = (this.currentIdentifier + 1) % 256; + return this.currentIdentifier; + } + + private sendRequest( + identifier: number, + packet: Buffer, + requestAuthenticator: Buffer, + port: number + ): Promise { + return new Promise((resolve, reject) => { + const pending: IPendingRequest = { + identifier, + requestAuthenticator, + resolve, + reject, + retries: 0, + packet, + port, + }; + + this.pendingRequests.set(identifier, pending); + this.sendWithRetry(pending); + }); + } + + private sendWithRetry(pending: IPendingRequest): void { + // Send packet + this.socket!.send(pending.packet, pending.port, this.options.host, (err) => { + if (err) { + this.pendingRequests.delete(pending.identifier); + pending.reject(err); + return; + } + + // Set timeout + pending.timeoutId = setTimeout(() => { + pending.retries++; + if (pending.retries >= this.options.retries) { + this.pendingRequests.delete(pending.identifier); + pending.reject(new Error(`Request timed out after ${this.options.retries} retries`)); + return; + } + + // Exponential backoff + const delay = this.options.retryDelay * Math.pow(2, pending.retries - 1); + setTimeout(() => { + if (this.pendingRequests.has(pending.identifier)) { + this.sendWithRetry(pending); + } + }, delay); + }, this.options.timeout); + }); + } + + private handleResponse(msg: Buffer): void { + if (msg.length < 20) { + return; + } + + const identifier = msg.readUInt8(1); + const pending = this.pendingRequests.get(identifier); + + if (!pending) { + // Response for unknown request + return; + } + + // Clear timeout + if (pending.timeoutId) { + clearTimeout(pending.timeoutId); + } + + // Remove from pending + this.pendingRequests.delete(identifier); + + // Resolve with response + pending.resolve(msg); + } + + private buildAuthResponse(packet: any): IClientAuthResponse { + const code = packet.code as ERadiusCode; + const accepted = code === ERadiusCode.AccessAccept; + const rejected = code === ERadiusCode.AccessReject; + const challenged = code === ERadiusCode.AccessChallenge; + + // Extract common attributes + let replyMessage: string | undefined; + let sessionTimeout: number | undefined; + let idleTimeout: number | undefined; + let state: Buffer | undefined; + let classAttr: Buffer | undefined; + let framedIpAddress: string | undefined; + let framedIpNetmask: string | undefined; + const framedRoutes: string[] = []; + + for (const attr of packet.parsedAttributes) { + switch (attr.type) { + case ERadiusAttributeType.ReplyMessage: + replyMessage = attr.value as string; + break; + case ERadiusAttributeType.SessionTimeout: + sessionTimeout = attr.value as number; + break; + case ERadiusAttributeType.IdleTimeout: + idleTimeout = attr.value as number; + break; + case ERadiusAttributeType.State: + state = attr.rawValue; + break; + case ERadiusAttributeType.Class: + classAttr = attr.rawValue; + break; + case ERadiusAttributeType.FramedIpAddress: + framedIpAddress = attr.value as string; + break; + case ERadiusAttributeType.FramedIpNetmask: + framedIpNetmask = attr.value as string; + break; + case ERadiusAttributeType.FramedRoute: + framedRoutes.push(attr.value as string); + break; + } + } + + return { + code, + accepted, + rejected, + challenged, + replyMessage, + sessionTimeout, + idleTimeout, + state, + class: classAttr, + framedIpAddress, + framedIpNetmask, + framedRoutes: framedRoutes.length > 0 ? framedRoutes : undefined, + attributes: packet.parsedAttributes, + rawPacket: packet, + }; + } +} + +export default RadiusClient; diff --git a/ts_client/index.ts b/ts_client/index.ts new file mode 100644 index 0000000..3e85fde --- /dev/null +++ b/ts_client/index.ts @@ -0,0 +1,4 @@ +// RADIUS Client Module + +export * from './interfaces.js'; +export { RadiusClient } from './classes.radiusclient.js'; diff --git a/ts_client/interfaces.ts b/ts_client/interfaces.ts new file mode 100644 index 0000000..e4ea625 --- /dev/null +++ b/ts_client/interfaces.ts @@ -0,0 +1,96 @@ +/** + * RADIUS Client Interfaces + */ + +import type { + ERadiusCode, + IRadiusPacket, + IParsedAttribute, + ENasPortType, + EServiceType, + EAcctStatusType, +} from '../ts_shared/index.js'; + +// Re-export all shared types for backwards compatibility +export * from '../ts_shared/index.js'; + +/** + * RADIUS Client options + */ +export interface IRadiusClientOptions { + host: string; + authPort?: number; + acctPort?: number; + secret: string; + timeout?: number; + retries?: number; + retryDelay?: number; + nasIpAddress?: string; + nasIdentifier?: string; +} + +/** + * Authentication request for the client + */ +export interface IClientAuthRequest { + username: string; + password?: string; // For PAP + chapPassword?: Buffer; // For CHAP (CHAP Ident + Response) + chapChallenge?: Buffer; // For CHAP + nasPort?: number; + nasPortType?: ENasPortType; + serviceType?: EServiceType; + calledStationId?: string; + callingStationId?: string; + state?: Buffer; // For multi-round authentication + customAttributes?: Array<{ type: number | string; value: string | number | Buffer }>; +} + +/** + * Authentication response from the server + */ +export interface IClientAuthResponse { + code: ERadiusCode; + accepted: boolean; + rejected: boolean; + challenged: boolean; + replyMessage?: string; + sessionTimeout?: number; + idleTimeout?: number; + state?: Buffer; + class?: Buffer; + framedIpAddress?: string; + framedIpNetmask?: string; + framedRoutes?: string[]; + attributes: IParsedAttribute[]; + rawPacket: IRadiusPacket; +} + +/** + * Accounting request for the client + */ +export interface IClientAccountingRequest { + statusType: EAcctStatusType; + sessionId: string; + username?: string; + nasPort?: number; + nasPortType?: ENasPortType; + sessionTime?: number; + inputOctets?: number; + outputOctets?: number; + inputPackets?: number; + outputPackets?: number; + terminateCause?: number; + calledStationId?: string; + callingStationId?: string; + customAttributes?: Array<{ type: number | string; value: string | number | Buffer }>; +} + +/** + * Accounting response from the server + */ +export interface IClientAccountingResponse { + success: boolean; + attributes: IParsedAttribute[]; + rawPacket: IRadiusPacket; +} diff --git a/ts_client/plugins.ts b/ts_client/plugins.ts new file mode 100644 index 0000000..d443eac --- /dev/null +++ b/ts_client/plugins.ts @@ -0,0 +1,13 @@ +import * as crypto from 'crypto'; +import * as dgram from 'dgram'; + +// Import from smartpromise for deferred promises +import * as smartpromise from '@push.rocks/smartpromise'; +import * as smartdelay from '@push.rocks/smartdelay'; + +export { + crypto, + dgram, + smartpromise, + smartdelay, +}; diff --git a/ts_client/readme.md b/ts_client/readme.md new file mode 100644 index 0000000..3451c8a --- /dev/null +++ b/ts_client/readme.md @@ -0,0 +1,151 @@ +# @push.rocks/smartradius/client + +> ๐Ÿ“ฑ RADIUS Client Implementation - Connect to RADIUS servers with PAP, CHAP, and accounting support + +## Overview + +This module provides a RADIUS client implementation for connecting to RADIUS servers. It supports PAP and CHAP authentication methods, accounting operations, and includes automatic retry with exponential backoff. + +## Features + +- โœ… **PAP Authentication** - Password Authentication Protocol +- โœ… **CHAP Authentication** - Challenge-Handshake Authentication Protocol +- โœ… **Accounting** - Session start, stop, and interim updates +- โœ… **Automatic Retries** - Configurable retry count with exponential backoff +- โœ… **Timeout Handling** - Per-request timeouts +- โœ… **Custom Attributes** - Support for adding custom RADIUS attributes +- โœ… **Response Validation** - Authenticator verification for security + +## Exports + +### Classes + +| Class | Description | +|-------|-------------| +| `RadiusClient` | Main client class for RADIUS operations | + +### Interfaces (Client-Specific) + +| Interface | Description | +|-----------|-------------| +| `IRadiusClientOptions` | Client configuration options | +| `IClientAuthRequest` | Authentication request parameters | +| `IClientAuthResponse` | Authentication response from server | +| `IClientAccountingRequest` | Accounting request parameters | +| `IClientAccountingResponse` | Accounting response from server | + +## Usage + +### Basic Authentication + +```typescript +import { RadiusClient } from '@push.rocks/smartradius'; + +const client = new RadiusClient({ + host: '192.168.1.1', + secret: 'shared-secret', + timeout: 5000, + retries: 3, +}); + +await client.connect(); + +// PAP Authentication +const papResult = await client.authenticatePap('username', 'password'); +if (papResult.accepted) { + console.log('Login successful!'); + console.log('Session timeout:', papResult.sessionTimeout); +} + +// CHAP Authentication +const chapResult = await client.authenticateChap('username', 'password'); +if (chapResult.accepted) { + console.log('CHAP login successful!'); +} + +await client.disconnect(); +``` + +### Accounting + +```typescript +import { RadiusClient, EAcctStatusType } from '@push.rocks/smartradius'; + +const client = new RadiusClient({ + host: '192.168.1.1', + secret: 'shared-secret', +}); + +await client.connect(); + +// Session start +await client.accountingStart('session-123', 'username'); + +// Interim update +await client.accountingUpdate('session-123', { + username: 'username', + sessionTime: 300, + inputOctets: 1024000, + outputOctets: 2048000, +}); + +// Session stop +await client.accountingStop('session-123', { + username: 'username', + sessionTime: 600, + inputOctets: 2048000, + outputOctets: 4096000, + terminateCause: 1, // User-Request +}); + +await client.disconnect(); +``` + +### Custom Attributes + +```typescript +const result = await client.authenticate({ + username: 'user', + password: 'pass', + nasPort: 1, + calledStationId: 'AA-BB-CC-DD-EE-FF', + callingStationId: '11-22-33-44-55-66', + customAttributes: [ + { type: 'Service-Type', value: 2 }, // Framed + { type: 26, value: Buffer.from('vendor-data') }, // VSA + ], +}); +``` + +## Client Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `host` | string | *required* | RADIUS server address | +| `authPort` | number | 1812 | Authentication port | +| `acctPort` | number | 1813 | Accounting port | +| `secret` | string | *required* | Shared secret | +| `timeout` | number | 5000 | Request timeout (ms) | +| `retries` | number | 3 | Number of retries | +| `retryDelay` | number | 1000 | Base delay between retries (ms) | +| `nasIpAddress` | string | '0.0.0.0' | NAS-IP-Address attribute | +| `nasIdentifier` | string | 'smartradius-client' | NAS-Identifier attribute | + +## Response Properties + +### IClientAuthResponse + +| Property | Type | Description | +|----------|------|-------------| +| `code` | ERadiusCode | Response packet code | +| `accepted` | boolean | True if Access-Accept | +| `rejected` | boolean | True if Access-Reject | +| `challenged` | boolean | True if Access-Challenge | +| `replyMessage` | string | Reply-Message attribute | +| `sessionTimeout` | number | Session-Timeout in seconds | +| `framedIpAddress` | string | Assigned IP address | +| `attributes` | IParsedAttribute[] | All response attributes | + +## Re-exports + +This module re-exports all types from `ts_shared` for convenience. diff --git a/ts_client/tspublish.json b/ts_client/tspublish.json new file mode 100644 index 0000000..01c3b75 --- /dev/null +++ b/ts_client/tspublish.json @@ -0,0 +1 @@ +{ "order": 3 } diff --git a/ts_server/classes.radiusattributes.ts b/ts_server/classes.radiusattributes.ts new file mode 100644 index 0000000..0167bd0 --- /dev/null +++ b/ts_server/classes.radiusattributes.ts @@ -0,0 +1,303 @@ +import * as plugins from './plugins.js'; +import type { + IAttributeDefinition, + IRadiusAttribute, + IParsedAttribute, + IVendorSpecificAttribute, + TAttributeValueType, +} from './interfaces.js'; +import { ERadiusAttributeType } from './interfaces.js'; + +/** + * RADIUS Attribute Dictionary + * Based on RFC 2865 and RFC 2866 + */ +export class RadiusAttributes { + /** + * Standard RADIUS attribute definitions + */ + private static readonly attributeDefinitions: Map = new Map([ + // RFC 2865 Authentication Attributes + [ERadiusAttributeType.UserName, { type: 1, name: 'User-Name', valueType: 'text' }], + [ERadiusAttributeType.UserPassword, { type: 2, name: 'User-Password', valueType: 'string', encrypted: true }], + [ERadiusAttributeType.ChapPassword, { type: 3, name: 'CHAP-Password', valueType: 'string' }], + [ERadiusAttributeType.NasIpAddress, { type: 4, name: 'NAS-IP-Address', valueType: 'address' }], + [ERadiusAttributeType.NasPort, { type: 5, name: 'NAS-Port', valueType: 'integer' }], + [ERadiusAttributeType.ServiceType, { type: 6, name: 'Service-Type', valueType: 'integer' }], + [ERadiusAttributeType.FramedProtocol, { type: 7, name: 'Framed-Protocol', valueType: 'integer' }], + [ERadiusAttributeType.FramedIpAddress, { type: 8, name: 'Framed-IP-Address', valueType: 'address' }], + [ERadiusAttributeType.FramedIpNetmask, { type: 9, name: 'Framed-IP-Netmask', valueType: 'address' }], + [ERadiusAttributeType.FramedRouting, { type: 10, name: 'Framed-Routing', valueType: 'integer' }], + [ERadiusAttributeType.FilterId, { type: 11, name: 'Filter-Id', valueType: 'text' }], + [ERadiusAttributeType.FramedMtu, { type: 12, name: 'Framed-MTU', valueType: 'integer' }], + [ERadiusAttributeType.FramedCompression, { type: 13, name: 'Framed-Compression', valueType: 'integer' }], + [ERadiusAttributeType.LoginIpHost, { type: 14, name: 'Login-IP-Host', valueType: 'address' }], + [ERadiusAttributeType.LoginService, { type: 15, name: 'Login-Service', valueType: 'integer' }], + [ERadiusAttributeType.LoginTcpPort, { type: 16, name: 'Login-TCP-Port', valueType: 'integer' }], + [ERadiusAttributeType.ReplyMessage, { type: 18, name: 'Reply-Message', valueType: 'text' }], + [ERadiusAttributeType.CallbackNumber, { type: 19, name: 'Callback-Number', valueType: 'text' }], + [ERadiusAttributeType.CallbackId, { type: 20, name: 'Callback-Id', valueType: 'text' }], + [ERadiusAttributeType.FramedRoute, { type: 22, name: 'Framed-Route', valueType: 'text' }], + [ERadiusAttributeType.FramedIpxNetwork, { type: 23, name: 'Framed-IPX-Network', valueType: 'integer' }], + [ERadiusAttributeType.State, { type: 24, name: 'State', valueType: 'string' }], + [ERadiusAttributeType.Class, { type: 25, name: 'Class', valueType: 'string' }], + [ERadiusAttributeType.VendorSpecific, { type: 26, name: 'Vendor-Specific', valueType: 'vsa' }], + [ERadiusAttributeType.SessionTimeout, { type: 27, name: 'Session-Timeout', valueType: 'integer' }], + [ERadiusAttributeType.IdleTimeout, { type: 28, name: 'Idle-Timeout', valueType: 'integer' }], + [ERadiusAttributeType.TerminationAction, { type: 29, name: 'Termination-Action', valueType: 'integer' }], + [ERadiusAttributeType.CalledStationId, { type: 30, name: 'Called-Station-Id', valueType: 'text' }], + [ERadiusAttributeType.CallingStationId, { type: 31, name: 'Calling-Station-Id', valueType: 'text' }], + [ERadiusAttributeType.NasIdentifier, { type: 32, name: 'NAS-Identifier', valueType: 'text' }], + [ERadiusAttributeType.ProxyState, { type: 33, name: 'Proxy-State', valueType: 'string' }], + [ERadiusAttributeType.LoginLatService, { type: 34, name: 'Login-LAT-Service', valueType: 'text' }], + [ERadiusAttributeType.LoginLatNode, { type: 35, name: 'Login-LAT-Node', valueType: 'text' }], + [ERadiusAttributeType.LoginLatGroup, { type: 36, name: 'Login-LAT-Group', valueType: 'string' }], + [ERadiusAttributeType.FramedAppleTalkLink, { type: 37, name: 'Framed-AppleTalk-Link', valueType: 'integer' }], + [ERadiusAttributeType.FramedAppleTalkNetwork, { type: 38, name: 'Framed-AppleTalk-Network', valueType: 'integer' }], + [ERadiusAttributeType.FramedAppleTalkZone, { type: 39, name: 'Framed-AppleTalk-Zone', valueType: 'text' }], + [ERadiusAttributeType.ChapChallenge, { type: 60, name: 'CHAP-Challenge', valueType: 'string' }], + [ERadiusAttributeType.NasPortType, { type: 61, name: 'NAS-Port-Type', valueType: 'integer' }], + [ERadiusAttributeType.PortLimit, { type: 62, name: 'Port-Limit', valueType: 'integer' }], + [ERadiusAttributeType.LoginLatPort, { type: 63, name: 'Login-LAT-Port', valueType: 'text' }], + // RFC 2866 Accounting Attributes + [ERadiusAttributeType.AcctStatusType, { type: 40, name: 'Acct-Status-Type', valueType: 'integer' }], + [ERadiusAttributeType.AcctDelayTime, { type: 41, name: 'Acct-Delay-Time', valueType: 'integer' }], + [ERadiusAttributeType.AcctInputOctets, { type: 42, name: 'Acct-Input-Octets', valueType: 'integer' }], + [ERadiusAttributeType.AcctOutputOctets, { type: 43, name: 'Acct-Output-Octets', valueType: 'integer' }], + [ERadiusAttributeType.AcctSessionId, { type: 44, name: 'Acct-Session-Id', valueType: 'text' }], + [ERadiusAttributeType.AcctAuthentic, { type: 45, name: 'Acct-Authentic', valueType: 'integer' }], + [ERadiusAttributeType.AcctSessionTime, { type: 46, name: 'Acct-Session-Time', valueType: 'integer' }], + [ERadiusAttributeType.AcctInputPackets, { type: 47, name: 'Acct-Input-Packets', valueType: 'integer' }], + [ERadiusAttributeType.AcctOutputPackets, { type: 48, name: 'Acct-Output-Packets', valueType: 'integer' }], + [ERadiusAttributeType.AcctTerminateCause, { type: 49, name: 'Acct-Terminate-Cause', valueType: 'integer' }], + [ERadiusAttributeType.AcctMultiSessionId, { type: 50, name: 'Acct-Multi-Session-Id', valueType: 'text' }], + [ERadiusAttributeType.AcctLinkCount, { type: 51, name: 'Acct-Link-Count', valueType: 'integer' }], + // EAP support + [ERadiusAttributeType.EapMessage, { type: 79, name: 'EAP-Message', valueType: 'string' }], + [ERadiusAttributeType.MessageAuthenticator, { type: 80, name: 'Message-Authenticator', valueType: 'string' }], + ]); + + /** + * Attribute name to type mapping + */ + private static readonly nameToType: Map = new Map( + Array.from(RadiusAttributes.attributeDefinitions.entries()).map(([type, def]) => [def.name, type]) + ); + + /** + * Get attribute definition by type + */ + public static getDefinition(type: number): IAttributeDefinition | undefined { + return this.attributeDefinitions.get(type); + } + + /** + * Get attribute type by name + */ + public static getTypeByName(name: string): number | undefined { + return this.nameToType.get(name); + } + + /** + * Get attribute name by type + */ + public static getNameByType(type: number): string { + const def = this.attributeDefinitions.get(type); + return def ? def.name : `Unknown-Attribute-${type}`; + } + + /** + * Parse attribute value based on its type + */ + public static parseValue(type: number, value: Buffer): string | number | Buffer { + const def = this.attributeDefinitions.get(type); + if (!def) { + return value; + } + + switch (def.valueType) { + case 'text': + return value.toString('utf8'); + case 'address': + return this.parseAddress(value); + case 'integer': + return this.parseInteger(value); + case 'time': + return this.parseInteger(value); + case 'string': + case 'vsa': + default: + return value; + } + } + + /** + * Encode attribute value + */ + public static encodeValue(type: number | string, value: string | number | Buffer): Buffer { + const attrType = typeof type === 'string' ? this.getTypeByName(type) : type; + if (attrType === undefined) { + throw new Error(`Unknown attribute type: ${type}`); + } + + const def = this.attributeDefinitions.get(attrType); + if (!def) { + if (Buffer.isBuffer(value)) { + return value; + } + return Buffer.from(String(value), 'utf8'); + } + + switch (def.valueType) { + case 'text': + return Buffer.from(String(value), 'utf8'); + case 'address': + return this.encodeAddress(String(value)); + case 'integer': + case 'time': + return this.encodeInteger(Number(value)); + case 'string': + case 'vsa': + default: + if (Buffer.isBuffer(value)) { + return value; + } + return Buffer.from(String(value), 'utf8'); + } + } + + /** + * Parse IPv4 address from buffer (4 bytes) + */ + private static parseAddress(buffer: Buffer): string { + if (buffer.length < 4) { + return '0.0.0.0'; + } + return `${buffer[0]}.${buffer[1]}.${buffer[2]}.${buffer[3]}`; + } + + /** + * Encode IPv4 address to buffer + */ + private static encodeAddress(address: string): Buffer { + const parts = address.split('.').map((p) => parseInt(p, 10)); + if (parts.length !== 4 || parts.some((p) => isNaN(p) || p < 0 || p > 255)) { + throw new Error(`Invalid IP address: ${address}`); + } + return Buffer.from(parts); + } + + /** + * Parse 32-bit unsigned integer from buffer (big-endian) + */ + private static parseInteger(buffer: Buffer): number { + if (buffer.length < 4) { + return 0; + } + return buffer.readUInt32BE(0); + } + + /** + * Encode 32-bit unsigned integer to buffer (big-endian) + */ + private static encodeInteger(value: number): Buffer { + const buffer = Buffer.allocUnsafe(4); + buffer.writeUInt32BE(value >>> 0, 0); + return buffer; + } + + /** + * Encode a complete attribute (type + length + value) + */ + public static encodeAttribute(type: number | string, value: string | number | Buffer): Buffer { + const attrType = typeof type === 'string' ? this.getTypeByName(type) : type; + if (attrType === undefined) { + throw new Error(`Unknown attribute type: ${type}`); + } + + const encodedValue = this.encodeValue(attrType, value); + const length = 2 + encodedValue.length; + + if (length > 255) { + throw new Error(`Attribute value too long: ${length} bytes (max 253 value bytes)`); + } + + const buffer = Buffer.allocUnsafe(length); + buffer.writeUInt8(attrType, 0); + buffer.writeUInt8(length, 1); + encodedValue.copy(buffer, 2); + return buffer; + } + + /** + * Parse raw attribute into named attribute + */ + public static parseAttribute(attr: IRadiusAttribute): IParsedAttribute { + return { + type: attr.type, + name: this.getNameByType(attr.type), + value: this.parseValue(attr.type, attr.value), + rawValue: attr.value, + }; + } + + /** + * Parse Vendor-Specific Attribute (RFC 2865 Section 5.26) + * Format: Vendor-Id (4 bytes) + Vendor-Type (1 byte) + Vendor-Length (1 byte) + Value + */ + public static parseVSA(buffer: Buffer): IVendorSpecificAttribute | null { + if (buffer.length < 6) { + return null; + } + + const vendorId = buffer.readUInt32BE(0); + const vendorType = buffer.readUInt8(4); + const vendorLength = buffer.readUInt8(5); + + if (buffer.length < 4 + vendorLength) { + return null; + } + + const vendorValue = buffer.subarray(6, 4 + vendorLength); + return { + vendorId, + vendorType, + vendorValue, + }; + } + + /** + * Encode Vendor-Specific Attribute + */ + public static encodeVSA(vsa: IVendorSpecificAttribute): Buffer { + const valueLength = vsa.vendorValue.length; + const vendorLength = 2 + valueLength; // vendor-type + vendor-length + value + const totalLength = 4 + vendorLength; // vendor-id + vendor sub-attributes + + const buffer = Buffer.allocUnsafe(totalLength); + buffer.writeUInt32BE(vsa.vendorId, 0); + buffer.writeUInt8(vsa.vendorType, 4); + buffer.writeUInt8(vendorLength, 5); + vsa.vendorValue.copy(buffer, 6); + + return buffer; + } + + /** + * Create a complete Vendor-Specific attribute (type 26) + */ + public static createVendorAttribute(vendorId: number, vendorType: number, vendorValue: Buffer): Buffer { + const vsaValue = this.encodeVSA({ vendorId, vendorType, vendorValue }); + return this.encodeAttribute(ERadiusAttributeType.VendorSpecific, vsaValue); + } + + /** + * Check if an attribute is encrypted (e.g., User-Password) + */ + public static isEncrypted(type: number): boolean { + const def = this.attributeDefinitions.get(type); + return def?.encrypted === true; + } +} + +export default RadiusAttributes; diff --git a/ts_server/classes.radiusauthenticator.ts b/ts_server/classes.radiusauthenticator.ts new file mode 100644 index 0000000..d6e1bdb --- /dev/null +++ b/ts_server/classes.radiusauthenticator.ts @@ -0,0 +1,302 @@ +import * as plugins from './plugins.js'; +import { ERadiusCode } from './interfaces.js'; + +/** + * RADIUS Authenticator handling + * Implements RFC 2865 and RFC 2866 authenticator calculations + */ +export class RadiusAuthenticator { + private static readonly AUTHENTICATOR_LENGTH = 16; + + /** + * Generate random Request Authenticator for Access-Request + * RFC 2865: The Request Authenticator value is a 16 octet random number + */ + public static generateRequestAuthenticator(): Buffer { + return plugins.crypto.randomBytes(this.AUTHENTICATOR_LENGTH); + } + + /** + * Calculate Response Authenticator for Access-Accept, Access-Reject, Access-Challenge + * RFC 2865: ResponseAuth = MD5(Code+ID+Length+RequestAuth+Attributes+Secret) + */ + public static calculateResponseAuthenticator( + code: ERadiusCode, + identifier: number, + requestAuthenticator: Buffer, + attributes: Buffer, + secret: string + ): Buffer { + const length = 20 + attributes.length; // 20 = header size + + const md5 = plugins.crypto.createHash('md5'); + const header = Buffer.allocUnsafe(4); + header.writeUInt8(code, 0); + header.writeUInt8(identifier, 1); + header.writeUInt16BE(length, 2); + + md5.update(header); + md5.update(requestAuthenticator); + md5.update(attributes); + md5.update(Buffer.from(secret, 'utf8')); + + return md5.digest(); + } + + /** + * Calculate Accounting Request Authenticator + * RFC 2866: MD5(Code+ID+Length+16ร—0x00+Attrs+Secret) + */ + public static calculateAccountingRequestAuthenticator( + code: ERadiusCode, + identifier: number, + attributes: Buffer, + secret: string + ): Buffer { + const length = 20 + attributes.length; + const zeroAuth = Buffer.alloc(this.AUTHENTICATOR_LENGTH, 0); + + const md5 = plugins.crypto.createHash('md5'); + const header = Buffer.allocUnsafe(4); + header.writeUInt8(code, 0); + header.writeUInt8(identifier, 1); + header.writeUInt16BE(length, 2); + + md5.update(header); + md5.update(zeroAuth); + md5.update(attributes); + md5.update(Buffer.from(secret, 'utf8')); + + return md5.digest(); + } + + /** + * Verify Accounting Request Authenticator + */ + public static verifyAccountingRequestAuthenticator( + packet: Buffer, + secret: string + ): boolean { + if (packet.length < 20) { + return false; + } + + const code = packet.readUInt8(0); + const identifier = packet.readUInt8(1); + const length = packet.readUInt16BE(2); + const receivedAuth = packet.subarray(4, 20); + const attributes = packet.subarray(20, length); + + const expectedAuth = this.calculateAccountingRequestAuthenticator( + code, + identifier, + attributes, + secret + ); + + return plugins.crypto.timingSafeEqual(receivedAuth, expectedAuth); + } + + /** + * Verify Response Authenticator + */ + public static verifyResponseAuthenticator( + responsePacket: Buffer, + requestAuthenticator: Buffer, + secret: string + ): boolean { + if (responsePacket.length < 20) { + return false; + } + + const code = responsePacket.readUInt8(0); + const identifier = responsePacket.readUInt8(1); + const length = responsePacket.readUInt16BE(2); + const receivedAuth = responsePacket.subarray(4, 20); + const attributes = responsePacket.subarray(20, length); + + const expectedAuth = this.calculateResponseAuthenticator( + code, + identifier, + requestAuthenticator, + attributes, + secret + ); + + return plugins.crypto.timingSafeEqual(receivedAuth, expectedAuth); + } + + /** + * Encrypt User-Password (PAP) + * RFC 2865 Section 5.2: + * b1 = MD5(S + RA) c(1) = p1 xor b1 + * b2 = MD5(S + c(1)) c(2) = p2 xor b2 + * ... + * bi = MD5(S + c(i-1)) c(i) = pi xor bi + */ + public static encryptPassword( + password: string, + requestAuthenticator: Buffer, + secret: string + ): Buffer { + const passwordBuffer = Buffer.from(password, 'utf8'); + + // Pad password to multiple of 16 bytes + const paddedLength = Math.max(16, Math.ceil(passwordBuffer.length / 16) * 16); + const padded = Buffer.alloc(paddedLength, 0); + passwordBuffer.copy(padded); + + // Limit to 128 bytes max + const maxLength = Math.min(paddedLength, 128); + const result = Buffer.allocUnsafe(maxLength); + + let previousCipher = requestAuthenticator; + + for (let i = 0; i < maxLength; i += 16) { + const md5 = plugins.crypto.createHash('md5'); + md5.update(Buffer.from(secret, 'utf8')); + md5.update(previousCipher); + const b = md5.digest(); + + for (let j = 0; j < 16 && i + j < maxLength; j++) { + result[i + j] = padded[i + j] ^ b[j]; + } + + previousCipher = result.subarray(i, i + 16); + } + + return result; + } + + /** + * Decrypt User-Password (PAP) + * Reverse of encryptPassword + */ + public static decryptPassword( + encryptedPassword: Buffer, + requestAuthenticator: Buffer, + secret: string + ): string { + if (encryptedPassword.length === 0 || encryptedPassword.length % 16 !== 0) { + throw new Error('Invalid encrypted password length'); + } + + const result = Buffer.allocUnsafe(encryptedPassword.length); + let previousCipher = requestAuthenticator; + + for (let i = 0; i < encryptedPassword.length; i += 16) { + const md5 = plugins.crypto.createHash('md5'); + md5.update(Buffer.from(secret, 'utf8')); + md5.update(previousCipher); + const b = md5.digest(); + + for (let j = 0; j < 16; j++) { + result[i + j] = encryptedPassword[i + j] ^ b[j]; + } + + previousCipher = encryptedPassword.subarray(i, i + 16); + } + + // Remove null padding + let end = result.length; + while (end > 0 && result[end - 1] === 0) { + end--; + } + + return result.subarray(0, end).toString('utf8'); + } + + /** + * Calculate CHAP Response + * RFC 2865: CHAP-Response = MD5(CHAP-ID + Password + Challenge) + */ + public static calculateChapResponse( + chapId: number, + password: string, + challenge: Buffer + ): Buffer { + const md5 = plugins.crypto.createHash('md5'); + md5.update(Buffer.from([chapId])); + md5.update(Buffer.from(password, 'utf8')); + md5.update(challenge); + return md5.digest(); + } + + /** + * Verify CHAP Response + * CHAP-Password format: CHAP Ident (1 byte) + String (16 bytes MD5 hash) + */ + public static verifyChapResponse( + chapPassword: Buffer, + challenge: Buffer, + password: string + ): boolean { + if (chapPassword.length !== 17) { + return false; + } + + const chapId = chapPassword[0]; + const receivedResponse = chapPassword.subarray(1); + const expectedResponse = this.calculateChapResponse(chapId, password, challenge); + + return plugins.crypto.timingSafeEqual(receivedResponse, expectedResponse); + } + + /** + * Calculate Message-Authenticator (HMAC-MD5) + * Used for EAP and other extended authentication methods + * RFC 3579: HMAC-MD5 over entire packet with Message-Authenticator set to 16 zero bytes + */ + public static calculateMessageAuthenticator( + packet: Buffer, + secret: string + ): Buffer { + // The packet should have Message-Authenticator attribute with 16 zero bytes + const hmac = plugins.crypto.createHmac('md5', Buffer.from(secret, 'utf8')); + hmac.update(packet); + return hmac.digest(); + } + + /** + * Verify Message-Authenticator + */ + public static verifyMessageAuthenticator( + packet: Buffer, + messageAuthenticator: Buffer, + messageAuthenticatorOffset: number, + secret: string + ): boolean { + if (messageAuthenticator.length !== 16) { + return false; + } + + // Create a copy of the packet with Message-Authenticator set to 16 zero bytes + const packetCopy = Buffer.from(packet); + Buffer.alloc(16, 0).copy(packetCopy, messageAuthenticatorOffset); + + const expectedAuth = this.calculateMessageAuthenticator(packetCopy, secret); + return plugins.crypto.timingSafeEqual(messageAuthenticator, expectedAuth); + } + + /** + * Create packet header + */ + public static createPacketHeader( + code: ERadiusCode, + identifier: number, + authenticator: Buffer, + attributesLength: number + ): Buffer { + const header = Buffer.allocUnsafe(20); + const length = 20 + attributesLength; + + header.writeUInt8(code, 0); + header.writeUInt8(identifier, 1); + header.writeUInt16BE(length, 2); + authenticator.copy(header, 4); + + return header; + } +} + +export default RadiusAuthenticator; diff --git a/ts_server/classes.radiuspacket.ts b/ts_server/classes.radiuspacket.ts new file mode 100644 index 0000000..d33bae6 --- /dev/null +++ b/ts_server/classes.radiuspacket.ts @@ -0,0 +1,426 @@ +import * as plugins from './plugins.js'; +import type { IRadiusPacket, IParsedRadiusPacket, IRadiusAttribute, IParsedAttribute } from './interfaces.js'; +import { ERadiusCode, ERadiusAttributeType } from './interfaces.js'; +import { RadiusAttributes } from './classes.radiusattributes.js'; +import { RadiusAuthenticator } from './classes.radiusauthenticator.js'; + +/** + * RADIUS Packet encoder/decoder + * Implements RFC 2865 Section 3 packet format + */ +export class RadiusPacket { + /** + * Minimum packet size (RFC 2865: 20 bytes) + */ + public static readonly MIN_PACKET_SIZE = 20; + + /** + * Maximum packet size (RFC 2865: 4096 bytes) + */ + public static readonly MAX_PACKET_SIZE = 4096; + + /** + * Header size (Code + Identifier + Length + Authenticator) + */ + public static readonly HEADER_SIZE = 20; + + /** + * Authenticator size + */ + public static readonly AUTHENTICATOR_SIZE = 16; + + /** + * Decode a RADIUS packet from a buffer + */ + public static decode(buffer: Buffer): IRadiusPacket { + if (buffer.length < this.MIN_PACKET_SIZE) { + throw new Error(`Packet too short: ${buffer.length} bytes (minimum ${this.MIN_PACKET_SIZE})`); + } + + const code = buffer.readUInt8(0); + const identifier = buffer.readUInt8(1); + const length = buffer.readUInt16BE(2); + + // Validate code + if (!this.isValidCode(code)) { + throw new Error(`Invalid packet code: ${code}`); + } + + // Validate length + if (length < this.MIN_PACKET_SIZE) { + throw new Error(`Invalid packet length in header: ${length}`); + } + if (length > this.MAX_PACKET_SIZE) { + throw new Error(`Packet too large: ${length} bytes (maximum ${this.MAX_PACKET_SIZE})`); + } + if (buffer.length < length) { + throw new Error(`Buffer too short for declared length: ${buffer.length} < ${length}`); + } + + const authenticator = Buffer.allocUnsafe(this.AUTHENTICATOR_SIZE); + buffer.copy(authenticator, 0, 4, 20); + + const attributes = this.decodeAttributes(buffer.subarray(20, length)); + + return { + code, + identifier, + authenticator, + attributes, + }; + } + + /** + * Decode packet and parse attributes + */ + public static decodeAndParse(buffer: Buffer): IParsedRadiusPacket { + const packet = this.decode(buffer); + const parsedAttributes = packet.attributes.map((attr) => RadiusAttributes.parseAttribute(attr)); + + return { + ...packet, + parsedAttributes, + }; + } + + /** + * Decode attributes from buffer + */ + private static decodeAttributes(buffer: Buffer): IRadiusAttribute[] { + const attributes: IRadiusAttribute[] = []; + let offset = 0; + + while (offset < buffer.length) { + if (offset + 2 > buffer.length) { + throw new Error('Malformed attribute: truncated header'); + } + + const type = buffer.readUInt8(offset); + const length = buffer.readUInt8(offset + 1); + + if (length < 2) { + throw new Error(`Invalid attribute length: ${length}`); + } + if (offset + length > buffer.length) { + throw new Error('Malformed attribute: truncated value'); + } + + const value = Buffer.allocUnsafe(length - 2); + buffer.copy(value, 0, offset + 2, offset + length); + + attributes.push({ type, value }); + offset += length; + } + + return attributes; + } + + /** + * Encode a RADIUS packet to a buffer + */ + public static encode(packet: IRadiusPacket): Buffer { + const attributesBuffer = this.encodeAttributes(packet.attributes); + const length = this.HEADER_SIZE + attributesBuffer.length; + + if (length > this.MAX_PACKET_SIZE) { + throw new Error(`Packet too large: ${length} bytes (maximum ${this.MAX_PACKET_SIZE})`); + } + + const buffer = Buffer.allocUnsafe(length); + buffer.writeUInt8(packet.code, 0); + buffer.writeUInt8(packet.identifier, 1); + buffer.writeUInt16BE(length, 2); + packet.authenticator.copy(buffer, 4); + attributesBuffer.copy(buffer, 20); + + return buffer; + } + + /** + * Encode attributes to buffer + */ + private static encodeAttributes(attributes: IRadiusAttribute[]): Buffer { + const buffers = attributes.map((attr) => { + const length = 2 + attr.value.length; + if (length > 255) { + throw new Error(`Attribute value too long: ${attr.value.length} bytes (max 253)`); + } + const buffer = Buffer.allocUnsafe(length); + buffer.writeUInt8(attr.type, 0); + buffer.writeUInt8(length, 1); + attr.value.copy(buffer, 2); + return buffer; + }); + + return Buffer.concat(buffers); + } + + /** + * Create an Access-Request packet + */ + public static createAccessRequest( + identifier: number, + secret: string, + attributes: Array<{ type: number | string; value: string | number | Buffer }> + ): Buffer { + const requestAuthenticator = RadiusAuthenticator.generateRequestAuthenticator(); + const rawAttributes: IRadiusAttribute[] = []; + + for (const attr of attributes) { + const attrType = typeof attr.type === 'string' + ? RadiusAttributes.getTypeByName(attr.type) + : attr.type; + + if (attrType === undefined) { + throw new Error(`Unknown attribute type: ${attr.type}`); + } + + let value: Buffer; + if (attrType === ERadiusAttributeType.UserPassword && typeof attr.value === 'string') { + // Encrypt password + value = RadiusAuthenticator.encryptPassword(attr.value, requestAuthenticator, secret); + } else { + value = RadiusAttributes.encodeValue(attrType, attr.value); + } + + rawAttributes.push({ type: attrType, value }); + } + + const packet: IRadiusPacket = { + code: ERadiusCode.AccessRequest, + identifier, + authenticator: requestAuthenticator, + attributes: rawAttributes, + }; + + return this.encode(packet); + } + + /** + * Create an Access-Accept packet + */ + public static createAccessAccept( + identifier: number, + requestAuthenticator: Buffer, + secret: string, + attributes: Array<{ type: number | string; value: string | number | Buffer }> = [] + ): Buffer { + return this.createResponse( + ERadiusCode.AccessAccept, + identifier, + requestAuthenticator, + secret, + attributes + ); + } + + /** + * Create an Access-Reject packet + */ + public static createAccessReject( + identifier: number, + requestAuthenticator: Buffer, + secret: string, + attributes: Array<{ type: number | string; value: string | number | Buffer }> = [] + ): Buffer { + return this.createResponse( + ERadiusCode.AccessReject, + identifier, + requestAuthenticator, + secret, + attributes + ); + } + + /** + * Create an Access-Challenge packet + */ + public static createAccessChallenge( + identifier: number, + requestAuthenticator: Buffer, + secret: string, + attributes: Array<{ type: number | string; value: string | number | Buffer }> = [] + ): Buffer { + return this.createResponse( + ERadiusCode.AccessChallenge, + identifier, + requestAuthenticator, + secret, + attributes + ); + } + + /** + * Create an Accounting-Request packet + */ + public static createAccountingRequest( + identifier: number, + secret: string, + attributes: Array<{ type: number | string; value: string | number | Buffer }> + ): Buffer { + const rawAttributes: IRadiusAttribute[] = []; + + for (const attr of attributes) { + const attrType = typeof attr.type === 'string' + ? RadiusAttributes.getTypeByName(attr.type) + : attr.type; + + if (attrType === undefined) { + throw new Error(`Unknown attribute type: ${attr.type}`); + } + + const value = RadiusAttributes.encodeValue(attrType, attr.value); + rawAttributes.push({ type: attrType, value }); + } + + // Calculate authenticator with zero placeholder + const attributesBuffer = this.encodeAttributes(rawAttributes); + const authenticator = RadiusAuthenticator.calculateAccountingRequestAuthenticator( + ERadiusCode.AccountingRequest, + identifier, + attributesBuffer, + secret + ); + + const packet: IRadiusPacket = { + code: ERadiusCode.AccountingRequest, + identifier, + authenticator, + attributes: rawAttributes, + }; + + return this.encode(packet); + } + + /** + * Create an Accounting-Response packet + */ + public static createAccountingResponse( + identifier: number, + requestAuthenticator: Buffer, + secret: string, + attributes: Array<{ type: number | string; value: string | number | Buffer }> = [] + ): Buffer { + return this.createResponse( + ERadiusCode.AccountingResponse, + identifier, + requestAuthenticator, + secret, + attributes + ); + } + + /** + * Create a response packet (Accept, Reject, Challenge, Accounting-Response) + */ + private static createResponse( + code: ERadiusCode, + identifier: number, + requestAuthenticator: Buffer, + secret: string, + attributes: Array<{ type: number | string; value: string | number | Buffer }> + ): Buffer { + const rawAttributes: IRadiusAttribute[] = []; + + for (const attr of attributes) { + const attrType = typeof attr.type === 'string' + ? RadiusAttributes.getTypeByName(attr.type) + : attr.type; + + if (attrType === undefined) { + throw new Error(`Unknown attribute type: ${attr.type}`); + } + + const value = RadiusAttributes.encodeValue(attrType, attr.value); + rawAttributes.push({ type: attrType, value }); + } + + const attributesBuffer = this.encodeAttributes(rawAttributes); + + // Calculate response authenticator + const responseAuthenticator = RadiusAuthenticator.calculateResponseAuthenticator( + code, + identifier, + requestAuthenticator, + attributesBuffer, + secret + ); + + const packet: IRadiusPacket = { + code, + identifier, + authenticator: responseAuthenticator, + attributes: rawAttributes, + }; + + return this.encode(packet); + } + + /** + * Get attribute value from a packet + */ + public static getAttribute(packet: IRadiusPacket, type: number | string): Buffer | undefined { + const attrType = typeof type === 'string' ? RadiusAttributes.getTypeByName(type) : type; + if (attrType === undefined) { + return undefined; + } + const attr = packet.attributes.find((a) => a.type === attrType); + return attr?.value; + } + + /** + * Get all attribute values from a packet + */ + public static getAttributes(packet: IRadiusPacket, type: number | string): Buffer[] { + const attrType = typeof type === 'string' ? RadiusAttributes.getTypeByName(type) : type; + if (attrType === undefined) { + return []; + } + return packet.attributes.filter((a) => a.type === attrType).map((a) => a.value); + } + + /** + * Get parsed attribute value + */ + public static getParsedAttribute(packet: IRadiusPacket, type: number | string): string | number | Buffer | undefined { + const value = this.getAttribute(packet, type); + if (value === undefined) { + return undefined; + } + const attrType = typeof type === 'string' ? RadiusAttributes.getTypeByName(type)! : type; + return RadiusAttributes.parseValue(attrType, value); + } + + /** + * Check if a packet code is valid + */ + private static isValidCode(code: number): boolean { + return code === ERadiusCode.AccessRequest || + code === ERadiusCode.AccessAccept || + code === ERadiusCode.AccessReject || + code === ERadiusCode.AccountingRequest || + code === ERadiusCode.AccountingResponse || + code === ERadiusCode.AccessChallenge || + code === ERadiusCode.StatusServer || + code === ERadiusCode.StatusClient; + } + + /** + * Get code name + */ + public static getCodeName(code: ERadiusCode): string { + const names: Record = { + [ERadiusCode.AccessRequest]: 'Access-Request', + [ERadiusCode.AccessAccept]: 'Access-Accept', + [ERadiusCode.AccessReject]: 'Access-Reject', + [ERadiusCode.AccountingRequest]: 'Accounting-Request', + [ERadiusCode.AccountingResponse]: 'Accounting-Response', + [ERadiusCode.AccessChallenge]: 'Access-Challenge', + [ERadiusCode.StatusServer]: 'Status-Server', + [ERadiusCode.StatusClient]: 'Status-Client', + }; + return names[code] || `Unknown-Code-${code}`; + } +} + +export default RadiusPacket; diff --git a/ts_server/classes.radiussecrets.ts b/ts_server/classes.radiussecrets.ts new file mode 100644 index 0000000..f5f203f --- /dev/null +++ b/ts_server/classes.radiussecrets.ts @@ -0,0 +1,116 @@ +import type { TSecretResolver } from './interfaces.js'; + +/** + * RADIUS Shared Secrets Manager + * Manages per-client shared secrets for RADIUS authentication + */ +export class RadiusSecrets { + private readonly secrets: Map = new Map(); + private defaultSecret?: string; + private customResolver?: TSecretResolver; + + /** + * Create a new secrets manager + */ + constructor(options?: { + defaultSecret?: string; + secrets?: Record; + resolver?: TSecretResolver; + }) { + if (options?.defaultSecret) { + this.defaultSecret = options.defaultSecret; + } + if (options?.secrets) { + for (const [ip, secret] of Object.entries(options.secrets)) { + this.secrets.set(ip, secret); + } + } + if (options?.resolver) { + this.customResolver = options.resolver; + } + } + + /** + * Set secret for a specific client IP + */ + public setClientSecret(clientIp: string, secret: string): void { + this.secrets.set(clientIp, secret); + } + + /** + * Remove secret for a specific client IP + */ + public removeClientSecret(clientIp: string): boolean { + return this.secrets.delete(clientIp); + } + + /** + * Set the default secret + */ + public setDefaultSecret(secret: string): void { + this.defaultSecret = secret; + } + + /** + * Set a custom resolver + */ + public setResolver(resolver: TSecretResolver): void { + this.customResolver = resolver; + } + + /** + * Get secret for a client IP + * Priority: 1. Custom resolver, 2. Per-client secret, 3. Default secret + */ + public getSecret(clientIp: string): string | undefined { + // Try custom resolver first + if (this.customResolver) { + const resolved = this.customResolver(clientIp); + if (resolved !== undefined) { + return resolved; + } + } + + // Try per-client secret + const clientSecret = this.secrets.get(clientIp); + if (clientSecret !== undefined) { + return clientSecret; + } + + // Fall back to default secret + return this.defaultSecret; + } + + /** + * Check if a client is known (has a secret) + */ + public isKnownClient(clientIp: string): boolean { + return this.getSecret(clientIp) !== undefined; + } + + /** + * Get all registered client IPs + */ + public getClientIps(): string[] { + return Array.from(this.secrets.keys()); + } + + /** + * Clear all per-client secrets + */ + public clearClientSecrets(): void { + this.secrets.clear(); + } + + /** + * Set secrets from a CIDR range (simplified - just IP addresses) + * For actual CIDR support, use a custom resolver + */ + public setSecretsForNetwork(network: string, secret: string): void { + // For simplicity, this just sets a single IP + // Real CIDR support would require a resolver + this.secrets.set(network, secret); + } +} + +export default RadiusSecrets; diff --git a/ts_server/classes.radiusserver.ts b/ts_server/classes.radiusserver.ts new file mode 100644 index 0000000..8820598 --- /dev/null +++ b/ts_server/classes.radiusserver.ts @@ -0,0 +1,649 @@ +import * as plugins from './plugins.js'; +import type { + IRadiusServerOptions, + IRadiusServerStats, + IRadiusPacket, + IAuthenticationRequest, + IAuthenticationResponse, + IAccountingRequest, + IAccountingResponse, + TAuthenticationHandler, + TAccountingHandler, +} from './interfaces.js'; +import { + ERadiusCode, + ERadiusAttributeType, + EAcctStatusType, + ENasPortType, + EServiceType, +} from './interfaces.js'; +import { RadiusPacket } from './classes.radiuspacket.js'; +import { RadiusAttributes } from './classes.radiusattributes.js'; +import { RadiusAuthenticator } from './classes.radiusauthenticator.js'; +import { RadiusSecrets } from './classes.radiussecrets.js'; + +/** + * RADIUS Server implementation + * Implements RFC 2865 (Authentication) and RFC 2866 (Accounting) + */ +export class RadiusServer { + private authSocket?: plugins.dgram.Socket; + private acctSocket?: plugins.dgram.Socket; + private readonly secrets: RadiusSecrets; + private authenticationHandler?: TAuthenticationHandler; + private accountingHandler?: TAccountingHandler; + + private readonly options: Required< + Pick + >; + + private readonly stats: IRadiusServerStats = { + authRequests: 0, + authAccepts: 0, + authRejects: 0, + authChallenges: 0, + authInvalidPackets: 0, + authUnknownClients: 0, + acctRequests: 0, + acctResponses: 0, + acctInvalidPackets: 0, + acctUnknownClients: 0, + }; + + // Duplicate detection cache: key = clientIp:clientPort:identifier + private readonly recentRequests: Map = new Map(); + private duplicateCleanupInterval?: ReturnType; + + constructor(options: IRadiusServerOptions = {}) { + this.options = { + authPort: options.authPort ?? 1812, + acctPort: options.acctPort ?? 1813, + bindAddress: options.bindAddress ?? '0.0.0.0', + duplicateDetectionWindow: options.duplicateDetectionWindow ?? 10000, // 10 seconds + maxPacketSize: options.maxPacketSize ?? RadiusPacket.MAX_PACKET_SIZE, + }; + + this.secrets = new RadiusSecrets({ + defaultSecret: options.defaultSecret, + resolver: options.secretResolver, + }); + + if (options.authenticationHandler) { + this.authenticationHandler = options.authenticationHandler; + } + if (options.accountingHandler) { + this.accountingHandler = options.accountingHandler; + } + } + + /** + * Start the RADIUS server + */ + public async start(): Promise { + await Promise.all([ + this.startAuthServer(), + this.startAcctServer(), + ]); + + // Start duplicate detection cleanup + this.duplicateCleanupInterval = setInterval(() => { + this.cleanupDuplicateCache(); + }, this.options.duplicateDetectionWindow); + } + + /** + * Stop the RADIUS server + */ + public async stop(): Promise { + if (this.duplicateCleanupInterval) { + clearInterval(this.duplicateCleanupInterval); + this.duplicateCleanupInterval = undefined; + } + + const stopPromises: Promise[] = []; + + if (this.authSocket) { + stopPromises.push(new Promise((resolve) => { + this.authSocket!.close(() => { + this.authSocket = undefined; + resolve(); + }); + })); + } + + if (this.acctSocket) { + stopPromises.push(new Promise((resolve) => { + this.acctSocket!.close(() => { + this.acctSocket = undefined; + resolve(); + }); + })); + } + + await Promise.all(stopPromises); + } + + /** + * Set the authentication handler + */ + public setAuthenticationHandler(handler: TAuthenticationHandler): void { + this.authenticationHandler = handler; + } + + /** + * Set the accounting handler + */ + public setAccountingHandler(handler: TAccountingHandler): void { + this.accountingHandler = handler; + } + + /** + * Set secret for a client + */ + public setClientSecret(clientIp: string, secret: string): void { + this.secrets.setClientSecret(clientIp, secret); + } + + /** + * Get server statistics + */ + public getStats(): IRadiusServerStats { + return { ...this.stats }; + } + + /** + * Reset server statistics + */ + public resetStats(): void { + this.stats.authRequests = 0; + this.stats.authAccepts = 0; + this.stats.authRejects = 0; + this.stats.authChallenges = 0; + this.stats.authInvalidPackets = 0; + this.stats.authUnknownClients = 0; + this.stats.acctRequests = 0; + this.stats.acctResponses = 0; + this.stats.acctInvalidPackets = 0; + this.stats.acctUnknownClients = 0; + } + + private async startAuthServer(): Promise { + return new Promise((resolve, reject) => { + this.authSocket = plugins.dgram.createSocket('udp4'); + + this.authSocket.on('error', (err) => { + reject(err); + }); + + this.authSocket.on('message', (msg, rinfo) => { + this.handleAuthMessage(msg, rinfo).catch((err) => { + console.error('Error handling auth message:', err); + }); + }); + + this.authSocket.bind(this.options.authPort, this.options.bindAddress, () => { + resolve(); + }); + }); + } + + private async startAcctServer(): Promise { + return new Promise((resolve, reject) => { + this.acctSocket = plugins.dgram.createSocket('udp4'); + + this.acctSocket.on('error', (err) => { + reject(err); + }); + + this.acctSocket.on('message', (msg, rinfo) => { + this.handleAcctMessage(msg, rinfo).catch((err) => { + console.error('Error handling acct message:', err); + }); + }); + + this.acctSocket.bind(this.options.acctPort, this.options.bindAddress, () => { + resolve(); + }); + }); + } + + private async handleAuthMessage(msg: Buffer, rinfo: plugins.dgram.RemoteInfo): Promise { + // Validate packet size + if (msg.length > this.options.maxPacketSize) { + this.stats.authInvalidPackets++; + return; + } + + // Get client secret + const secret = this.secrets.getSecret(rinfo.address); + if (!secret) { + this.stats.authUnknownClients++; + return; + } + + // Parse packet + let packet: IRadiusPacket; + try { + packet = RadiusPacket.decode(msg); + } catch (err) { + this.stats.authInvalidPackets++; + return; + } + + // Only handle Access-Request + if (packet.code !== ERadiusCode.AccessRequest) { + this.stats.authInvalidPackets++; + return; + } + + this.stats.authRequests++; + + // Check for duplicate + const duplicateKey = `${rinfo.address}:${rinfo.port}:${packet.identifier}`; + const cached = this.recentRequests.get(duplicateKey); + if (cached && cached.response) { + // Retransmit cached response + this.authSocket!.send(cached.response, rinfo.port, rinfo.address); + return; + } + + // Mark as in-progress (no response yet) + this.recentRequests.set(duplicateKey, { timestamp: Date.now() }); + + // Build authentication request context + const authRequest = this.buildAuthRequest(packet, secret, rinfo); + + // Call authentication handler + let response: IAuthenticationResponse; + if (this.authenticationHandler) { + try { + response = await this.authenticationHandler(authRequest); + } catch (err) { + response = { + code: ERadiusCode.AccessReject, + replyMessage: 'Internal server error', + }; + } + } else { + // No handler configured - reject all + response = { + code: ERadiusCode.AccessReject, + replyMessage: 'Authentication service not configured', + }; + } + + // Build response packet + const responsePacket = this.buildAuthResponse( + response, + packet.identifier, + packet.authenticator, + secret + ); + + // Update stats + switch (response.code) { + case ERadiusCode.AccessAccept: + this.stats.authAccepts++; + break; + case ERadiusCode.AccessReject: + this.stats.authRejects++; + break; + case ERadiusCode.AccessChallenge: + this.stats.authChallenges++; + break; + } + + // Cache response for duplicate detection + this.recentRequests.set(duplicateKey, { + timestamp: Date.now(), + response: responsePacket, + }); + + // Send response + this.authSocket!.send(responsePacket, rinfo.port, rinfo.address); + } + + private async handleAcctMessage(msg: Buffer, rinfo: plugins.dgram.RemoteInfo): Promise { + // Validate packet size + if (msg.length > this.options.maxPacketSize) { + this.stats.acctInvalidPackets++; + return; + } + + // Get client secret + const secret = this.secrets.getSecret(rinfo.address); + if (!secret) { + this.stats.acctUnknownClients++; + return; + } + + // Verify accounting request authenticator + if (!RadiusAuthenticator.verifyAccountingRequestAuthenticator(msg, secret)) { + this.stats.acctInvalidPackets++; + return; + } + + // Parse packet + let packet: IRadiusPacket; + try { + packet = RadiusPacket.decode(msg); + } catch (err) { + this.stats.acctInvalidPackets++; + return; + } + + // Only handle Accounting-Request + if (packet.code !== ERadiusCode.AccountingRequest) { + this.stats.acctInvalidPackets++; + return; + } + + this.stats.acctRequests++; + + // Check for duplicate + const duplicateKey = `acct:${rinfo.address}:${rinfo.port}:${packet.identifier}`; + const cached = this.recentRequests.get(duplicateKey); + if (cached && cached.response) { + // Retransmit cached response + this.acctSocket!.send(cached.response, rinfo.port, rinfo.address); + return; + } + + // Mark as in-progress + this.recentRequests.set(duplicateKey, { timestamp: Date.now() }); + + // Build accounting request context + const acctRequest = this.buildAcctRequest(packet, rinfo); + + // Call accounting handler + let response: IAccountingResponse; + if (this.accountingHandler) { + try { + response = await this.accountingHandler(acctRequest); + } catch (err) { + // Don't respond if we can't record the accounting data + return; + } + } else { + // No handler configured - accept all + response = { success: true }; + } + + // Only respond if accounting was successful + if (!response.success) { + return; + } + + // Build response packet + const responsePacket = RadiusPacket.createAccountingResponse( + packet.identifier, + packet.authenticator, + secret, + response.attributes || [] + ); + + this.stats.acctResponses++; + + // Cache response for duplicate detection + this.recentRequests.set(duplicateKey, { + timestamp: Date.now(), + response: responsePacket, + }); + + // Send response + this.acctSocket!.send(responsePacket, rinfo.port, rinfo.address); + } + + private buildAuthRequest( + packet: IRadiusPacket, + secret: string, + rinfo: plugins.dgram.RemoteInfo + ): IAuthenticationRequest { + const getUsernameAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.UserName); + const username = getUsernameAttr ? getUsernameAttr.toString('utf8') : ''; + + // Decrypt PAP password if present + const passwordAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.UserPassword); + let password: string | undefined; + if (passwordAttr) { + try { + password = RadiusAuthenticator.decryptPassword(passwordAttr, packet.authenticator, secret); + } catch { + password = undefined; + } + } + + // Get CHAP attributes + const chapPassword = RadiusPacket.getAttribute(packet, ERadiusAttributeType.ChapPassword); + let chapChallenge = RadiusPacket.getAttribute(packet, ERadiusAttributeType.ChapChallenge); + // If no CHAP-Challenge attribute, use Request Authenticator as challenge + if (chapPassword && !chapChallenge) { + chapChallenge = packet.authenticator; + } + + // Get NAS attributes + const nasIpAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasIpAddress); + const nasIpAddress = nasIpAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.NasIpAddress, nasIpAttr) as string : undefined; + + const nasIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasIdentifier); + const nasIdentifier = nasIdAttr ? nasIdAttr.toString('utf8') : undefined; + + const nasPortAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasPort); + const nasPort = nasPortAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.NasPort, nasPortAttr) as number : undefined; + + const nasPortTypeAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasPortType); + const nasPortType = nasPortTypeAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.NasPortType, nasPortTypeAttr) as ENasPortType : undefined; + + // Get other common attributes + const calledStationIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.CalledStationId); + const calledStationId = calledStationIdAttr ? calledStationIdAttr.toString('utf8') : undefined; + + const callingStationIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.CallingStationId); + const callingStationId = callingStationIdAttr ? callingStationIdAttr.toString('utf8') : undefined; + + const serviceTypeAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.ServiceType); + const serviceType = serviceTypeAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.ServiceType, serviceTypeAttr) as EServiceType : undefined; + + const framedProtocolAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.FramedProtocol); + const framedProtocol = framedProtocolAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.FramedProtocol, framedProtocolAttr) as number : undefined; + + const stateAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.State); + + return { + username, + password, + chapPassword, + chapChallenge, + nasIpAddress, + nasIdentifier, + nasPort, + nasPortType, + calledStationId, + callingStationId, + serviceType, + framedProtocol, + state: stateAttr, + rawPacket: packet, + clientAddress: rinfo.address, + clientPort: rinfo.port, + }; + } + + private buildAcctRequest( + packet: IRadiusPacket, + rinfo: plugins.dgram.RemoteInfo + ): IAccountingRequest { + const statusTypeAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctStatusType); + const statusType = statusTypeAttr + ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctStatusType, statusTypeAttr) as EAcctStatusType + : EAcctStatusType.Start; + + const sessionIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctSessionId); + const sessionId = sessionIdAttr ? sessionIdAttr.toString('utf8') : ''; + + const usernameAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.UserName); + const username = usernameAttr ? usernameAttr.toString('utf8') : undefined; + + const nasIpAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasIpAddress); + const nasIpAddress = nasIpAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.NasIpAddress, nasIpAttr) as string : undefined; + + const nasIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasIdentifier); + const nasIdentifier = nasIdAttr ? nasIdAttr.toString('utf8') : undefined; + + const nasPortAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasPort); + const nasPort = nasPortAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.NasPort, nasPortAttr) as number : undefined; + + const nasPortTypeAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasPortType); + const nasPortType = nasPortTypeAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.NasPortType, nasPortTypeAttr) as ENasPortType : undefined; + + const delayTimeAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctDelayTime); + const delayTime = delayTimeAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctDelayTime, delayTimeAttr) as number : undefined; + + const inputOctetsAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctInputOctets); + const inputOctets = inputOctetsAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctInputOctets, inputOctetsAttr) as number : undefined; + + const outputOctetsAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctOutputOctets); + const outputOctets = outputOctetsAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctOutputOctets, outputOctetsAttr) as number : undefined; + + const sessionTimeAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctSessionTime); + const sessionTime = sessionTimeAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctSessionTime, sessionTimeAttr) as number : undefined; + + const inputPacketsAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctInputPackets); + const inputPackets = inputPacketsAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctInputPackets, inputPacketsAttr) as number : undefined; + + const outputPacketsAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctOutputPackets); + const outputPackets = outputPacketsAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctOutputPackets, outputPacketsAttr) as number : undefined; + + const terminateCauseAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctTerminateCause); + const terminateCause = terminateCauseAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctTerminateCause, terminateCauseAttr) as number : undefined; + + const authenticAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctAuthentic); + const authentic = authenticAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctAuthentic, authenticAttr) as number : undefined; + + const multiSessionIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctMultiSessionId); + const multiSessionId = multiSessionIdAttr ? multiSessionIdAttr.toString('utf8') : undefined; + + const linkCountAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctLinkCount); + const linkCount = linkCountAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctLinkCount, linkCountAttr) as number : undefined; + + const calledStationIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.CalledStationId); + const calledStationId = calledStationIdAttr ? calledStationIdAttr.toString('utf8') : undefined; + + const callingStationIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.CallingStationId); + const callingStationId = callingStationIdAttr ? callingStationIdAttr.toString('utf8') : undefined; + + return { + statusType, + sessionId, + username, + nasIpAddress, + nasIdentifier, + nasPort, + nasPortType, + delayTime, + inputOctets, + outputOctets, + sessionTime, + inputPackets, + outputPackets, + terminateCause, + authentic, + multiSessionId, + linkCount, + calledStationId, + callingStationId, + rawPacket: packet, + clientAddress: rinfo.address, + clientPort: rinfo.port, + }; + } + + private buildAuthResponse( + response: IAuthenticationResponse, + identifier: number, + requestAuthenticator: Buffer, + secret: string + ): Buffer { + const attributes: Array<{ type: number | string; value: string | number | Buffer }> = []; + + // Add reply message if present + if (response.replyMessage) { + attributes.push({ type: ERadiusAttributeType.ReplyMessage, value: response.replyMessage }); + } + + // Add session timeout if present + if (response.sessionTimeout !== undefined) { + attributes.push({ type: ERadiusAttributeType.SessionTimeout, value: response.sessionTimeout }); + } + + // Add idle timeout if present + if (response.idleTimeout !== undefined) { + attributes.push({ type: ERadiusAttributeType.IdleTimeout, value: response.idleTimeout }); + } + + // Add state if present + if (response.state) { + attributes.push({ type: ERadiusAttributeType.State, value: response.state }); + } + + // Add class if present + if (response.class) { + attributes.push({ type: ERadiusAttributeType.Class, value: response.class }); + } + + // Add framed IP if present + if (response.framedIpAddress) { + attributes.push({ type: ERadiusAttributeType.FramedIpAddress, value: response.framedIpAddress }); + } + + // Add framed IP netmask if present + if (response.framedIpNetmask) { + attributes.push({ type: ERadiusAttributeType.FramedIpNetmask, value: response.framedIpNetmask }); + } + + // Add framed routes if present + if (response.framedRoutes) { + for (const route of response.framedRoutes) { + attributes.push({ type: ERadiusAttributeType.FramedRoute, value: route }); + } + } + + // Add vendor attributes if present + if (response.vendorAttributes) { + for (const vsa of response.vendorAttributes) { + const vsaBuffer = RadiusAttributes.encodeVSA(vsa); + attributes.push({ type: ERadiusAttributeType.VendorSpecific, value: vsaBuffer }); + } + } + + // Add custom attributes if present + if (response.attributes) { + attributes.push(...response.attributes); + } + + // Create response based on code + switch (response.code) { + case ERadiusCode.AccessAccept: + return RadiusPacket.createAccessAccept(identifier, requestAuthenticator, secret, attributes); + case ERadiusCode.AccessReject: + return RadiusPacket.createAccessReject(identifier, requestAuthenticator, secret, attributes); + case ERadiusCode.AccessChallenge: + return RadiusPacket.createAccessChallenge(identifier, requestAuthenticator, secret, attributes); + default: + return RadiusPacket.createAccessReject(identifier, requestAuthenticator, secret, attributes); + } + } + + private cleanupDuplicateCache(): void { + const now = Date.now(); + const expiry = this.options.duplicateDetectionWindow; + + for (const [key, entry] of this.recentRequests) { + if (now - entry.timestamp > expiry) { + this.recentRequests.delete(key); + } + } + } +} + +export default RadiusServer; diff --git a/ts_server/index.ts b/ts_server/index.ts new file mode 100644 index 0000000..c44a9b1 --- /dev/null +++ b/ts_server/index.ts @@ -0,0 +1,9 @@ +// RADIUS Server Module +// Implements RFC 2865 (Authentication) and RFC 2866 (Accounting) + +export * from './interfaces.js'; +export { RadiusServer } from './classes.radiusserver.js'; +export { RadiusPacket } from './classes.radiuspacket.js'; +export { RadiusAttributes } from './classes.radiusattributes.js'; +export { RadiusAuthenticator } from './classes.radiusauthenticator.js'; +export { RadiusSecrets } from './classes.radiussecrets.js'; diff --git a/ts_server/interfaces.ts b/ts_server/interfaces.ts new file mode 100644 index 0000000..f9045d5 --- /dev/null +++ b/ts_server/interfaces.ts @@ -0,0 +1,140 @@ +/** + * RADIUS Server Interfaces + * Server-specific types for handling authentication and accounting + */ + +import type { + ERadiusCode, + EServiceType, + EFramedProtocol, + ENasPortType, + EAcctStatusType, + EAcctAuthentic, + EAcctTerminateCause, + IRadiusPacket, + IVendorSpecificAttribute, +} from '../ts_shared/index.js'; + +// Re-export all shared types for backwards compatibility +export * from '../ts_shared/index.js'; + +/** + * Authentication request context + */ +export interface IAuthenticationRequest { + username: string; + password?: string; // For PAP + chapPassword?: Buffer; // For CHAP + chapChallenge?: Buffer; // For CHAP + nasIpAddress?: string; + nasIdentifier?: string; + nasPort?: number; + nasPortType?: ENasPortType; + calledStationId?: string; + callingStationId?: string; + serviceType?: EServiceType; + framedProtocol?: EFramedProtocol; + state?: Buffer; + rawPacket: IRadiusPacket; + clientAddress: string; + clientPort: number; +} + +/** + * Authentication response + */ +export interface IAuthenticationResponse { + code: ERadiusCode.AccessAccept | ERadiusCode.AccessReject | ERadiusCode.AccessChallenge; + attributes?: Array<{ type: number | string; value: string | number | Buffer }>; + replyMessage?: string; + sessionTimeout?: number; + idleTimeout?: number; + state?: Buffer; + class?: Buffer; + framedIpAddress?: string; + framedIpNetmask?: string; + framedRoutes?: string[]; + vendorAttributes?: IVendorSpecificAttribute[]; +} + +/** + * Accounting request context + */ +export interface IAccountingRequest { + statusType: EAcctStatusType; + sessionId: string; + username?: string; + nasIpAddress?: string; + nasIdentifier?: string; + nasPort?: number; + nasPortType?: ENasPortType; + delayTime?: number; + inputOctets?: number; + outputOctets?: number; + sessionTime?: number; + inputPackets?: number; + outputPackets?: number; + terminateCause?: EAcctTerminateCause; + authentic?: EAcctAuthentic; + multiSessionId?: string; + linkCount?: number; + calledStationId?: string; + callingStationId?: string; + rawPacket: IRadiusPacket; + clientAddress: string; + clientPort: number; +} + +/** + * Accounting response + */ +export interface IAccountingResponse { + success: boolean; + attributes?: Array<{ type: number | string; value: string | number | Buffer }>; +} + +/** + * Authentication handler function type + */ +export type TAuthenticationHandler = (request: IAuthenticationRequest) => Promise; + +/** + * Accounting handler function type + */ +export type TAccountingHandler = (request: IAccountingRequest) => Promise; + +/** + * Client secret resolver - returns secret for a given client IP + */ +export type TSecretResolver = (clientAddress: string) => string | undefined; + +/** + * RADIUS Server options + */ +export interface IRadiusServerOptions { + authPort?: number; + acctPort?: number; + bindAddress?: string; + defaultSecret?: string; + secretResolver?: TSecretResolver; + authenticationHandler?: TAuthenticationHandler; + accountingHandler?: TAccountingHandler; + duplicateDetectionWindow?: number; // ms + maxPacketSize?: number; +} + +/** + * RADIUS server statistics + */ +export interface IRadiusServerStats { + authRequests: number; + authAccepts: number; + authRejects: number; + authChallenges: number; + authInvalidPackets: number; + authUnknownClients: number; + acctRequests: number; + acctResponses: number; + acctInvalidPackets: number; + acctUnknownClients: number; +} diff --git a/ts_server/plugins.ts b/ts_server/plugins.ts new file mode 100644 index 0000000..718a472 --- /dev/null +++ b/ts_server/plugins.ts @@ -0,0 +1,7 @@ +import * as crypto from 'crypto'; +import * as dgram from 'dgram'; + +export { + crypto, + dgram, +}; diff --git a/ts_server/readme.md b/ts_server/readme.md new file mode 100644 index 0000000..641773f --- /dev/null +++ b/ts_server/readme.md @@ -0,0 +1,135 @@ +# @push.rocks/smartradius/server + +> ๐Ÿ–ฅ๏ธ RADIUS Server Implementation - Full RFC 2865/2866 compliant authentication and accounting server + +## Overview + +This module provides a complete RADIUS server implementation supporting both authentication (RFC 2865) and accounting (RFC 2866) protocols. It handles PAP and CHAP authentication, accounting session tracking, and includes duplicate detection with response caching. + +## Features + +- โœ… **PAP Authentication** - Password Authentication Protocol with RFC-compliant encryption +- โœ… **CHAP Authentication** - Challenge-Handshake Authentication Protocol +- โœ… **Accounting** - Session start/stop/interim-update tracking +- โœ… **Duplicate Detection** - Automatic response caching for retransmitted requests +- โœ… **Per-Client Secrets** - Support for different shared secrets per NAS +- โœ… **Statistics** - Built-in request/response counters +- โœ… **VSA Support** - Vendor-Specific Attributes handling +- โœ… **Message-Authenticator** - HMAC-MD5 for EAP support + +## Exports + +### Classes + +| Class | Description | +|-------|-------------| +| `RadiusServer` | Main server class handling authentication and accounting | +| `RadiusPacket` | Packet encoder/decoder for RADIUS protocol | +| `RadiusAttributes` | Attribute parsing and encoding utilities | +| `RadiusAuthenticator` | Cryptographic operations (PAP encryption, CHAP, authenticators) | +| `RadiusSecrets` | Client secret management | + +### Interfaces (Server-Specific) + +| Interface | Description | +|-----------|-------------| +| `IRadiusServerOptions` | Server configuration options | +| `IRadiusServerStats` | Server statistics counters | +| `IAuthenticationRequest` | Request context passed to auth handler | +| `IAuthenticationResponse` | Response from auth handler | +| `IAccountingRequest` | Request context passed to accounting handler | +| `IAccountingResponse` | Response from accounting handler | +| `TAuthenticationHandler` | Handler function type for authentication | +| `TAccountingHandler` | Handler function type for accounting | +| `TSecretResolver` | Function type for resolving client secrets | + +## Usage + +```typescript +import { RadiusServer, ERadiusCode } from '@push.rocks/smartradius'; + +const server = new RadiusServer({ + authPort: 1812, + acctPort: 1813, + defaultSecret: 'shared-secret', + + authenticationHandler: async (request) => { + // PAP authentication + if (request.password === 'correct-password') { + return { + code: ERadiusCode.AccessAccept, + replyMessage: 'Welcome!', + sessionTimeout: 3600, + }; + } + + // CHAP authentication + if (request.chapPassword && request.chapChallenge) { + const isValid = RadiusAuthenticator.verifyChapResponse( + request.chapPassword, + request.chapChallenge, + 'expected-password' + ); + if (isValid) { + return { code: ERadiusCode.AccessAccept }; + } + } + + return { code: ERadiusCode.AccessReject }; + }, + + accountingHandler: async (request) => { + console.log(`Session ${request.sessionId}: ${request.statusType}`); + return { success: true }; + }, +}); + +await server.start(); +``` + +## Low-Level Packet Operations + +```typescript +import { + RadiusPacket, + RadiusAuthenticator, + RadiusAttributes, + ERadiusAttributeType, +} from '@push.rocks/smartradius'; + +// Decode incoming packet +const packet = RadiusPacket.decodeAndParse(buffer); + +// Encrypt PAP password +const encrypted = RadiusAuthenticator.encryptPassword( + password, authenticator, secret +); + +// Verify CHAP response +const valid = RadiusAuthenticator.verifyChapResponse( + chapPassword, challenge, expectedPassword +); + +// Create Vendor-Specific Attribute +const vsa = RadiusAttributes.createVendorAttribute( + 9, // Cisco vendor ID + 1, // Vendor type + Buffer.from('value') +); +``` + +## Server Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `authPort` | number | 1812 | Authentication port | +| `acctPort` | number | 1813 | Accounting port | +| `bindAddress` | string | '0.0.0.0' | Address to bind to | +| `defaultSecret` | string | - | Default shared secret | +| `secretResolver` | function | - | Per-client secret resolver | +| `duplicateDetectionWindow` | number | 10000 | Duplicate detection window (ms) | +| `maxPacketSize` | number | 4096 | Maximum packet size | + +## Re-exports + +This module re-exports all types from `ts_shared` for convenience, so you can import everything from a single location. diff --git a/ts_server/tspublish.json b/ts_server/tspublish.json new file mode 100644 index 0000000..e6d80aa --- /dev/null +++ b/ts_server/tspublish.json @@ -0,0 +1 @@ +{ "order": 2 } diff --git a/ts_shared/enums.ts b/ts_shared/enums.ts new file mode 100644 index 0000000..8224fca --- /dev/null +++ b/ts_shared/enums.ts @@ -0,0 +1,222 @@ +/** + * RADIUS Protocol Enums + * Based on RFC 2865 (Authentication) and RFC 2866 (Accounting) + */ + +/** + * RADIUS Packet Codes (RFC 2865 Section 3) + */ +export enum ERadiusCode { + AccessRequest = 1, + AccessAccept = 2, + AccessReject = 3, + AccountingRequest = 4, + AccountingResponse = 5, + AccessChallenge = 11, + StatusServer = 12, // Experimental + StatusClient = 13, // Experimental +} + +/** + * RADIUS Attribute Types (RFC 2865 Section 5) + */ +export enum ERadiusAttributeType { + UserName = 1, + UserPassword = 2, + ChapPassword = 3, + NasIpAddress = 4, + NasPort = 5, + ServiceType = 6, + FramedProtocol = 7, + FramedIpAddress = 8, + FramedIpNetmask = 9, + FramedRouting = 10, + FilterId = 11, + FramedMtu = 12, + FramedCompression = 13, + LoginIpHost = 14, + LoginService = 15, + LoginTcpPort = 16, + ReplyMessage = 18, + CallbackNumber = 19, + CallbackId = 20, + FramedRoute = 22, + FramedIpxNetwork = 23, + State = 24, + Class = 25, + VendorSpecific = 26, + SessionTimeout = 27, + IdleTimeout = 28, + TerminationAction = 29, + CalledStationId = 30, + CallingStationId = 31, + NasIdentifier = 32, + ProxyState = 33, + LoginLatService = 34, + LoginLatNode = 35, + LoginLatGroup = 36, + FramedAppleTalkLink = 37, + FramedAppleTalkNetwork = 38, + FramedAppleTalkZone = 39, + ChapChallenge = 60, + NasPortType = 61, + PortLimit = 62, + LoginLatPort = 63, + // Accounting attributes (RFC 2866) + AcctStatusType = 40, + AcctDelayTime = 41, + AcctInputOctets = 42, + AcctOutputOctets = 43, + AcctSessionId = 44, + AcctAuthentic = 45, + AcctSessionTime = 46, + AcctInputPackets = 47, + AcctOutputPackets = 48, + AcctTerminateCause = 49, + AcctMultiSessionId = 50, + AcctLinkCount = 51, + // EAP support + EapMessage = 79, + MessageAuthenticator = 80, +} + +/** + * Service-Type values (RFC 2865 Section 5.6) + */ +export enum EServiceType { + Login = 1, + Framed = 2, + CallbackLogin = 3, + CallbackFramed = 4, + Outbound = 5, + Administrative = 6, + NasPrompt = 7, + AuthenticateOnly = 8, + CallbackNasPrompt = 9, + CallCheck = 10, + CallbackAdministrative = 11, +} + +/** + * Framed-Protocol values (RFC 2865 Section 5.7) + */ +export enum EFramedProtocol { + Ppp = 1, + Slip = 2, + Arap = 3, + Gandalf = 4, + Xylogics = 5, + X75 = 6, +} + +/** + * Framed-Routing values (RFC 2865 Section 5.10) + */ +export enum EFramedRouting { + None = 0, + Send = 1, + Listen = 2, + SendAndListen = 3, +} + +/** + * Framed-Compression values (RFC 2865 Section 5.13) + */ +export enum EFramedCompression { + None = 0, + VjTcpIp = 1, + IpxHeaderCompression = 2, + StacLzs = 3, +} + +/** + * Login-Service values (RFC 2865 Section 5.15) + */ +export enum ELoginService { + Telnet = 0, + Rlogin = 1, + TcpClear = 2, + PortMaster = 3, + Lat = 4, + X25Pad = 5, + X25T3Pos = 6, + TcpClearQuiet = 8, +} + +/** + * Termination-Action values (RFC 2865 Section 5.29) + */ +export enum ETerminationAction { + Default = 0, + RadiusRequest = 1, +} + +/** + * NAS-Port-Type values (RFC 2865 Section 5.41) + */ +export enum ENasPortType { + Async = 0, + Sync = 1, + IsdnSync = 2, + IsdnAsyncV120 = 3, + IsdnAsyncV110 = 4, + Virtual = 5, + Piafs = 6, + HdlcClearChannel = 7, + X25 = 8, + X75 = 9, + G3Fax = 10, + Sdsl = 11, + AdslCap = 12, + AdslDmt = 13, + Idsl = 14, + Ethernet = 15, + Xdsl = 16, + Cable = 17, + WirelessOther = 18, + WirelessIeee80211 = 19, +} + +/** + * Acct-Status-Type values (RFC 2866 Section 5.1) + */ +export enum EAcctStatusType { + Start = 1, + Stop = 2, + InterimUpdate = 3, + AccountingOn = 7, + AccountingOff = 8, +} + +/** + * Acct-Authentic values (RFC 2866 Section 5.6) + */ +export enum EAcctAuthentic { + Radius = 1, + Local = 2, + Remote = 3, +} + +/** + * Acct-Terminate-Cause values (RFC 2866 Section 5.10) + */ +export enum EAcctTerminateCause { + UserRequest = 1, + LostCarrier = 2, + LostService = 3, + IdleTimeout = 4, + SessionTimeout = 5, + AdminReset = 6, + AdminReboot = 7, + PortError = 8, + NasError = 9, + NasRequest = 10, + NasReboot = 11, + PortUnneeded = 12, + PortPreempted = 13, + PortSuspended = 14, + ServiceUnavailable = 15, + Callback = 16, + UserError = 17, + HostRequest = 18, +} diff --git a/ts_shared/index.ts b/ts_shared/index.ts new file mode 100644 index 0000000..246fa62 --- /dev/null +++ b/ts_shared/index.ts @@ -0,0 +1,5 @@ +// RADIUS Protocol Shared Module +// Contains RFC 2865/2866 protocol definitions used by both server and client + +export * from './enums.js'; +export * from './interfaces.js'; diff --git a/ts_shared/interfaces.ts b/ts_shared/interfaces.ts new file mode 100644 index 0000000..68d4d48 --- /dev/null +++ b/ts_shared/interfaces.ts @@ -0,0 +1,65 @@ +/** + * RADIUS Protocol Core Interfaces + * Based on RFC 2865 (Authentication) and RFC 2866 (Accounting) + */ + +import { ERadiusCode } from './enums.js'; + +/** + * Attribute value type + */ +export type TAttributeValueType = 'text' | 'string' | 'address' | 'integer' | 'time' | 'vsa'; + +/** + * Attribute definition + */ +export interface IAttributeDefinition { + type: number; + name: string; + valueType: TAttributeValueType; + encrypted?: boolean; +} + +/** + * Raw attribute (type-length-value) + */ +export interface IRadiusAttribute { + type: number; + value: Buffer; +} + +/** + * Parsed attribute with named value + */ +export interface IParsedAttribute { + type: number; + name: string; + value: string | number | Buffer; + rawValue: Buffer; +} + +/** + * Vendor-Specific Attribute + */ +export interface IVendorSpecificAttribute { + vendorId: number; + vendorType: number; + vendorValue: Buffer; +} + +/** + * RADIUS Packet structure + */ +export interface IRadiusPacket { + code: ERadiusCode; + identifier: number; + authenticator: Buffer; + attributes: IRadiusAttribute[]; +} + +/** + * Parsed RADIUS packet with named attributes + */ +export interface IParsedRadiusPacket extends IRadiusPacket { + parsedAttributes: IParsedAttribute[]; +} diff --git a/ts_shared/readme.md b/ts_shared/readme.md new file mode 100644 index 0000000..f7cabd2 --- /dev/null +++ b/ts_shared/readme.md @@ -0,0 +1,81 @@ +# @push.rocks/smartradius/shared + +> ๐Ÿ“ก RADIUS Protocol Definitions - Shared types and enums for RFC 2865/2866 compliance + +## Overview + +This module contains the core RADIUS protocol definitions shared between the server and client implementations. It provides all the RFC 2865 (Authentication) and RFC 2866 (Accounting) enums and interfaces that represent the RADIUS protocol. + +## Contents + +### Enums (`enums.ts`) + +All RFC-compliant protocol constants: + +| Enum | Description | RFC Reference | +|------|-------------|---------------| +| `ERadiusCode` | Packet types (Access-Request, Access-Accept, etc.) | RFC 2865 ยง3 | +| `ERadiusAttributeType` | Standard attribute types (1-63, 79-80) | RFC 2865 ยง5 | +| `EServiceType` | Service-Type attribute values | RFC 2865 ยง5.6 | +| `EFramedProtocol` | Framed-Protocol values | RFC 2865 ยง5.7 | +| `EFramedRouting` | Framed-Routing values | RFC 2865 ยง5.10 | +| `EFramedCompression` | Framed-Compression values | RFC 2865 ยง5.13 | +| `ELoginService` | Login-Service values | RFC 2865 ยง5.15 | +| `ETerminationAction` | Termination-Action values | RFC 2865 ยง5.29 | +| `ENasPortType` | NAS-Port-Type values | RFC 2865 ยง5.41 | +| `EAcctStatusType` | Acct-Status-Type values | RFC 2866 ยง5.1 | +| `EAcctAuthentic` | Acct-Authentic values | RFC 2866 ยง5.6 | +| `EAcctTerminateCause` | Acct-Terminate-Cause values | RFC 2866 ยง5.10 | + +### Interfaces (`interfaces.ts`) + +Core data structures representing RADIUS protocol elements: + +| Interface | Description | +|-----------|-------------| +| `TAttributeValueType` | Value type union ('text', 'string', 'address', 'integer', 'time', 'vsa') | +| `IAttributeDefinition` | Attribute metadata (type, name, valueType, encrypted) | +| `IRadiusAttribute` | Raw TLV (type-length-value) format | +| `IParsedAttribute` | Parsed attribute with name and decoded value | +| `IVendorSpecificAttribute` | VSA format (vendorId, vendorType, vendorValue) | +| `IRadiusPacket` | Packet structure (code, identifier, authenticator, attributes) | +| `IParsedRadiusPacket` | Packet with parsed attributes | + +## Usage + +```typescript +import { + ERadiusCode, + ERadiusAttributeType, + EAcctStatusType, + IRadiusPacket, + IParsedAttribute, +} from '@push.rocks/smartradius'; + +// Check packet type +if (packet.code === ERadiusCode.AccessRequest) { + // Handle authentication request +} + +// Check accounting status +if (request.statusType === EAcctStatusType.Start) { + // Session started +} + +// Access attribute by type +const username = packet.attributes.find( + a => a.type === ERadiusAttributeType.UserName +); +``` + +## Why a Shared Module? + +This module provides clear separation between: +- **Protocol definitions** (what RADIUS is) - lives here +- **Server implementation** (how to serve RADIUS) - lives in ts_server +- **Client implementation** (how to consume RADIUS) - lives in ts_client + +This separation provides: +- ๐ŸŽฏ Better semantic clarity for API consumers +- ๐Ÿ“ฆ Smaller imports when only types are needed +- ๐Ÿ”„ Clean dependency graph (shared โ†’ server/client โ†’ main) diff --git a/ts_shared/tspublish.json b/ts_shared/tspublish.json new file mode 100644 index 0000000..5f81df6 --- /dev/null +++ b/ts_shared/tspublish.json @@ -0,0 +1 @@ +{ "order": 1 }