Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions src/dictionary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
// Standard RADIUS Attributes from RFC 2865, 2866, and others.
// Format: ID: { name, type }

export type AttributeType =
| 'string'
| 'integer'
| 'ipaddr'
| 'ipv6addr'
| 'date'
| 'ifid'
| 'integer64'
| 'ipv6prefix';

export interface AttributeDefinition {
name: string;
type: AttributeType;
}

export const RADIUS_ATTRIBUTES: Record<number, AttributeDefinition> = {
1: { name: 'User-Name', type: 'string' },
2: { name: 'User-Password', type: 'string' }, // Special handling in protocol usually, but string representation
3: { name: 'CHAP-Password', type: 'string' },
4: { name: 'NAS-IP-Address', type: 'ipaddr' },
5: { name: 'NAS-Port', type: 'integer' },
6: { name: 'Service-Type', type: 'integer' },
7: { name: 'Framed-Protocol', type: 'integer' },
8: { name: 'Framed-IP-Address', type: 'ipaddr' },
9: { name: 'Framed-IP-Netmask', type: 'ipaddr' },
10: { name: 'Framed-Routing', type: 'integer' },
11: { name: 'Filter-Id', type: 'string' },
12: { name: 'Framed-MTU', type: 'integer' },
13: { name: 'Framed-Compression', type: 'integer' },
14: { name: 'Login-IP-Host', type: 'ipaddr' },
15: { name: 'Login-Service', type: 'integer' },
16: { name: 'Login-TCP-Port', type: 'integer' },
// 17 (unassigned)
18: { name: 'Reply-Message', type: 'string' },
19: { name: 'Callback-Number', type: 'string' },
20: { name: 'Callback-Id', type: 'string' },
// 21 (unassigned)
22: { name: 'Framed-Route', type: 'string' },
23: { name: 'Framed-IPX-Network', type: 'integer' },
24: { name: 'State', type: 'string' },
25: { name: 'Class', type: 'string' },
26: { name: 'Vendor-Specific', type: 'string' }, // Special handling
27: { name: 'Session-Timeout', type: 'integer' },
28: { name: 'Idle-Timeout', type: 'integer' },
29: { name: 'Termination-Action', type: 'integer' },
30: { name: 'Called-Station-Id', type: 'string' },
31: { name: 'Calling-Station-Id', type: 'string' },
32: { name: 'NAS-Identifier', type: 'string' },
33: { name: 'Proxy-State', type: 'string' },
34: { name: 'Login-LAT-Service', type: 'string' },
35: { name: 'Login-LAT-Node', type: 'string' },
36: { name: 'Login-LAT-Group', type: 'string' },
37: { name: 'Framed-AppleTalk-Link', type: 'integer' },
38: { name: 'Framed-AppleTalk-Network', type: 'integer' },
39: { name: 'Framed-AppleTalk-Zone', type: 'string' },
// 40-59 (Accounting)
40: { name: 'Acct-Status-Type', type: 'integer' },
41: { name: 'Acct-Delay-Time', type: 'integer' },
42: { name: 'Acct-Input-Octets', type: 'integer' },
43: { name: 'Acct-Output-Octets', type: 'integer' },
44: { name: 'Acct-Session-Id', type: 'string' },
45: { name: 'Acct-Authentic', type: 'integer' },
46: { name: 'Acct-Session-Time', type: 'integer' },
47: { name: 'Acct-Input-Packets', type: 'integer' },
48: { name: 'Acct-Output-Packets', type: 'integer' },
49: { name: 'Acct-Terminate-Cause', type: 'integer' },
50: { name: 'Acct-Multi-Session-Id', type: 'string' },
51: { name: 'Acct-Link-Count', type: 'integer' },
52: { name: 'Acct-Input-Gigawords', type: 'integer' },
53: { name: 'Acct-Output-Gigawords', type: 'integer' },
// 54 (unassigned)
55: { name: 'Event-Timestamp', type: 'date' },
// 56-59 (unassigned)
60: { name: 'CHAP-Challenge', type: 'string' },
61: { name: 'NAS-Port-Type', type: 'integer' },
62: { name: 'Port-Limit', type: 'integer' },
63: { name: 'Login-LAT-Port', type: 'string' },
// 64 (Tunneling)
64: { name: 'Tunnel-Type', type: 'integer' },
65: { name: 'Tunnel-Medium-Type', type: 'integer' },
66: { name: 'Tunnel-Client-Endpoint', type: 'string' },
67: { name: 'Tunnel-Server-Endpoint', type: 'string' },
68: { name: 'Acct-Tunnel-Connection', type: 'string' },
69: { name: 'Tunnel-Password', type: 'string' },
70: { name: 'ARAP-Password', type: 'string' },
71: { name: 'ARAP-Features', type: 'string' },
72: { name: 'ARAP-Zone-Access', type: 'integer' },
73: { name: 'ARAP-Security', type: 'integer' },
74: { name: 'ARAP-Security-Data', type: 'string' },
75: { name: 'Password-Retry', type: 'integer' },
76: { name: 'Prompt', type: 'integer' },
77: { name: 'Connect-Info', type: 'string' },
78: { name: 'Configuration-Token', type: 'string' },
79: { name: 'EAP-Message', type: 'string' },
80: { name: 'Message-Authenticator', type: 'string' }, // 16 bytes
81: { name: 'Tunnel-Private-Group-Id', type: 'string' },
82: { name: 'Tunnel-Assignment-Id', type: 'string' },
83: { name: 'Tunnel-Preference', type: 'integer' },
84: { name: 'ARAP-Challenge-Response', type: 'string' },
85: { name: 'Acct-Interim-Interval', type: 'integer' },
86: { name: 'Acct-Tunnel-Packets-Lost', type: 'integer' },
87: { name: 'NAS-Port-Id', type: 'string' },
88: { name: 'Framed-Pool', type: 'string' },
89: { name: 'CUI', type: 'string' },
90: { name: 'Tunnel-Client-Auth-Id', type: 'string' },
91: { name: 'Tunnel-Server-Auth-Id', type: 'string' },
// 92-93 (unassigned)
94: { name: 'Originating-Line-Info', type: 'string' },
95: { name: 'NAS-IPv6-Address', type: 'ipv6addr' },
96: { name: 'Framed-Interface-Id', type: 'ifid' },
97: { name: 'Framed-IPv6-Prefix', type: 'ipv6prefix' },
98: { name: 'Login-IPv6-Host', type: 'ipv6addr' },
99: { name: 'Framed-IPv6-Route', type: 'string' },
100: { name: 'Framed-IPv6-Pool', type: 'string' },
101: { name: 'Error-Cause', type: 'integer' },
102: { name: 'EAP-Key-Name', type: 'string' },
103: { name: 'Digest-Response', type: 'string' },
104: { name: 'Digest-Realm', type: 'string' },
105: { name: 'Digest-Nonce', type: 'string' },
106: { name: 'Digest-Response-Auth', type: 'string' },
107: { name: 'Digest-Nextnonce', type: 'string' },
108: { name: 'Digest-Method', type: 'string' },
109: { name: 'Digest-URI', type: 'string' },
110: { name: 'Digest-Qop', type: 'string' },
111: { name: 'Digest-Algorithm', type: 'string' },
112: { name: 'Digest-Entity-Body-Hash', type: 'string' },
113: { name: 'Digest-CNonce', type: 'string' },
114: { name: 'Digest-Nonce-Count', type: 'string' },
115: { name: 'Digest-Username', type: 'string' },
116: { name: 'Digest-Opaque', type: 'string' },
117: { name: 'Digest-Auth-Param', type: 'string' },
118: { name: 'Digest-AKA-Auts', type: 'string' },
119: { name: 'Digest-Domain', type: 'string' },
120: { name: 'Digest-Stale', type: 'string' },
121: { name: 'Digest-HA1', type: 'string' },
122: { name: 'SIP-AOR', type: 'string' },
123: { name: 'Delegated-IPv6-Prefix', type: 'ipv6prefix' },
124: { name: 'MIP6-Feature-Vector', type: 'integer' },
125: { name: 'MIP6-Home-Link-Prefix', type: 'ipv6prefix' },
126: { name: 'Operator-Name', type: 'string' },
127: { name: 'Location-Info', type: 'string' },
128: { name: 'Location-Data', type: 'string' },
129: { name: 'Basic-Location-Policy-Rules', type: 'string' },
130: { name: 'Extended-Location-Policy-Rules', type: 'string' },
131: { name: 'Location-Capable', type: 'integer' },
132: { name: 'Requested-Location-Info', type: 'string' },
133: { name: 'Framed-Management-Protocol', type: 'string' },
134: { name: 'Management-Transport-Protection', type: 'integer' },
135: { name: 'Management-Policy-Id', type: 'string' },
136: { name: 'Management-Privilege-Level', type: 'integer' },
137: { name: 'PKM-SS-Cert', type: 'string' },
138: { name: 'PKM-CA-Cert', type: 'string' },
139: { name: 'PKM-Config-Settings', type: 'string' },
140: { name: 'PKM-Cryptosuite-List', type: 'string' },
141: { name: 'PKM-SAID', type: 'string' },
142: { name: 'PKM-SA-Descriptor', type: 'string' },
143: { name: 'PKM-Auth-Key', type: 'string' },
144: { name: 'DS-Lite-Tunnel-Name', type: 'string' },
145: { name: 'Mobile-Node-Identifier', type: 'string' },
146: { name: 'Service-Selection', type: 'string' },
147: { name: 'PMIP6-Home-LMA-IPv6-Address', type: 'ipv6addr' },
148: { name: 'PMIP6-Visited-LMA-IPv6-Address', type: 'ipv6addr' },
149: { name: 'PMIP6-Home-LMA-IPv4-Address', type: 'ipaddr' },
150: { name: 'PMIP6-Visited-LMA-IPv4-Address', type: 'ipaddr' },
151: { name: 'PMIP6-Home-HN-Prefix', type: 'ipv6prefix' },
152: { name: 'PMIP6-Visited-HN-Prefix', type: 'ipv6prefix' },
153: { name: 'PMIP6-Home-Interface-ID', type: 'ifid' },
154: { name: 'PMIP6-Visited-Interface-ID', type: 'ifid' },
155: { name: 'PMIP6-Home-HNP-PMIP6', type: 'ipv6prefix' },
156: { name: 'PMIP6-Visited-HNP-PMIP6', type: 'ipv6prefix' },
157: { name: 'GGSN-Address', type: 'ipaddr' },
158: { name: 'GGSN-IPv6-Address', type: 'ipv6addr' },
159: { name: '3GPP-IMSI', type: 'string' },
160: { name: '3GPP-Charging-Id', type: 'integer' },
161: { name: '3GPP-PDP-Type', type: 'integer' },
162: { name: '3GPP-CG-Address', type: 'ipaddr' },
163: { name: '3GPP-GPRS-Negotiated-QoS-Profile', type: 'string' },
164: { name: '3GPP-SGSN-Address', type: 'ipaddr' },
165: { name: '3GPP-GGSN-Address', type: 'ipaddr' },
166: { name: '3GPP-IMSI-MCC-MNC', type: 'string' },
167: { name: '3GPP-GGSN-MCC-MNC', type: 'string' },
168: { name: '3GPP-NSAPI', type: 'integer' },
169: { name: '3GPP-Session-Stop-Indicator', type: 'string' }, // boolean flag really
170: { name: '3GPP-Selection-Mode', type: 'integer' },
171: { name: '3GPP-Charging-Characteristics', type: 'string' },
172: { name: '3GPP-CG-IPv6-Address', type: 'ipv6addr' },
173: { name: '3GPP-SGSN-IPv6-Address', type: 'ipv6addr' },
174: { name: '3GPP-GGSN-IPv6-Address', type: 'ipv6addr' },
175: { name: '3GPP-IPv6-DNS-Servers', type: 'ipv6addr' },
176: { name: '3GPP-SGSN-MCC-MNC', type: 'string' },
177: { name: '3GPP-Teardown-Indicator', type: 'string' },
178: { name: '3GPP-IMEISV', type: 'string' },
179: { name: '3GPP-RAT-Type', type: 'integer' },
180: { name: '3GPP-User-Location-Info', type: 'string' },
181: { name: '3GPP-MS-TimeZone', type: 'string' },
182: { name: '3GPP-Camel-Charging-Info', type: 'string' },
183: { name: '3GPP-Packet-Filter', type: 'string' },
184: { name: '3GPP-Negotiated-DSCP', type: 'integer' },
185: { name: '3GPP-Allocate-IP-Type', type: 'integer' },
186: { name: '3GPP-Delivery-Order', type: 'integer' },
187: { name: '3GPP-Traffic-Class', type: 'integer' },
188: { name: '3GPP-Maximum-Bitrate-Uplink', type: 'integer' },
189: { name: '3GPP-Maximum-Bitrate-Downlink', type: 'integer' },
190: { name: '3GPP-Guaranteed-Bitrate-Uplink', type: 'integer' },
191: { name: '3GPP-Guaranteed-Bitrate-Downlink', type: 'integer' },
192: { name: 'EAP-Payload', type: 'string' },
193: { name: 'EAP-Reissued-Payload', type: 'string' },
194: { name: 'EAP-Master-Session-Key', type: 'string' },
195: { name: 'EAP-Keying-Material', type: 'string' },
196: { name: 'EAP-Authorization-Data', type: 'string' },
197: { name: 'EAP-Session-Id', type: 'string' },
198: { name: 'EAP-Session-Support', type: 'integer' },
199: { name: 'EAP-Peer-Id', type: 'string' },
200: { name: 'EAP-Peer-Id-Type', type: 'integer' },
201: { name: 'EAP-TLS-Method-Type', type: 'integer' },
202: { name: 'EAP-TLS-Start', type: 'integer' },
203: { name: 'EAP-TLS-Finish', type: 'integer' },
204: { name: 'EAP-TLS-Verify', type: 'integer' },
205: { name: 'EAP-TLS-Require-Client-Cert', type: 'integer' },
206: { name: 'EAP-TLS-Require-Client-Cert-Type', type: 'integer' },
// ... and so on. This covers a large swath.
};
170 changes: 170 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { RADIUS_ATTRIBUTES, type AttributeDefinition } from "./dictionary";
import type { ParsedRadiusAttribute, ParsedAttribute, VendorSpecificAttribute } from "./types";

export function decodeString(buffer: Buffer): string {
return buffer.toString("utf8");
}

export function decodeInteger(buffer: Buffer): number {
if (buffer.length !== 4) return 0;
return buffer.readUInt32BE(0);
}

export function decodeInteger64(buffer: Buffer): bigint {
if (buffer.length !== 8) return BigInt(0);
return buffer.readBigUInt64BE(0);
}

export function decodeDate(buffer: Buffer): Date {
if (buffer.length !== 4) return new Date(0);
const seconds = buffer.readUInt32BE(0);
return new Date(seconds * 1000);
}

export function decodeIpAddr(buffer: Buffer): string {
if (buffer.length !== 4) return "0.0.0.0";
return `${buffer[0]}.${buffer[1]}.${buffer[2]}.${buffer[3]}`;
}

export function decodeIpv6Addr(buffer: Buffer): string {
if (buffer.length !== 16) return "::";
const parts: string[] = [];
for (let i = 0; i < 16; i += 2) {
parts.push(buffer.readUInt16BE(i).toString(16));
}
return parts.join(":");
}

export function decodeIpv6Prefix(buffer: Buffer): string {
if (buffer.length < 2) return "";
// buffer[0] is reserved
const prefixLength = buffer[1];
const prefixBuffer = buffer.slice(2);

const fullAddress = Buffer.alloc(16, 0);
prefixBuffer.copy(fullAddress);

const parts: string[] = [];
for (let i = 0; i < 16; i += 2) {
parts.push(fullAddress.readUInt16BE(i).toString(16));
}
return `${parts.join(":")}/${prefixLength}`;
}

export function decodeIfId(buffer: Buffer): string {
if (buffer.length !== 8) return buffer.toString("hex");
const parts: string[] = [];
for (let i = 0; i < 8; i++) {
parts.push(buffer[i].toString(16).padStart(2, "0"));
}
return parts.join(":");
}

export function decodeVendorSpecific(value: Buffer): VendorSpecificAttribute {
if (value.length < 4) {
return {
id: 26,
name: "Vendor-Specific",
vendorId: 0,
value: value.toString("hex"),
raw: value.toString("hex")
};
}

const vendorId = value.readUInt32BE(0);
const data = value.slice(4);

// Attempt to parse sub-attributes
const subAttributes: { vendorType: number; value: string }[] = [];
let offset = 0;
let parsable = true;

// Generic VSA parsing: Type (1 byte), Length (1 byte), Value (Length-2)
while (offset < data.length) {
if (offset + 2 > data.length) {
parsable = false;
break;
}
const t = data[offset];
const l = data[offset + 1];

if (l < 2 || offset + l > data.length) {
parsable = false;
break;
}

const val = data.slice(offset + 2, offset + l);
subAttributes.push({
vendorType: t,
value: val.toString("hex")
});
offset += l;
}

return {
id: 26,
name: "Vendor-Specific",
vendorId,
value: parsable && subAttributes.length > 0 ? subAttributes : data.toString("hex"),
raw: value.toString("hex")
};
}

export function decodeAttribute(id: number, value: Buffer): ParsedRadiusAttribute {
if (id === 26) {
return decodeVendorSpecific(value);
}

const def = RADIUS_ATTRIBUTES[id];

if (!def) {
return {
id,
name: `Unknown-Attribute-${id}`,
value: value.toString("hex"),
raw: value.toString("hex")
};
}

let decodedValue: any;

try {
switch (def.type) {
case "string":
decodedValue = decodeString(value);
break;
case "integer":
decodedValue = decodeInteger(value);
break;
case "integer64":
decodedValue = decodeInteger64(value);
break;
case "date":
decodedValue = decodeDate(value);
break;
case "ipaddr":
decodedValue = decodeIpAddr(value);
break;
case "ipv6addr":
decodedValue = decodeIpv6Addr(value);
break;
case "ipv6prefix":
decodedValue = decodeIpv6Prefix(value);
break;
case "ifid":
decodedValue = decodeIfId(value);
break;
default:
decodedValue = value.toString("hex");
}
} catch (e) {
decodedValue = value.toString("hex");
}

return {
id,
name: def.name,
value: decodedValue,
raw: value.toString("hex")
};
}
Loading