Skip to main content

netget/server/tacacs/
codec.rs

1//! Selected RFC8907 legacy packets. Bodies remain internal; events use typed fields.
2use 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/// Password-bearing commands deliberately have no Debug implementation.
202#[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}