1use anyhow::{bail, ensure, Context, Result};
3use md5::{Digest, Md5};
4use serde::{Deserialize, Serialize};
5use std::time::Duration;
6use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
7pub const MAX_BODY_BYTES: usize = 16 * 1024;
8pub const MAX_ARGUMENTS: usize = 32;
9pub const MAX_TEXT_BYTES: usize = 255;
10pub const MAX_MESSAGE_BYTES: usize = 1024;
11pub const MAX_AUTH_ROUNDS: usize = 8;
12pub const DEFAULT_IO_SECONDS: u64 = 30;
13pub const DEFAULT_HANDLER_SECONDS: u64 = 30;
14pub const WRITE_TIMEOUT: Duration = Duration::from_secs(10);
15pub const DEFAULT_LLM_FALLBACK: bool = false;
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub struct Header {
18 pub version: u8,
19 pub kind: u8,
20 pub sequence: u8,
21 pub flags: u8,
22 pub session_id: u32,
23 pub length: usize,
24}
25impl Header {
26 pub fn parse(b: &[u8]) -> Result<Self> {
27 ensure!(b.len() == 12, "header length");
28 let h = Self {
29 version: b[0],
30 kind: b[1],
31 sequence: b[2],
32 flags: b[3],
33 session_id: u32::from_be_bytes(b[4..8].try_into()?),
34 length: u32::from_be_bytes(b[8..12].try_into()?) as usize,
35 };
36 ensure!(h.length <= MAX_BODY_BYTES, "body capacity");
37 Ok(h)
38 }
39 pub fn bytes(self) -> Result<[u8; 12]> {
40 ensure!(self.length <= MAX_BODY_BYTES, "body capacity");
41 let mut b = [
42 self.version,
43 self.kind,
44 self.sequence,
45 self.flags,
46 0,
47 0,
48 0,
49 0,
50 0,
51 0,
52 0,
53 0,
54 ];
55 b[4..8].copy_from_slice(&self.session_id.to_be_bytes());
56 b[8..12].copy_from_slice(&(self.length as u32).to_be_bytes());
57 Ok(b)
58 }
59 pub fn validate(self) -> Result<()> {
60 ensure!(matches!(self.version, 0xc0 | 0xc1), "unsupported version");
61 ensure!(matches!(self.kind, 1..=3), "unsupported type");
62 ensure!(self.sequence != 0, "sequence zero");
63 ensure!(self.flags & 1 == 0, "legacy requires obfuscated body");
64 Ok(())
65 }
66 pub fn reply(self) -> Result<Self> {
67 Ok(Self {
68 sequence: self
69 .sequence
70 .checked_add(1)
71 .context("sequence must never wrap")?,
72 flags: 0,
73 length: 0,
74 ..self
75 })
76 }
77}
78pub fn validate_secret(secret: &str) -> Result<()> {
79 ensure!(
80 !secret.is_empty() && secret.len() <= MAX_TEXT_BYTES,
81 "secret must have1..255 bytes"
82 );
83 Ok(())
84}
85pub fn obfuscate(header: Header, body: &[u8], secret: &[u8]) -> Vec<u8> {
86 let mut prefix = Vec::with_capacity(secret.len() + 6);
87 prefix.extend(header.session_id.to_be_bytes());
88 prefix.extend(secret);
89 prefix.extend([header.version, header.sequence]);
90 let mut previous = Vec::new();
91 let mut result = body.to_vec();
92 for chunk in result.chunks_mut(16) {
93 let mut hash = Md5::new();
94 hash.update(&prefix);
95 hash.update(&previous);
96 previous = hash.finalize().to_vec();
97 for (byte, pad) in chunk.iter_mut().zip(&previous) {
98 *byte ^= *pad;
99 }
100 }
101 result
102}
103pub fn packet(mut header: Header, body: &[u8], secret: &[u8]) -> Result<Vec<u8>> {
104 ensure!(body.len() <= MAX_BODY_BYTES, "body capacity");
105 header.length = body.len();
106 let mut bytes = header.bytes()?.to_vec();
107 bytes.extend(obfuscate(header, body, secret));
108 Ok(bytes)
109}
110pub async fn read_packet<R: AsyncRead + Unpin>(
111 reader: &mut R,
112 secret: &[u8],
113 timeout: Duration,
114) -> Result<(Header, Vec<u8>)> {
115 tokio::time::timeout(timeout, async {
116 let mut bytes = [0; 12];
117 reader.read_exact(&mut bytes).await?;
118 let header = Header::parse(&bytes)?;
119 let mut body = vec![0; header.length];
120 reader.read_exact(&mut body).await?;
121 Ok((header, obfuscate(header, &body, secret)))
122 })
123 .await
124 .context("packet deadline")?
125}
126pub async fn write_packet<W: AsyncWrite + Unpin>(
127 writer: &mut W,
128 header: Header,
129 body: &[u8],
130 secret: &[u8],
131) -> Result<usize> {
132 let bytes = packet(header, body, secret)?;
133 tokio::time::timeout(WRITE_TIMEOUT, writer.write_all(&bytes))
134 .await
135 .context("write deadline")??;
136 Ok(bytes.len())
137}
138macro_rules! enumeration {($name:ident {$($variant:ident=$value:expr),+$(,)?})=>{
139 #[derive(Clone,Copy,Debug,Serialize,Deserialize,PartialEq,Eq)]#[serde(rename_all="snake_case")]
140 pub enum $name{$($variant),+}
141 impl $name {pub fn byte(self)->u8{match self{$(Self::$variant=>$value),+}} pub fn parse(value:u8)->Result<Self>{match value{$($value=>Ok(Self::$variant)),+, _=>bail!(concat!(stringify!($name)," enum"))}}}
142}}
143enumeration!(AuthType{NotSet=0,Ascii=1,Pap=2,Chap=3,MsChap=5,MsChapV2=6});
144enumeration!(Service{None=0,Login=1,Enable=2,Ppp=3,Pt=5,Rcmd=6,X25=7,Nasi=8,FwProxy=9});
145enumeration!(AuthMethod{NotSet=0,None=1,Krb5=2,Line=3,Enable=4,Local=5,TacacsPlus=6,Guest=8,Radius=16,Krb4=17,Rcmd=32});
146enumeration!(AuthStatus{Pass=1,Fail=2,GetData=3,GetUser=4,GetPass=5,Restart=6,Error=7,Follow=33});
147enumeration!(AuthorStatus{PassAdd=1,PassReplace=2,Fail=16,Error=17,Follow=33});
148enumeration!(AccountStatus{Success=1,Error=2,Follow=33});
149enumeration!(AccountKind{Start=2,Stop=4,Watchdog=8,Update=10});
150fn login() -> Service {
151 Service::Login
152}
153fn ascii() -> AuthType {
154 AuthType::Ascii
155}
156fn tacacs() -> AuthMethod {
157 AuthMethod::TacacsPlus
158}
159fn privilege() -> u8 {
160 1
161}
162fn mandatory() -> bool {
163 true
164}
165#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
166#[serde(deny_unknown_fields)]
167pub struct Argument {
168 pub name: String,
169 pub value: String,
170 #[serde(default = "mandatory")]
171 pub mandatory: bool,
172}
173impl Argument {
174 pub fn wire(&self) -> Result<String> {
175 text(&self.name, MAX_TEXT_BYTES)?;
176 ensure!(
177 !self.name.is_empty() && !self.name.contains(['=', '*']),
178 "argument name"
179 );
180 text(&self.value, MAX_TEXT_BYTES)?;
181 let v = format!(
182 "{}{}{}",
183 self.name,
184 if self.mandatory { '=' } else { '*' },
185 self.value
186 );
187 ensure!(v.len() <= MAX_TEXT_BYTES, "argument byte capacity");
188 Ok(v)
189 }
190 pub fn parse(value: &str) -> Result<Self> {
191 text(value, MAX_TEXT_BYTES)?;
192 let i = value.find(['=', '*']).context("argument separator")?;
193 ensure!(i > 0, "argument name");
194 Ok(Self {
195 name: value[..i].into(),
196 value: value[i + 1..].into(),
197 mandatory: value.as_bytes()[i] == b'=',
198 })
199 }
200}
201#[derive(Clone, Serialize, Deserialize)]
203#[serde(deny_unknown_fields)]
204pub struct Authentication {
205 pub username: String,
206 pub password: String,
207 #[serde(default = "ascii")]
208 pub method: AuthType,
209 #[serde(default = "privilege")]
210 pub privilege_level: u8,
211 #[serde(default)]
212 pub port: String,
213 #[serde(default)]
214 pub remote_address: String,
215}
216#[derive(Clone, Debug, Serialize, Deserialize)]
217#[serde(deny_unknown_fields)]
218pub struct Request {
219 #[serde(default = "tacacs")]
220 pub authentication_method: AuthMethod,
221 #[serde(default = "privilege")]
222 pub privilege_level: u8,
223 #[serde(default = "ascii")]
224 pub authentication_type: AuthType,
225 #[serde(default = "login")]
226 pub service: Service,
227 #[serde(default)]
228 pub username: String,
229 #[serde(default)]
230 pub port: String,
231 #[serde(default)]
232 pub remote_address: String,
233 #[serde(default)]
234 pub arguments: Vec<Argument>,
235}
236#[derive(Clone, Debug, Serialize, Deserialize)]
237#[serde(deny_unknown_fields)]
238pub struct Accounting {
239 pub record_type: AccountKind,
240 pub request: Request,
241}
242#[derive(Clone)]
243pub struct Start {
244 pub action: u8,
245 pub privilege_level: u8,
246 pub method: AuthType,
247 pub service: Service,
248 pub username: String,
249 pub port: String,
250 pub remote_address: String,
251 pub password: Option<String>,
252}
253#[derive(Clone)]
254pub struct Continue {
255 pub user_message: String,
256 pub abort: bool,
257}
258#[derive(Clone, Debug, Serialize, Deserialize)]
259#[serde(deny_unknown_fields)]
260pub struct AuthReply {
261 pub status: AuthStatus,
262 #[serde(default)]
263 pub no_echo: bool,
264 #[serde(default)]
265 pub server_message: String,
266 #[serde(default)]
267 pub data: String,
268}
269#[derive(Clone, Debug, Serialize, Deserialize)]
270#[serde(deny_unknown_fields)]
271pub struct AuthorReply {
272 pub status: AuthorStatus,
273 #[serde(default)]
274 pub arguments: Vec<Argument>,
275 #[serde(default)]
276 pub server_message: String,
277 #[serde(default)]
278 pub data: String,
279}
280#[derive(Clone, Debug, Serialize, Deserialize)]
281#[serde(deny_unknown_fields)]
282pub struct AccountReply {
283 pub status: AccountStatus,
284 #[serde(default)]
285 pub server_message: String,
286 #[serde(default)]
287 pub data: String,
288}
289pub fn text(value: &str, maximum: usize) -> Result<()> {
290 ensure!(
291 value.len() <= maximum && value.bytes().all(|b| matches!(b, 32..=126)),
292 "printable ASCII text or capacity"
293 );
294 Ok(())
295}
296pub fn username(value: &str) -> Result<()> {
297 text(value, MAX_TEXT_BYTES)?;
298 ensure!(!value.contains(' '), "selected username has no spaces");
299 Ok(())
300}
301pub fn request_valid(r: &Request) -> Result<()> {
302 ensure!(r.privilege_level <= 15, "privilege0..15");
303 username(&r.username)?;
304 text(&r.port, MAX_TEXT_BYTES)?;
305 text(&r.remote_address, MAX_TEXT_BYTES)?;
306 ensure!(r.arguments.len() <= MAX_ARGUMENTS, "argument capacity32");
307 for a in &r.arguments {
308 a.wire()?;
309 }
310 Ok(())
311}
312struct Cursor<'a> {
313 b: &'a [u8],
314 i: usize,
315}
316impl<'a> Cursor<'a> {
317 fn new(b: &'a [u8]) -> Self {
318 Self { b, i: 0 }
319 }
320 fn take(&mut self, n: usize) -> Result<&'a [u8]> {
321 let end = self.i.checked_add(n).context("length overflow")?;
322 let v = self.b.get(self.i..end).context("truncated body")?;
323 self.i = end;
324 Ok(v)
325 }
326 fn byte(&mut self) -> Result<u8> {
327 Ok(self.take(1)?[0])
328 }
329 fn u16(&mut self) -> Result<usize> {
330 Ok(u16::from_be_bytes(self.take(2)?.try_into()?) as usize)
331 }
332 fn string(&mut self, n: usize, max: usize) -> Result<String> {
333 ensure!(n <= max, "text capacity");
334 let s = std::str::from_utf8(self.take(n)?)?.to_string();
335 text(&s, max)?;
336 Ok(s)
337 }
338 fn end(self) -> Result<()> {
339 ensure!(self.i == self.b.len(), "trailing body");
340 Ok(())
341 }
342}
343pub fn parse_start(body: &[u8]) -> Result<Start> {
344 let mut c = Cursor::new(body);
345 let action = c.byte()?;
346 ensure!(matches!(action, 1 | 2 | 4), "authentication action enum");
347 let privilege_level = c.byte()?;
348 ensure!(privilege_level <= 15, "privilege0..15");
349 let method = AuthType::parse(c.byte()?)?;
350 ensure!(method != AuthType::NotSet, "start authentication type");
351 let service = Service::parse(c.byte()?)?;
352 let lengths = [c.byte()?, c.byte()?, c.byte()?, c.byte()?];
353 let username = c.string(lengths[0] as usize, MAX_TEXT_BYTES)?;
354 self::username(&username)?;
355 let port = c.string(lengths[1] as usize, MAX_TEXT_BYTES)?;
356 let remote_address = c.string(lengths[2] as usize, MAX_TEXT_BYTES)?;
357 let data = c.take(lengths[3] as usize)?;
358 let password = if method == AuthType::Pap {
359 let s = std::str::from_utf8(data)?.to_string();
360 text(&s, MAX_TEXT_BYTES)?;
361 Some(s)
362 } else {
363 None
364 };
365 c.end()?;
366 Ok(Start {
367 action,
368 privilege_level,
369 method,
370 service,
371 username,
372 port,
373 remote_address,
374 password,
375 })
376}
377pub fn authentication_body(a: &Authentication) -> Result<(u8, Vec<u8>)> {
378 ensure!(
379 matches!(a.method, AuthType::Ascii | AuthType::Pap),
380 "only ASCII/PAP login"
381 );
382 username(&a.username)?;
383 ensure!(!a.username.is_empty(), "username required");
384 text(&a.password, MAX_TEXT_BYTES)?;
385 text(&a.port, MAX_TEXT_BYTES)?;
386 text(&a.remote_address, MAX_TEXT_BYTES)?;
387 ensure!(a.privilege_level <= 15, "privilege0..15");
388 let data = if a.method == AuthType::Pap {
389 a.password.as_bytes()
390 } else {
391 &[]
392 };
393 let mut b = vec![
394 1,
395 a.privilege_level,
396 a.method.byte(),
397 1,
398 a.username.len() as u8,
399 a.port.len() as u8,
400 a.remote_address.len() as u8,
401 data.len() as u8,
402 ];
403 b.extend(a.username.as_bytes());
404 b.extend(a.port.as_bytes());
405 b.extend(a.remote_address.as_bytes());
406 b.extend(data);
407 Ok((
408 if a.method == AuthType::Pap {
409 0xc1
410 } else {
411 0xc0
412 },
413 b,
414 ))
415}
416pub fn parse_continue(body: &[u8]) -> Result<Continue> {
417 let mut c = Cursor::new(body);
418 let u = c.u16()?;
419 let d = c.u16()?;
420 let flags = c.byte()?;
421 let user_message = c.string(u, MAX_TEXT_BYTES)?;
422 c.take(d)?;
423 c.end()?;
424 Ok(Continue {
425 user_message,
426 abort: flags & 1 != 0,
427 })
428}
429pub fn continue_body(value: &str, abort: bool) -> Result<Vec<u8>> {
430 text(value, MAX_TEXT_BYTES)?;
431 let mut b = if abort {
432 vec![0, 0]
433 } else {
434 (value.len() as u16).to_be_bytes().to_vec()
435 };
436 b.extend(if abort {
437 (value.len() as u16).to_be_bytes()
438 } else {
439 [0, 0]
440 });
441 b.push(u8::from(abort));
442 b.extend(value.as_bytes());
443 Ok(b)
444}
445pub fn request_body(r: &Request, account: Option<AccountKind>) -> Result<Vec<u8>> {
446 request_valid(r)?;
447 let mut b = Vec::new();
448 if let Some(k) = account {
449 b.push(k.byte());
450 }
451 b.extend([
452 r.authentication_method.byte(),
453 r.privilege_level,
454 r.authentication_type.byte(),
455 r.service.byte(),
456 r.username.len() as u8,
457 r.port.len() as u8,
458 r.remote_address.len() as u8,
459 r.arguments.len() as u8,
460 ]);
461 let args = r
462 .arguments
463 .iter()
464 .map(Argument::wire)
465 .collect::<Result<Vec<_>>>()?;
466 b.extend(args.iter().map(|s| s.len() as u8));
467 b.extend(r.username.as_bytes());
468 b.extend(r.port.as_bytes());
469 b.extend(r.remote_address.as_bytes());
470 for a in args {
471 b.extend(a.as_bytes());
472 }
473 ensure!(b.len() <= MAX_BODY_BYTES, "body capacity");
474 Ok(b)
475}
476pub fn parse_request(body: &[u8], account: bool) -> Result<(Request, Option<AccountKind>)> {
477 let mut c = Cursor::new(body);
478 let kind = if account {
479 Some(AccountKind::parse(c.byte()? & 0x0e)?)
480 } else {
481 None
482 };
483 let authentication_method = AuthMethod::parse(c.byte()?)?;
484 let privilege_level = c.byte()?;
485 let authentication_type = AuthType::parse(c.byte()?)?;
486 let service = Service::parse(c.byte()?)?;
487 let u = c.byte()? as usize;
488 let p = c.byte()? as usize;
489 let r = c.byte()? as usize;
490 let count = c.byte()? as usize;
491 ensure!(count <= MAX_ARGUMENTS, "argument capacity32");
492 let lengths = c.take(count)?.to_vec();
493 let username = c.string(u, MAX_TEXT_BYTES)?;
494 let port = c.string(p, MAX_TEXT_BYTES)?;
495 let remote_address = c.string(r, MAX_TEXT_BYTES)?;
496 let mut arguments = Vec::with_capacity(count);
497 for length in lengths {
498 arguments.push(Argument::parse(
499 &c.string(length as usize, MAX_TEXT_BYTES)?,
500 )?);
501 }
502 c.end()?;
503 let request = Request {
504 authentication_method,
505 privilege_level,
506 authentication_type,
507 service,
508 username,
509 port,
510 remote_address,
511 arguments,
512 };
513 request_valid(&request)?;
514 Ok((request, kind))
515}
516fn reply_text(message: &str, data: &str) -> Result<()> {
517 text(message, MAX_MESSAGE_BYTES)?;
518 text(data, MAX_MESSAGE_BYTES)
519}
520pub fn auth_reply_body(r: &AuthReply) -> Result<Vec<u8>> {
521 reply_text(&r.server_message, &r.data)?;
522 let mut b = vec![r.status.byte(), u8::from(r.no_echo)];
523 b.extend((r.server_message.len() as u16).to_be_bytes());
524 b.extend((r.data.len() as u16).to_be_bytes());
525 b.extend(r.server_message.as_bytes());
526 b.extend(r.data.as_bytes());
527 Ok(b)
528}
529pub fn parse_auth_reply(body: &[u8]) -> Result<AuthReply> {
530 let mut c = Cursor::new(body);
531 let status = AuthStatus::parse(c.byte()?)?;
532 let no_echo = c.byte()? & 1 != 0;
533 let m = c.u16()?;
534 let d = c.u16()?;
535 let server_message = c.string(m, MAX_MESSAGE_BYTES)?;
536 let data = c.string(d, MAX_MESSAGE_BYTES)?;
537 c.end()?;
538 Ok(AuthReply {
539 status,
540 no_echo,
541 server_message,
542 data,
543 })
544}
545pub fn author_reply_body(r: &AuthorReply) -> Result<Vec<u8>> {
546 reply_text(&r.server_message, &r.data)?;
547 ensure!(r.arguments.len() <= MAX_ARGUMENTS, "argument capacity32");
548 ensure!(
549 r.status != AuthorStatus::Follow || r.arguments.is_empty(),
550 "FOLLOW arguments"
551 );
552 let args = r
553 .arguments
554 .iter()
555 .map(Argument::wire)
556 .collect::<Result<Vec<_>>>()?;
557 let mut b = vec![r.status.byte(), r.arguments.len() as u8];
558 b.extend((r.server_message.len() as u16).to_be_bytes());
559 b.extend((r.data.len() as u16).to_be_bytes());
560 b.extend(args.iter().map(|a| a.len() as u8));
561 b.extend(r.server_message.as_bytes());
562 b.extend(r.data.as_bytes());
563 for a in args {
564 b.extend(a.as_bytes());
565 }
566 Ok(b)
567}
568pub fn parse_author_reply(body: &[u8]) -> Result<AuthorReply> {
569 let mut c = Cursor::new(body);
570 let status = AuthorStatus::parse(c.byte()?)?;
571 let count = c.byte()? as usize;
572 ensure!(count <= MAX_ARGUMENTS, "argument capacity32");
573 let m = c.u16()?;
574 let d = c.u16()?;
575 let lengths = c.take(count)?.to_vec();
576 let server_message = c.string(m, MAX_MESSAGE_BYTES)?;
577 let data = c.string(d, MAX_MESSAGE_BYTES)?;
578 let mut arguments = Vec::with_capacity(count);
579 for n in lengths {
580 arguments.push(Argument::parse(&c.string(n as usize, MAX_TEXT_BYTES)?)?);
581 }
582 c.end()?;
583 ensure!(
584 status != AuthorStatus::Follow || arguments.is_empty(),
585 "FOLLOW arguments"
586 );
587 Ok(AuthorReply {
588 status,
589 arguments,
590 server_message,
591 data,
592 })
593}
594pub fn account_reply_body(r: &AccountReply) -> Result<Vec<u8>> {
595 reply_text(&r.server_message, &r.data)?;
596 let mut b = (r.server_message.len() as u16).to_be_bytes().to_vec();
597 b.extend((r.data.len() as u16).to_be_bytes());
598 b.push(r.status.byte());
599 b.extend(r.server_message.as_bytes());
600 b.extend(r.data.as_bytes());
601 Ok(b)
602}
603pub fn parse_account_reply(body: &[u8]) -> Result<AccountReply> {
604 let mut c = Cursor::new(body);
605 let m = c.u16()?;
606 let d = c.u16()?;
607 let status = AccountStatus::parse(c.byte()?)?;
608 let server_message = c.string(m, MAX_MESSAGE_BYTES)?;
609 let data = c.string(d, MAX_MESSAGE_BYTES)?;
610 c.end()?;
611 Ok(AccountReply {
612 status,
613 server_message,
614 data,
615 })
616}
617
618pub const MAX_JSON_BYTES: usize = 128 * 1024;
619pub const MAX_JSON_NODES: usize = 1024;
620pub const MAX_JSON_DEPTH: usize = 8;
621pub fn within_json_budget(value: &serde_json::Value) -> bool {
622 crate::utils::json_budget::within_budget(value, MAX_JSON_BYTES, MAX_JSON_NODES, MAX_JSON_DEPTH)
623}
624pub fn owned_json(value: serde_json::Value) -> Result<serde_json::Value> {
625 if within_json_budget(&value) {
626 Ok(value)
627 } else {
628 crate::utils::json_budget::drop_iteratively(value);
629 bail!("TACACS JSON depth/node/retained-content budget")
630 }
631}