1use std::str;
2pub const CODE_ACCOUNTING_REQUEST: u8 = 4;
3pub const CODE_ACCOUNTING_RESPONSE: u8 = 5;
4
5pub const ATTR_ACCT_STATUS_TYPE: u8 = 40;
6pub const ATTR_ACCT_SESSION_ID: u8 = 44;
7pub const ATTR_ACCT_SESSION_TIME: u8 = 46;
8
9#[derive(Debug,Clone)]
10pub struct RadiusPacket {
11 pub code: u8,
12 pub identifier: u8,
13 pub length: u16,
14 pub authenticator: [u8; 16],
15 pub attributes: Vec<RadiusAttribute>,
16}
17
18#[derive(Debug,Clone)]
19pub struct RadiusAttribute {
20 pub typ: u8,
21 pub len: u8,
22 pub value: Vec<u8>,
23}
24
25impl RadiusAttribute {
26 pub fn reply_message(msg: &str) -> Self {
27 let value = msg.as_bytes().to_vec();
28 let len = (value.len() + 2) as u8;
29
30 RadiusAttribute {
31 typ: 18,
32 len,
33 value,
34 }
35 }
36
37 pub fn user_name(name: &str) -> Self {
38 let value = name.as_bytes().to_vec();
39 let len = (value.len() + 2) as u8;
40
41 RadiusAttribute {
42 typ: 1,
43 len,
44 value,
45 }
46 }
47
48 pub fn vendor_specific(vendor_id: u32, payload: Vec<u8>) -> Self {
49 let mut value = Vec::new();
50 value.extend_from_slice(&vendor_id.to_be_bytes());
51 value.extend(payload);
52
53 RadiusAttribute {
54 typ: 26,
55 len: (2 + value.len()) as u8,
56 value,
57 }
58 }
59
60 pub fn wispr_bandwidth_max_up(bps: u32) -> Self {
61 let mut value = Vec::new();
62 value.extend_from_slice(&14122u32.to_be_bytes());
63 value.push(7);
64 value.push(6);
65 value.extend_from_slice(&bps.to_be_bytes());
66
67 RadiusAttribute {
68 typ: 26,
69 len: (2 + value.len()) as u8,
70 value,
71 }
72 }
73
74 pub fn wispr_bandwidth_max_down(bps: u32) -> Self {
75 let mut value = Vec::new();
76 value.extend_from_slice(&14122u32.to_be_bytes());
77 value.push(8);
78 value.push(6);
79 value.extend_from_slice(&bps.to_be_bytes());
80
81 RadiusAttribute {
82 typ: 26,
83 len: (2 + value.len()) as u8,
84 value,
85 }
86 }
87
88 pub fn session_timeout(seconds: u32) -> Self {
89 RadiusAttribute {
90 typ: 27,
91 len: 6,
92 value: seconds.to_be_bytes().to_vec(),
93 }
94 }
95
96 pub fn idle_timeout(seconds: u32) -> Self {
97 RadiusAttribute {
98 typ: 28,
99 len: 6,
100 value: seconds.to_be_bytes().to_vec(),
101 }
102 }
103}
104
105impl RadiusPacket {
106 pub fn from_bytes(buf: &[u8]) -> Result<Self, String> {
107 if buf.len() < 20 {
108 return Err("Packet too short".to_string());
109 }
110
111 let code = buf[0];
112 let identifier = buf[1];
113 let length = u16::from_be_bytes([buf[2], buf[3]]) as usize;
114
115 if buf.len() < length {
116 return Err(format!(
117 "Length mismatch: header says {}, but got {} bytes",
118 length,
119 buf.len()
120 ));
121 }
122
123 let authenticator: [u8; 16] = buf[4..20].try_into().unwrap();
124 let mut attributes = Vec::new();
125 let mut i = 20;
126
127 while i < length {
128 if i + 2 > length {
129 return Err("Truncated attribute header".to_string());
130 }
131
132 let typ = buf[i];
133 let len = buf[i + 1] as usize;
134
135 if len < 2 || i + len > length {
136 return Err(format!(
137 "Invalid attribute length at index {}: len = {}",
138 i, len
139 ));
140 }
141
142 let value = buf[i + 2..i + len].to_vec();
143 attributes.push(RadiusAttribute {
144 typ,
145 len: len as u8,
146 value,
147 });
148
149 i += len;
150 }
151
152 Ok(RadiusPacket {
153 code,
154 identifier,
155 length: length as u16,
156 authenticator,
157 attributes,
158 })
159 }
160
161 pub fn to_bytes(&self) -> Vec<u8> {
162 let mut buf = Vec::new();
163 buf.push(self.code);
164 buf.push(self.identifier);
165 buf.extend_from_slice(&[0x00, 0x00]);
166 buf.extend_from_slice(&self.authenticator);
167
168 for attr in &self.attributes {
169 buf.push(attr.typ);
170 buf.push(attr.len);
171 buf.extend_from_slice(&attr.value);
172 }
173
174 let length = buf.len() as u16;
175 buf[2] = (length >> 8) as u8;
176 buf[3] = length as u8;
177
178 buf
179 }
180
181 pub fn access_accept(identifier: u8, attributes: Vec<RadiusAttribute>) -> Self {
182 RadiusPacket {
183 code: 2,
184 identifier,
185 length: 0,
186 authenticator: [0u8; 16],
187 attributes,
188 }
189 }
190
191 pub fn access_reject(identifier: u8, msg: &str) -> Self {
192 RadiusPacket {
193 code: 3,
194 identifier,
195 length: 0,
196 authenticator: [0u8; 16],
197 attributes: vec![RadiusAttribute::reply_message(msg)],
198 }
199 }
200
201 pub fn access_challenge(identifier: u8, msg: &str) -> Self {
202 RadiusPacket {
203 code: 11,
204 identifier,
205 length: 0,
206 authenticator: [0u8; 16],
207 attributes: vec![RadiusAttribute::reply_message(msg)],
208 }
209 }
210
211 pub fn log(&self) {
212 let code_name = match self.code {
213 1 => "Access-Request",
214 2 => "Access-Accept",
215 3 => "Access-Reject",
216 _ => "Unknown",
217 };
218
219 println!("📨 RADIUS {} (id: {})", code_name, self.identifier);
220
221 for attr in &self.attributes {
222 let name = radius_type_name(attr.typ);
223 if attr.typ == 1 || attr.typ == 18 || attr.typ == 80 {
224 match str::from_utf8(&attr.value) {
225 Ok(val) => println!(" • {}: {}", name, val.trim()),
226 Err(_) => println!(" • {}: {:?}", name, attr.value),
227 }
228 } else if attr.typ == 26 {
229 println!(" • {}: {}", name, decode_vendor_specific(&attr.value));
230 } else {
231 println!(" • {}: {:?}", name, attr.value);
232 }
233 }
234 }
235 pub fn reply_accept(&self, attributes: Vec<RadiusAttribute>) -> RadiusPacket {
236 RadiusPacket {
237 code: 2,
238 identifier: self.identifier,
239 length: 0,
240 authenticator: [0; 16],
241 attributes,
242 }
243 }
244
245 pub fn reply_reject(&self, message: &str) -> RadiusPacket {
246 RadiusPacket {
247 code: 3,
248 identifier: self.identifier,
249 length: 0,
250 authenticator: [0; 16],
251 attributes: vec![RadiusAttribute::reply_message(message)],
252 }
253 }
254
255 pub fn reply_challenge(&self, message: &str) -> RadiusPacket {
256 RadiusPacket {
257 code: 11,
258 identifier: self.identifier,
259 length: 0,
260 authenticator: [0; 16],
261 attributes: vec![RadiusAttribute::reply_message(message)],
262 }
263 }
264
265 pub fn username(&self) -> Option<String> {
266 self.attributes
267 .iter()
268 .find(|a| a.typ == 1)
269 .and_then(|a| String::from_utf8(a.value.clone()).ok())
270 }
271}
272
273fn radius_type_name(typ: u8) -> &'static str {
274 match typ {
275 1 => "User-Name",
276 2 => "User-Password",
277 3 => "CHAP-Password",
278 4 => "NAS-IP-Address",
279 18 => "Reply-Message",
280 26 => "Vendor-Specific",
281 27 => "Session-Timeout",
282 28 => "Idle-Timeout",
283 80 => "Message-Authenticator",
284 _ => "Unknown",
285 }
286}
287
288fn decode_vendor_specific(value: &[u8]) -> String {
289 if value.len() < 6 {
290 return "Invalid VSA".to_string();
291 }
292
293 let vendor_id = u32::from_be_bytes([value[0], value[1], value[2], value[3]]);
294 let vendor_type = value.get(4).copied().unwrap_or(0);
295 let vendor_len = value.get(5).copied().unwrap_or(0) as usize;
296
297 let data_start = 6;
298 let data_end = data_start + (vendor_len.saturating_sub(2));
299 let payload = &value.get(data_start..data_end).unwrap_or(&[]);
300
301 format!("VendorID={}, Type={}, Data={:?}", vendor_id, vendor_type, payload)
302}
303#[derive(Debug, Clone)]
304pub struct AccountingPacket {
305 pub code: u8,
306 pub identifier: u8,
307 pub length: u16,
308 pub authenticator: [u8; 16],
309 pub attributes: Vec<AccountingAttribute>,
310}
311
312#[derive(Debug, Clone)]
313pub enum AccountingAttribute {
314 UserName(String),
315 AcctStatusType(String), AcctSessionId(String),
317 AcctSessionTime(u32),
318 AcctInputOctets(u64),
319 AcctOutputOctets(u64),
320 AcctTerminateCause(String),
321 NasIp([u8; 4]),
322 Unknown(u8, Vec<u8>),
323}
324impl From<RadiusPacket> for AccountingPacket {
325 fn from(pkt: RadiusPacket) -> Self {
326 let attributes = pkt.attributes.into_iter().map(|attr| {
327 match attr.typ {
328 1 => AccountingAttribute::UserName(String::from_utf8_lossy(&attr.value).to_string()),
329 40 => { let v = u32::from_be_bytes(attr.value.try_into().unwrap_or([0,0,0,0]));
331 let status = match v {
332 1 => "Start",
333 2 => "Stop",
334 3 => "Interim-Update",
335 _ => "Unknown",
336 };
337 AccountingAttribute::AcctStatusType(status.to_string())
338 }
339 44 => AccountingAttribute::AcctSessionId(String::from_utf8_lossy(&attr.value).to_string()),
340 46 => {
341 let secs = u32::from_be_bytes(attr.value.try_into().unwrap_or([0,0,0,0]));
342 AccountingAttribute::AcctSessionTime(secs)
343 }
344 42 => {
345 let val = u32::from_be_bytes(attr.value.try_into().unwrap_or([0,0,0,0]));
346 AccountingAttribute::AcctInputOctets(val as u64)
347 }
348 43 => {
349 let val = u32::from_be_bytes(attr.value.try_into().unwrap_or([0,0,0,0]));
350 AccountingAttribute::AcctOutputOctets(val as u64)
351 }
352 49 => { let v = u32::from_be_bytes(attr.value.try_into().unwrap_or([0,0,0,0]));
354 let cause = match v {
355 1 => "User-Request",
356 2 => "Lost-Carrier",
357 3 => "Lost-Service",
358 _ => "Other",
359 };
360 AccountingAttribute::AcctTerminateCause(cause.to_string())
361 }
362 4 => {
363 if attr.value.len() == 4 {
364 AccountingAttribute::NasIp([attr.value[0], attr.value[1], attr.value[2], attr.value[3]])
365 } else {
366 AccountingAttribute::Unknown(attr.typ, attr.value)
367 }
368 }
369 _ => AccountingAttribute::Unknown(attr.typ, attr.value),
370 }
371 }).collect();
372
373 AccountingPacket {
374 code: pkt.code,
375 identifier: pkt.identifier,
376 length: pkt.length,
377 authenticator: pkt.authenticator,
378 attributes,
379 }
380 }
381}