Skip to content

gibme-npm/radius

Repository files navigation

@gibme/radius

A TypeScript library for the RADIUS protocol (RFC 2865/RFC 2866) with support for packet parsing, construction, and serialization. Includes a UDP server and client, FreeRADIUS dictionary-based attribute resolution, vendor-specific attributes (VSAs), extended attributes, TLV nesting, struct decoding, and three password encryption modes.

Requirements

  • Node.js >= 22

Installation

yarn add @gibme/radius
# or
npm install @gibme/radius

Quick Start

import { Packet } from '@gibme/radius';

// Decode an incoming UDP payload
const packet = Packet.from({
    bytes: udpPayload,
    secret: 'shared-secret'
});

console.log(packet.type);          // Packet.Type.ACCESS_REQUEST
console.log(packet.authenticated); // true if Message-Authenticator verified
console.log(packet.attributes);    // { 'User-Name': 'alice', 'NAS-IP-Address': '10.0.0.1', ... }

Usage

Decoding a Packet

Packet.from() decodes a RADIUS packet from raw bytes. Pass the shared secret to verify the Message-Authenticator (HMAC-MD5 with timing-safe comparison) and decrypt encrypted attributes.

const packet = Packet.from({
    bytes: udpPayload,       // Buffer, Uint8Array, or hex string
    secret: 'shared-secret'
});

For response packets, pass the original request's authenticator so the response authenticator and Message-Authenticator HMAC can be verified:

const response = Packet.from({
    bytes: responseBytes,
    secret: 'shared-secret',
    requestor: request.authenticator
});

Decoding without a secret is supported for inspecting unencrypted attributes, though authenticated will be false:

const packet = Packet.from({ bytes: udpPayload });
console.log(packet.authenticated); // false
console.log(packet.attributes['NAS-IP-Address']); // still accessible

authenticated is a convenience boolean (true only when the HMAC verifies). To tell why a packet did not verify, read packet.messageAuthenticator, which reports one of four states:

messageAuthenticator Meaning
'verified' Attribute 80 present and the HMAC-MD5 matched (authenticated === true).
'mismatch' Attribute 80 present but the HMAC-MD5 did not match — wrong shared secret or tampering.
'absent' No Message-Authenticator attribute in the packet (common for legacy CHAP/PAP requests).
'unchecked' No shared secret was supplied, so it could not be verified.
const packet = Packet.from({ bytes: udpPayload, secret: 'shared-secret' });
if (packet.messageAuthenticator === 'absent') {
    // the client never sent attribute 80 — not a wrong-secret problem
}

Response authentication policy

This library enforces Message-Authenticator (RFC 2869 Section 5.14, HMAC-MD5) for all authenticated packets. Responses without a Message-Authenticator attribute decode with authenticated = false and are dropped by Server and Client. The library does not validate the RFC 2865 Section 3 Response Authenticator separately because Message-Authenticator's HMAC-MD5 is cryptographically stronger than plain MD5 and mitigates the Response-Authenticator collision attack class (BlastRADIUS / CVE-2024-3596). Consumers integrating with RADIUS servers that do not emit Message-Authenticator will need to enable it server-side or add their own Response Authenticator check.

Relaxing enforcement for legacy peers

Both Server and Client accept two opt-in flags for interoperating with equipment that does not send a Message-Authenticator (e.g. legacy NAS gear doing CHAP/PAP). Both default to false, preserving the strict enforcement above.

Option Effect when true
allowMissingMessageAuthenticator A packet with no attribute 80 is emitted on packet instead of discard.
allowMismatchedMessageAuthenticator A packet whose attribute 80 fails HMAC verification is emitted on packet instead of discard.

When either flag lets a non-verified packet through, it arrives on the normal packet event; inspect packet.messageAuthenticator to decide how to handle it. Packets that are still rejected fire discard with a specific error (Message-Authenticator missing or Message-Authenticator mismatch).

Security note: enabling these flags weakens the CVE-2024-3596 (BlastRADIUS) mitigation. Only enable them when you must interoperate with peers that cannot send a Message-Authenticator, and treat the resulting packets accordingly.

const server = await Server.create({
    secret: 'shared-secret',
    allowMissingMessageAuthenticator: true // accept legacy CHAP/PAP requests lacking attr 80
});

server.on('packet', (packet, remoteInfo) => {
    if (packet.messageAuthenticator !== 'verified') {
        // arrived only because an allow* flag is set — apply extra scrutiny
    }
});

Constructing a Packet

Packet.construct() builds a new RADIUS packet. When a secret is provided, a Message-Authenticator attribute is automatically added and computed during serialization.

const packet = Packet.construct({
    type: Packet.Type.ACCESS_REQUEST,
    secret: 'shared-secret',
    attributes: [
        ['User-Name', 'alice'],
        ['User-Password', 'hunter2'],
        ['NAS-IP-Address', '192.168.1.1'],
        ['Service-Type', 'Framed-User']
    ]
});

const wireBytes = packet.toBuffer();

Tagged attributes (e.g., tunnel attributes per RFC 2868) use a 3-element tuple [name, value, tag]:

const reply = Packet.construct({
    type: Packet.Type.ACCESS_ACCEPT,
    secret: 'shared-secret',
    requestor: request.authenticator,
    attributes: [
        ['Tunnel-Type', 'VLAN', 0],
        ['Tunnel-Medium-Type', 'IEEE-802', 0],
        ['Tunnel-Private-Group-Id', '100', 0]
    ]
});

Attribute names are case-insensitive and resolved against the loaded FreeRADIUS dictionary. Enum values can be passed by name (e.g., 'Framed-User') or numeric code.

Replying to a Request

The reply() convenience method on a decoded packet automatically sets the identifier, secret, and request authenticator:

const response = request.reply({
    type: Packet.Type.ACCESS_ACCEPT,
    attributes: [
        ['Reply-Message', 'Welcome!'],
        ['Session-Timeout', 3600]
    ]
});

const responseBytes = response.toBuffer();

Accessing Attributes

Two accessors are available for decoded attribute values:

  • packet.attributes returns a Record<string, ParsedAttribute> where each key is the attribute name. When duplicate attributes exist, the last value wins.
  • packet.attributeList returns an ordered Array<{ name, value }> preserving duplicates and wire order, useful for multi-valued attributes like multiple Tunnel-Type entries.
// Last-one-wins record
const userName = packet.attributes['User-Name'];

// Ordered list preserving duplicates
const allTunnelTypes = packet.attributeList
    .filter(a => a.name === 'Tunnel-Type');

Server

The Server class listens for incoming RADIUS requests over UDP. Incoming packets are verified for authenticity before being emitted. Malformed or unauthenticated packets are discarded with a 'discard' event for observability, unless the allowMissingMessageAuthenticator / allowMismatchedMessageAuthenticator options are set (see Relaxing enforcement for legacy peers).

import { Server, Packet } from '@gibme/radius';

const server = await Server.create({
    secret: 'shared-secret',
    port: 1812                // default
});

server.on('packet', (packet, remoteInfo) => {
    console.log(`${remoteInfo.address}: ${packet.attributes['User-Name']}`);

    const reply = packet.reply({
        type: Packet.Type.ACCESS_ACCEPT,
        attributes: [
            ['Reply-Message', 'Authenticated']
        ]
    });

    server.send(reply, { address: remoteInfo.address, port: remoteInfo.port });
});

server.on('discard', (error, message, remoteInfo) => {
    console.error(`Discarded packet from ${remoteInfo.address}: ${error.message}`);
});

Client

The Client class sends RADIUS requests and correlates responses by destination, port, and identifier. Unanswered requests emit a 'timeout' event (default 10 seconds).

import { Client, Packet } from '@gibme/radius';

const client = await Client.create({
    secret: 'shared-secret',
    timeout: 5000              // ms before timeout event
});

client.on('packet', (packet, remoteInfo) => {
    console.log(`Response: ${packet.type}`);
    console.log(packet.attributes);
});

client.on('timeout', (info) => {
    console.log(`No response from ${info.address}:${info.port}`);
});

await client.send(
    {
        type: Packet.Type.ACCESS_REQUEST,
        attributes: [
            ['User-Name', 'alice'],
            ['User-Password', 'secret123'],
            ['NAS-IP-Address', '192.168.1.1']
        ]
    },
    { address: '10.0.0.1', port: 1812 }
);

// Clean up when done
await client.destroy();

Custom Dictionary

Additional FreeRADIUS-compatible dictionary files can be loaded at runtime. The bundled dictionaries are loaded automatically at module initialization.

import { load_dictionary } from '@gibme/radius';

load_dictionary('/path/to/custom/dictionary');

Supported Data Types

Type Description
string UTF-8 text
octets Raw byte sequence
ipaddr IPv4 address
ipv6addr IPv6 address
ipv4prefix IPv4 prefix with mask length
ipv6prefix IPv6 prefix with mask length
integer / uint32 32-bit unsigned integer
uint64 64-bit unsigned integer (decoded as bigint)
short 16-bit unsigned integer
byte 8-bit unsigned integer
signed 32-bit signed integer
date Unix timestamp (decoded as Date)
ifid 8-byte interface identifier
combo-ip IPv4 (4 bytes) or IPv6 (16 bytes) address
ether 6-byte MAC address
struct Composite type with named fields
tlv Nested type-length-value container

Password Encryption

Three modes are supported, matching the FreeRADIUS encrypt= flag:

Mode Attribute Example Reference
Type 1 (User-Password) User-Password RFC 2865
Type 2 (Tunnel-Password) Tunnel-Password, Microsoft-MPPE-Send-Key RFC 2868
Type 3 (Ascend-Secret) Ascend-Send-Secret Ascend proprietary

Packet Types

Code Type Direction
1 Access-Request Client -> Server
2 Access-Accept Server -> Client
3 Access-Reject Server -> Client
4 Accounting-Request Client -> Server
5 Accounting-Response Server -> Client
11 Access-Challenge Server -> Client
12 Status-Server Client -> Server
13 Status-Client Server -> Client
52 Protocol-Error Server -> Client

Dictionary

The bundled dictionary/ directory contains 100+ FreeRADIUS-compatible dictionary files covering standard RFC attributes and vendor-specific attributes from manufacturers including Cisco, Juniper, Ruckus, Fortinet, Microsoft, Nokia, and many more. Dictionaries are loaded automatically at module initialization.

The bundled dictionaries are loaded recursively by directory scan at module initialization. Lines beginning with $ (including $INCLUDE) are not interpreted by the parser; to load additional dictionary files at runtime, pass a directory path to load_dictionary and every file matching dictionary* under that path is loaded. Nested TLV hierarchies, extended attributes (types 241-246), struct members, and per-vendor wire format specifications (non-standard type/length field sizes, continuation bytes) are fully supported.

License

MIT

About

RADIUS protocol library for TypeScript/Node.js with packet parsing, construction, UDP server/client, FreeRADIUS dictionary support, vendor-specific attributes, and password encryption

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors