1use anyhow::{anyhow, Context, Result};
15use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
16
17pub const V1_LEN: usize = 20;
20
21pub const V1_VERSION_BYTE: u8 = 0;
25
26pub const V2_TLV_GROUP_STATE: u8 = 1;
28pub const V2_TLV_INTERFACE_STATE: u8 = 2;
30pub const V2_TLV_TEXT_AUTH: u8 = 3;
33pub const V2_TLV_MD5_AUTH: u8 = 4;
35
36pub const V2_GROUP_STATE_LEN: u8 = 40;
38pub const V2_GROUP_STATE_TOTAL: usize = 2 + V2_GROUP_STATE_LEN as usize;
40pub const V2_TEXT_AUTH_LEN: u8 = 8;
42pub const V2_MD5_AUTH_LEN: u8 = 28;
44
45pub const AUTH_FIELD_LEN: usize = 8;
47
48pub const DEFAULT_AUTH_DATA: &str = "cisco";
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum HsrpVersion {
55 V1,
56 V2,
57}
58
59impl HsrpVersion {
60 pub fn as_number(self) -> u8 {
61 match self {
62 HsrpVersion::V1 => 1,
63 HsrpVersion::V2 => 2,
64 }
65 }
66
67 pub fn from_number(value: u64) -> Result<Self> {
69 match value {
70 1 => Ok(HsrpVersion::V1),
71 2 => Ok(HsrpVersion::V2),
72 other => Err(anyhow!(
73 "HSRP version must be 1 or 2, got {other}. The two are different wire formats \
74 and are not interoperable."
75 )),
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum Opcode {
83 Hello,
85 Coup,
87 Resign,
89}
90
91impl Opcode {
92 pub fn code(self) -> u8 {
93 match self {
94 Opcode::Hello => 0,
95 Opcode::Coup => 1,
96 Opcode::Resign => 2,
97 }
98 }
99
100 pub fn from_code(code: u8) -> Result<Self> {
101 match code {
102 0 => Ok(Opcode::Hello),
103 1 => Ok(Opcode::Coup),
104 2 => Ok(Opcode::Resign),
105 other => Err(anyhow!("Unknown HSRP opcode {other} (expected 0, 1 or 2)")),
106 }
107 }
108
109 pub fn as_str(self) -> &'static str {
111 match self {
112 Opcode::Hello => "hello",
113 Opcode::Coup => "coup",
114 Opcode::Resign => "resign",
115 }
116 }
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum HsrpState {
125 Initial,
126 Learn,
127 Listen,
128 Speak,
129 Standby,
130 Active,
131}
132
133impl HsrpState {
134 pub fn v1_code(self) -> u8 {
136 match self {
137 HsrpState::Initial => 0,
138 HsrpState::Learn => 1,
139 HsrpState::Listen => 2,
140 HsrpState::Speak => 4,
141 HsrpState::Standby => 8,
142 HsrpState::Active => 16,
143 }
144 }
145
146 pub fn v2_code(self) -> u8 {
148 match self {
149 HsrpState::Initial => 0,
150 HsrpState::Learn => 1,
151 HsrpState::Listen => 2,
152 HsrpState::Speak => 3,
153 HsrpState::Standby => 4,
154 HsrpState::Active => 5,
155 }
156 }
157
158 pub fn from_v1_code(code: u8) -> Result<Self> {
159 match code {
160 0 => Ok(HsrpState::Initial),
161 1 => Ok(HsrpState::Learn),
162 2 => Ok(HsrpState::Listen),
163 4 => Ok(HsrpState::Speak),
164 8 => Ok(HsrpState::Standby),
165 16 => Ok(HsrpState::Active),
166 other => Err(anyhow!(
167 "Unknown HSRPv1 state code {other} (expected 0, 1, 2, 4, 8 or 16)"
168 )),
169 }
170 }
171
172 pub fn from_v2_code(code: u8) -> Result<Self> {
173 match code {
174 0 => Ok(HsrpState::Initial),
175 1 => Ok(HsrpState::Learn),
176 2 => Ok(HsrpState::Listen),
177 3 => Ok(HsrpState::Speak),
178 4 => Ok(HsrpState::Standby),
179 5 => Ok(HsrpState::Active),
180 other => Err(anyhow!("Unknown HSRPv2 state code {other} (expected 0-5)")),
181 }
182 }
183
184 pub fn as_str(self) -> &'static str {
185 match self {
186 HsrpState::Initial => "initial",
187 HsrpState::Learn => "learn",
188 HsrpState::Listen => "listen",
189 HsrpState::Speak => "speak",
190 HsrpState::Standby => "standby",
191 HsrpState::Active => "active",
192 }
193 }
194
195 pub fn from_str_name(value: &str) -> Result<Self> {
198 match value.trim().to_ascii_lowercase().as_str() {
199 "initial" => Ok(HsrpState::Initial),
200 "learn" => Ok(HsrpState::Learn),
201 "listen" => Ok(HsrpState::Listen),
202 "speak" => Ok(HsrpState::Speak),
203 "standby" => Ok(HsrpState::Standby),
204 "active" => Ok(HsrpState::Active),
205 other => Err(anyhow!(
206 "Unknown HSRP state '{other}'. Use one of: initial, learn, listen, speak, \
207 standby, active. (State names are used rather than numbers because HSRPv1 and \
208 HSRPv2 encode them differently - code 4 is Speak in v1 and Standby in v2.)"
209 )),
210 }
211 }
212
213 pub fn claims_gateway(self) -> bool {
215 matches!(self, HsrpState::Active)
216 }
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub struct Md5Auth {
227 pub algorithm: u8,
228 pub flags: u16,
229 pub sender_address: Ipv4Addr,
230 pub key_id: u32,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq)]
240pub struct HsrpMessage {
241 pub version: HsrpVersion,
242 pub opcode: Opcode,
243 pub state: HsrpState,
244 pub hellotime_secs: u32,
245 pub holdtime_secs: u32,
246 pub priority: u32,
247 pub group: u16,
248 pub auth_data: Option<String>,
252 pub virtual_ip: IpAddr,
253 pub identifier: [u8; 6],
255 pub md5_auth: Option<Md5Auth>,
257}
258
259impl HsrpMessage {
260 pub fn identifier_str(&self) -> String {
262 self.identifier
263 .iter()
264 .map(|b| format!("{b:02x}"))
265 .collect::<Vec<_>>()
266 .join(":")
267 }
268}
269
270fn encode_auth_field(auth: Option<&str>) -> Result<[u8; AUTH_FIELD_LEN]> {
276 let mut field = [0u8; AUTH_FIELD_LEN];
277 let Some(auth) = auth else {
278 return Ok(field);
279 };
280 if let Some((index, bad)) = auth.char_indices().find(|(_, c)| c.is_control()) {
281 return Err(anyhow!(
282 "HSRP auth_data contains a control character (U+{:04X}) at byte {index}. The field \
283 is a plaintext group password that peers print back in their diagnostics and that \
284 this server's own event log renders unquoted, so a newline there forges a log \
285 line. Send printable text only.",
286 bad as u32
287 ));
288 }
289 let bytes = auth.as_bytes();
290 if bytes.len() > AUTH_FIELD_LEN {
291 return Err(anyhow!(
292 "HSRP auth_data must be at most {AUTH_FIELD_LEN} bytes, got {} ('{auth}'). The \
293 field is a fixed 8-byte NUL-padded string.",
294 bytes.len()
295 ));
296 }
297 field[..bytes.len()].copy_from_slice(bytes);
298 Ok(field)
299}
300
301fn decode_auth_field(field: &[u8]) -> Option<String> {
307 let trimmed: Vec<u8> = field.iter().copied().take_while(|b| *b != 0).collect();
308 if trimmed.is_empty() {
309 return None;
310 }
311 Some(crate::utils::sanitize::line_field(
317 &String::from_utf8_lossy(&trimmed),
318 ))
319}
320
321fn u16_at(buf: &[u8], offset: usize) -> u16 {
322 u16::from_be_bytes([buf[offset], buf[offset + 1]])
323}
324
325fn u32_at(buf: &[u8], offset: usize) -> u32 {
326 u32::from_be_bytes([
327 buf[offset],
328 buf[offset + 1],
329 buf[offset + 2],
330 buf[offset + 3],
331 ])
332}
333
334pub fn encode(message: &HsrpMessage) -> Result<Vec<u8>> {
340 match message.version {
341 HsrpVersion::V1 => encode_v1(message),
342 HsrpVersion::V2 => encode_v2(message),
343 }
344}
345
346fn encode_v1(message: &HsrpMessage) -> Result<Vec<u8>> {
365 let IpAddr::V4(vip) = message.virtual_ip else {
366 return Err(anyhow!(
367 "HSRPv1 carries a 4-byte virtual IP and has no IPv6 form; '{}' is IPv6. Use \
368 version 2 for IPv6.",
369 message.virtual_ip
370 ));
371 };
372
373 let priority = u8::try_from(message.priority).map_err(|_| {
374 anyhow!(
375 "HSRPv1 priority is a single byte (0-255), got {}. HSRPv2 widened it to 32 bits.",
376 message.priority
377 )
378 })?;
379 let group = u8::try_from(message.group).map_err(|_| {
380 anyhow!(
381 "HSRPv1 group is a single byte (0-255), got {}. HSRPv2 widened it to 0-4095.",
382 message.group
383 )
384 })?;
385 let hellotime = u8::try_from(message.hellotime_secs).map_err(|_| {
386 anyhow!(
387 "HSRPv1 hellotime is a single byte of seconds (0-255), got {}.",
388 message.hellotime_secs
389 )
390 })?;
391 let holdtime = u8::try_from(message.holdtime_secs).map_err(|_| {
392 anyhow!(
393 "HSRPv1 holdtime is a single byte of seconds (0-255), got {}.",
394 message.holdtime_secs
395 )
396 })?;
397
398 let auth = encode_auth_field(message.auth_data.as_deref())?;
399
400 let mut out = Vec::with_capacity(V1_LEN);
401 out.push(V1_VERSION_BYTE);
402 out.push(message.opcode.code());
403 out.push(message.state.v1_code());
404 out.push(hellotime);
405 out.push(holdtime);
406 out.push(priority);
407 out.push(group);
408 out.push(0); out.extend_from_slice(&auth);
410 out.extend_from_slice(&vip.octets());
411
412 debug_assert_eq!(out.len(), V1_LEN);
413 Ok(out)
414}
415
416fn encode_v2(message: &HsrpMessage) -> Result<Vec<u8>> {
434 if message.group > 4095 {
435 return Err(anyhow!(
436 "HSRPv2 group must be 0-4095, got {}.",
437 message.group
438 ));
439 }
440
441 let hellotime_ms = message.hellotime_secs.checked_mul(1000).ok_or_else(|| {
444 anyhow!(
445 "HSRPv2 hellotime {}s does not fit the 32-bit millisecond field.",
446 message.hellotime_secs
447 )
448 })?;
449 let holdtime_ms = message.holdtime_secs.checked_mul(1000).ok_or_else(|| {
450 anyhow!(
451 "HSRPv2 holdtime {}s does not fit the 32-bit millisecond field.",
452 message.holdtime_secs
453 )
454 })?;
455
456 let (ip_version, vip_bytes): (u8, [u8; 16]) = match message.virtual_ip {
457 IpAddr::V4(v4) => {
458 let mut buf = [0u8; 16];
461 buf[..4].copy_from_slice(&v4.octets());
462 (4, buf)
463 }
464 IpAddr::V6(v6) => (6, v6.octets()),
465 };
466
467 let mut out = Vec::with_capacity(V2_GROUP_STATE_TOTAL + 2 + AUTH_FIELD_LEN);
468 out.push(V2_TLV_GROUP_STATE);
469 out.push(V2_GROUP_STATE_LEN);
470 out.push(HsrpVersion::V2.as_number());
471 out.push(message.opcode.code());
472 out.push(message.state.v2_code());
473 out.push(ip_version);
474 out.extend_from_slice(&message.group.to_be_bytes());
475 out.extend_from_slice(&message.identifier);
476 out.extend_from_slice(&message.priority.to_be_bytes());
477 out.extend_from_slice(&hellotime_ms.to_be_bytes());
478 out.extend_from_slice(&holdtime_ms.to_be_bytes());
479 out.extend_from_slice(&vip_bytes);
480
481 debug_assert_eq!(out.len(), V2_GROUP_STATE_TOTAL);
482
483 if let Some(auth) = message.auth_data.as_deref() {
486 let field = encode_auth_field(Some(auth))?;
487 out.push(V2_TLV_TEXT_AUTH);
488 out.push(V2_TEXT_AUTH_LEN);
489 out.extend_from_slice(&field);
490 }
491
492 Ok(out)
493}
494
495pub fn decode(buf: &[u8]) -> Result<HsrpMessage> {
507 match buf.first() {
508 Some(&V1_VERSION_BYTE) => decode_v1(buf),
509 Some(_) => decode_v2(buf),
510 None => Err(anyhow!("Empty HSRP datagram")),
511 }
512}
513
514fn decode_v1(buf: &[u8]) -> Result<HsrpMessage> {
515 if buf.len() < V1_LEN {
516 return Err(anyhow!(
517 "HSRPv1 datagram is {} bytes, needs {V1_LEN}",
518 buf.len()
519 ));
520 }
521
522 let opcode = Opcode::from_code(buf[1])?;
523 let state = HsrpState::from_v1_code(buf[2])?;
524 let virtual_ip = Ipv4Addr::new(buf[16], buf[17], buf[18], buf[19]);
525
526 Ok(HsrpMessage {
527 version: HsrpVersion::V1,
528 opcode,
529 state,
530 hellotime_secs: u32::from(buf[3]),
531 holdtime_secs: u32::from(buf[4]),
532 priority: u32::from(buf[5]),
533 group: u16::from(buf[6]),
534 auth_data: decode_auth_field(&buf[8..16]),
538 virtual_ip: IpAddr::V4(virtual_ip),
539 identifier: [0u8; 6],
540 md5_auth: None,
541 })
542}
543
544fn decode_v2(buf: &[u8]) -> Result<HsrpMessage> {
547 let mut group_state: Option<HsrpMessage> = None;
548 let mut text_auth: Option<String> = None;
549 let mut md5_auth: Option<Md5Auth> = None;
550
551 let mut offset = 0usize;
552 while offset + 2 <= buf.len() {
553 let tlv_type = buf[offset];
554 let tlv_len = usize::from(buf[offset + 1]);
555 let body_start = offset + 2;
556 let body_end = body_start
557 .checked_add(tlv_len)
558 .filter(|end| *end <= buf.len())
559 .ok_or_else(|| {
560 anyhow!(
561 "HSRPv2 TLV type {tlv_type} at offset {offset} claims {tlv_len} bytes, past \
562 the end of a {}-byte datagram",
563 buf.len()
564 )
565 })?;
566 let body = &buf[body_start..body_end];
567
568 match tlv_type {
569 V2_TLV_GROUP_STATE => {
570 group_state = Some(decode_v2_group_state(body)?);
571 }
572 V2_TLV_TEXT_AUTH => {
573 text_auth = decode_auth_field(body);
574 }
575 V2_TLV_MD5_AUTH => {
576 md5_auth = decode_v2_md5_auth(body);
577 }
578 V2_TLV_INTERFACE_STATE => {
579 }
581 _ => {
582 }
586 }
587
588 offset = body_end;
589 }
590
591 let mut message = group_state.context(
592 "HSRPv2 datagram carried no Group State TLV (type 1); there is nothing to report",
593 )?;
594 message.auth_data = text_auth;
595 message.md5_auth = md5_auth;
596 Ok(message)
597}
598
599fn decode_v2_group_state(body: &[u8]) -> Result<HsrpMessage> {
600 if body.len() < usize::from(V2_GROUP_STATE_LEN) {
601 return Err(anyhow!(
602 "HSRPv2 Group State TLV is {} bytes, needs {V2_GROUP_STATE_LEN}",
603 body.len()
604 ));
605 }
606
607 let wire_version = body[0];
608 if wire_version != HsrpVersion::V2.as_number() {
609 return Err(anyhow!(
610 "HSRPv2 Group State TLV declares version {wire_version}, expected 2"
611 ));
612 }
613
614 let opcode = Opcode::from_code(body[1])?;
615 let state = HsrpState::from_v2_code(body[2])?;
616 let ip_version = body[3];
617 let group = u16_at(body, 4);
618
619 let mut identifier = [0u8; 6];
620 identifier.copy_from_slice(&body[6..12]);
621
622 let priority = u32_at(body, 12);
623 let hellotime_ms = u32_at(body, 16);
624 let holdtime_ms = u32_at(body, 20);
625
626 let virtual_ip = match ip_version {
627 4 => IpAddr::V4(Ipv4Addr::new(body[24], body[25], body[26], body[27])),
628 6 => {
629 let mut octets = [0u8; 16];
630 octets.copy_from_slice(&body[24..40]);
631 IpAddr::V6(Ipv6Addr::from(octets))
632 }
633 other => {
634 return Err(anyhow!(
635 "HSRPv2 Group State TLV declares IP version {other}, expected 4 or 6"
636 ))
637 }
638 };
639
640 Ok(HsrpMessage {
641 version: HsrpVersion::V2,
642 opcode,
643 state,
644 hellotime_secs: hellotime_ms / 1000,
646 holdtime_secs: holdtime_ms / 1000,
647 priority,
648 group,
649 auth_data: None,
650 virtual_ip,
651 identifier,
652 md5_auth: None,
653 })
654}
655
656fn decode_v2_md5_auth(body: &[u8]) -> Option<Md5Auth> {
662 if body.len() < usize::from(V2_MD5_AUTH_LEN) {
663 return None;
664 }
665 Some(Md5Auth {
666 algorithm: body[0],
667 flags: u16_at(body, 2),
669 sender_address: Ipv4Addr::new(body[4], body[5], body[6], body[7]),
670 key_id: u32_at(body, 8),
671 })
673}