use anyhow::{bail, ensure, Context, Result};
use sha1::{Digest, Sha1};
use std::net::Ipv4Addr;
pub const IP_PROTOCOL_VRRP: u8 = 112;
pub const VRRP_MULTICAST_IPV4: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 18);
pub const VRRP_MULTICAST_IPV6: std::net::Ipv6Addr =
std::net::Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 0x12);
pub const VRRP_TYPE_ADVERTISEMENT: u8 = 1;
pub const VRRP_VERSION_2: u8 = 2;
pub const VRRP_VERSION_3: u8 = 3;
pub const VRRP_PRIORITY_ADDRESS_OWNER: u8 = 255;
pub const VRRP_PRIORITY_RESIGN: u8 = 0;
pub const VRRP_AUTH_TYPE_NONE: u8 = 0;
pub const VRRP_V2_HEADER_LEN: usize = 8;
pub const VRRP_V2_AUTH_DATA_LEN: usize = 8;
pub const VRRP_V3_HEADER_LEN: usize = 8;
pub const VRRP_V3_MAX_INTERVAL_CENTISECONDS: u16 = 0x0fff;
pub const CARP_VERSION: u8 = 2;
pub const CARP_TYPE_ADVERTISEMENT: u8 = 1;
pub const CARP_HEADER_LEN: usize = 36;
pub const CARP_AUTH_LEN_WORDS: u8 = 7;
pub const CARP_KEY_LEN: usize = 20;
pub fn internet_checksum(data: &[u8]) -> u16 {
let mut sum: u32 = 0;
let mut chunks = data.chunks_exact(2);
for chunk in &mut chunks {
sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
}
if let Some(&last) = chunks.remainder().first() {
sum += u16::from_be_bytes([last, 0]) as u32;
}
while (sum >> 16) != 0 {
sum = (sum & 0xffff) + (sum >> 16);
}
!(sum as u16)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PseudoHeader {
pub source: Ipv4Addr,
pub destination: Ipv4Addr,
}
impl PseudoHeader {
pub fn new(source: Ipv4Addr, destination: Ipv4Addr) -> Self {
Self {
source,
destination,
}
}
pub fn bytes(&self, message_len: usize) -> Result<[u8; 12]> {
let length = u16::try_from(message_len)
.with_context(|| format!("VRRP message of {message_len} octets exceeds 65535"))?;
let mut out = [0u8; 12];
out[0..4].copy_from_slice(&self.source.octets());
out[4..8].copy_from_slice(&self.destination.octets());
out[8] = 0;
out[9] = IP_PROTOCOL_VRRP;
out[10..12].copy_from_slice(&length.to_be_bytes());
Ok(out)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChecksumScope {
Message,
PseudoHeaderAndMessage,
}
impl ChecksumScope {
pub fn as_str(self) -> &'static str {
match self {
ChecksumScope::Message => "message",
ChecksumScope::PseudoHeaderAndMessage => "pseudo_header_and_message",
}
}
}
pub fn checksum_is_valid(message: &[u8], pseudo: Option<&PseudoHeader>) -> bool {
match pseudo {
None => internet_checksum(message) == 0,
Some(pseudo) => {
let Ok(prefix) = pseudo.bytes(message.len()) else {
return false;
};
let mut buffer = Vec::with_capacity(prefix.len() + message.len());
buffer.extend_from_slice(&prefix);
buffer.extend_from_slice(message);
internet_checksum(&buffer) == 0
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct VrrpAdvertisement {
pub version: u8,
pub vrid: u8,
pub priority: u8,
pub advert_interval_seconds: f64,
pub auth_type: u8,
pub addresses: Vec<Ipv4Addr>,
pub checksum: u16,
}
impl VrrpAdvertisement {
pub fn is_resignation(&self) -> bool {
self.priority == VRRP_PRIORITY_RESIGN
}
pub fn is_address_owner(&self) -> bool {
self.priority == VRRP_PRIORITY_ADDRESS_OWNER
}
pub fn checksum_scope(&self) -> ChecksumScope {
if self.version >= VRRP_VERSION_3 {
ChecksumScope::PseudoHeaderAndMessage
} else {
ChecksumScope::Message
}
}
pub fn interval_field(&self) -> Result<u16> {
ensure!(
self.advert_interval_seconds.is_finite() && self.advert_interval_seconds > 0.0,
"advert_interval must be a positive number of seconds, got {}",
self.advert_interval_seconds
);
match self.version {
VRRP_VERSION_2 => {
let seconds = self.advert_interval_seconds;
ensure!(
(seconds - seconds.round()).abs() < 1e-9,
"VRRPv2 carries the advertisement interval in WHOLE SECONDS in a single \
octet, so {seconds}s cannot be expressed. Use version 3, whose field is \
in centiseconds, for sub-second intervals."
);
let seconds = seconds.round();
ensure!(
(1.0..=255.0).contains(&seconds),
"VRRPv2 advertisement interval must be 1..=255 seconds, got {seconds}"
);
Ok(seconds as u16)
}
VRRP_VERSION_3 => {
let centiseconds = (self.advert_interval_seconds * 100.0).round();
ensure!(
(1.0..=(VRRP_V3_MAX_INTERVAL_CENTISECONDS as f64)).contains(¢iseconds),
"VRRPv3 carries the advertisement interval in CENTISECONDS in a 12-bit \
field, so it must be 0.01..=40.95 seconds; {}s is {centiseconds} \
centiseconds",
self.advert_interval_seconds
);
Ok(centiseconds as u16)
}
other => bail!("unsupported VRRP version {other} (expected 2 or 3)"),
}
}
pub fn validate(&self) -> Result<()> {
ensure!(
self.version == VRRP_VERSION_2 || self.version == VRRP_VERSION_3,
"unsupported VRRP version {} (expected 2 or 3)",
self.version
);
ensure!(self.vrid != 0, "VRID 0 is not a valid virtual router id");
ensure!(
self.addresses.len() <= 255,
"a VRRP advertisement can carry at most 255 addresses, got {}",
self.addresses.len()
);
if self.version == VRRP_VERSION_3 {
ensure!(
self.auth_type == VRRP_AUTH_TYPE_NONE,
"VRRPv3 has no authentication-type field — those four bits are reserved and \
must be zero (RFC 5798 §5.2.5); got auth_type {}",
self.auth_type
);
}
self.interval_field()?;
Ok(())
}
pub fn encode(&self, pseudo: Option<&PseudoHeader>) -> Result<Vec<u8>> {
self.validate()?;
let interval = self.interval_field()?;
let mut message = Vec::new();
message.push((self.version << 4) | VRRP_TYPE_ADVERTISEMENT);
message.push(self.vrid);
message.push(self.priority);
message.push(self.addresses.len() as u8);
match self.version {
VRRP_VERSION_2 => {
message.push(self.auth_type);
message.push(interval as u8);
}
_ => {
message.push(((interval >> 8) & 0x0f) as u8);
message.push((interval & 0xff) as u8);
}
}
message.extend_from_slice(&[0, 0]); for address in &self.addresses {
message.extend_from_slice(&address.octets());
}
if self.version == VRRP_VERSION_2 {
message.extend_from_slice(&[0u8; VRRP_V2_AUTH_DATA_LEN]);
}
let checksum = match self.version {
VRRP_VERSION_2 => internet_checksum(&message),
_ => {
let pseudo = pseudo.context(
"VRRPv3 checksums cover an IP pseudo-header (RFC 5798 §5.2.8), so the \
source and destination addresses are required to encode one. Encoding \
without them would produce a packet every conformant peer discards.",
)?;
let prefix = pseudo.bytes(message.len())?;
let mut buffer = Vec::with_capacity(prefix.len() + message.len());
buffer.extend_from_slice(&prefix);
buffer.extend_from_slice(&message);
internet_checksum(&buffer)
}
};
message[6..8].copy_from_slice(&checksum.to_be_bytes());
Ok(message)
}
pub fn decode(data: &[u8]) -> Result<Self> {
ensure!(
data.len() >= VRRP_V2_HEADER_LEN,
"VRRP message truncated: {} octets, need at least {}",
data.len(),
VRRP_V2_HEADER_LEN
);
let version = data[0] >> 4;
let packet_type = data[0] & 0x0f;
ensure!(
packet_type == VRRP_TYPE_ADVERTISEMENT,
"VRRP packet type {packet_type} is not an advertisement (the only type defined)"
);
ensure!(
version == VRRP_VERSION_2 || version == VRRP_VERSION_3,
"unsupported VRRP version {version} (expected 2 or 3). Note that a CARP \
advertisement also starts 0x21 and must be decoded as CARP, not as VRRPv2."
);
let vrid = data[1];
let priority = data[2];
let count = data[3] as usize;
let checksum = u16::from_be_bytes([data[6], data[7]]);
let (auth_type, advert_interval_seconds) = if version == VRRP_VERSION_2 {
(data[4], data[5] as f64)
} else {
let centiseconds = (((data[4] & 0x0f) as u16) << 8) | data[5] as u16;
(VRRP_AUTH_TYPE_NONE, centiseconds as f64 / 100.0)
};
let needed = VRRP_V2_HEADER_LEN + count * 4;
ensure!(
data.len() >= needed,
"VRRP message declares {count} address(es) but is only {} octets; {needed} needed",
data.len()
);
let mut addresses = Vec::with_capacity(count);
for i in 0..count {
let offset = VRRP_V2_HEADER_LEN + i * 4;
addresses.push(Ipv4Addr::new(
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
));
}
Ok(Self {
version,
vrid,
priority,
advert_interval_seconds,
auth_type,
addresses,
checksum,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CarpAdvertisement {
pub version: u8,
pub vhid: u8,
pub advskew: u8,
pub auth_length_words: u8,
pub demote: u8,
pub advbase: u8,
pub counter: u64,
pub hmac: [u8; 20],
pub checksum: u16,
}
impl CarpAdvertisement {
pub fn interval_seconds(&self) -> f64 {
self.advbase as f64 + (self.advskew as f64 / 256.0)
}
pub fn validate(&self) -> Result<()> {
ensure!(
self.version == CARP_VERSION,
"unsupported CARP version {} (OpenBSD's CARP_VERSION is {CARP_VERSION})",
self.version
);
ensure!(self.vhid != 0, "CARP vhid 0 is not a valid virtual host id");
ensure!(
self.advbase >= 1,
"CARP advbase must be at least 1 second; 0 would advertise continuously"
);
Ok(())
}
pub fn encode(&self) -> Result<Vec<u8>> {
self.validate()?;
let mut message = Vec::with_capacity(CARP_HEADER_LEN);
message.push((self.version << 4) | CARP_TYPE_ADVERTISEMENT);
message.push(self.vhid);
message.push(self.advskew);
message.push(self.auth_length_words);
message.push(self.demote);
message.push(self.advbase);
message.extend_from_slice(&[0, 0]); message.extend_from_slice(&self.counter.to_be_bytes());
message.extend_from_slice(&self.hmac);
debug_assert_eq!(message.len(), CARP_HEADER_LEN);
let checksum = internet_checksum(&message);
message[6..8].copy_from_slice(&checksum.to_be_bytes());
Ok(message)
}
pub fn decode(data: &[u8]) -> Result<Self> {
ensure!(
data.len() >= CARP_HEADER_LEN,
"CARP advertisement is {} octets, must be {CARP_HEADER_LEN}",
data.len()
);
let version = data[0] >> 4;
let packet_type = data[0] & 0x0f;
ensure!(
packet_type == CARP_TYPE_ADVERTISEMENT,
"CARP packet type {packet_type} is not an advertisement"
);
let mut hmac = [0u8; 20];
hmac.copy_from_slice(&data[16..36]);
Ok(Self {
version,
vhid: data[1],
advskew: data[2],
auth_length_words: data[3],
demote: data[4],
advbase: data[5],
checksum: u16::from_be_bytes([data[6], data[7]]),
counter: u64::from_be_bytes([
data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15],
]),
hmac,
})
}
}
pub fn carp_hmac(passphrase: &[u8], vhid: u8, addresses: &[Ipv4Addr], counter: u64) -> [u8; 20] {
let mut key = [0u8; CARP_KEY_LEN];
let take = passphrase.len().min(CARP_KEY_LEN);
key[..take].copy_from_slice(&passphrase[..take]);
let mut message = vec![CARP_VERSION, CARP_TYPE_ADVERTISEMENT, vhid];
for address in addresses {
message.extend_from_slice(&address.octets());
}
message.extend_from_slice(&counter.to_be_bytes());
hmac_sha1(&key, &message)
}
pub fn sha1(data: &[u8]) -> [u8; 20] {
let mut out = [0u8; 20];
out.copy_from_slice(&Sha1::digest(data));
out
}
pub fn hmac_sha1(key: &[u8], data: &[u8]) -> [u8; 20] {
const BLOCK: usize = 64;
let mut padded = [0u8; BLOCK];
if key.len() > BLOCK {
padded[..20].copy_from_slice(&sha1(key));
} else {
padded[..key.len()].copy_from_slice(key);
}
let ipad: Vec<u8> = padded.iter().map(|byte| byte ^ 0x36).collect();
let opad: Vec<u8> = padded.iter().map(|byte| byte ^ 0x5c).collect();
let mut inner = Sha1::new();
inner.update(&ipad);
inner.update(data);
let inner = inner.finalize();
let mut outer = Sha1::new();
outer.update(&opad);
outer.update(inner);
let mut out = [0u8; 20];
out.copy_from_slice(&outer.finalize());
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Variant {
Vrrp,
Carp,
}
impl Variant {
pub fn from_name(name: &str) -> Result<Self> {
match name.trim().to_ascii_lowercase().as_str() {
"vrrp" | "vrrpv2" | "vrrpv3" | "rfc3768" | "rfc5798" => Ok(Variant::Vrrp),
"carp" | "openbsd" => Ok(Variant::Carp),
other => bail!("unknown variant '{other}' (expected 'vrrp' or 'carp')"),
}
}
pub fn as_str(self) -> &'static str {
match self {
Variant::Vrrp => "vrrp",
Variant::Carp => "carp",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Advertisement {
Vrrp(VrrpAdvertisement),
Carp(CarpAdvertisement),
}
impl Advertisement {
pub fn decode(variant: Variant, data: &[u8]) -> Result<Self> {
match variant {
Variant::Vrrp => VrrpAdvertisement::decode(data).map(Advertisement::Vrrp),
Variant::Carp => CarpAdvertisement::decode(data).map(Advertisement::Carp),
}
}
pub fn variant(&self) -> Variant {
match self {
Advertisement::Vrrp(_) => Variant::Vrrp,
Advertisement::Carp(_) => Variant::Carp,
}
}
}