use std::fmt;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use crate::error::DecodeError;
use crate::nlri::{Ipv4Prefix, Ipv6Prefix};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct EthernetTagId(pub u32);
impl EthernetTagId {
pub const MAX_ET: Self = Self(0xFFFF_FFFF);
#[must_use]
pub fn is_max_et(&self) -> bool {
self.0 == Self::MAX_ET.0
}
}
impl fmt::Display for EthernetTagId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_max_et() {
write!(f, "MAX_ET")
} else {
write!(f, "{}", self.0)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct MacAddress(pub [u8; 6]);
impl MacAddress {
#[must_use]
pub const fn new(bytes: [u8; 6]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn octets(&self) -> [u8; 6] {
self.0
}
}
impl fmt::Display for MacAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let o = &self.0;
write!(
f,
"{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
o[0], o[1], o[2], o[3], o[4], o[5]
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct EthernetSegmentIdentifier(pub [u8; 10]);
impl EthernetSegmentIdentifier {
pub const ZERO: Self = Self([0u8; 10]);
#[must_use]
pub const fn new(bytes: [u8; 10]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn octets(&self) -> [u8; 10] {
self.0
}
#[must_use]
pub const fn esi_type(&self) -> u8 {
self.0[0]
}
#[must_use]
pub fn is_zero(&self) -> bool {
self.0.iter().all(|&b| b == 0)
}
}
impl fmt::Display for EthernetSegmentIdentifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, byte) in self.0.iter().enumerate() {
if i > 0 {
write!(f, ":")?;
}
write!(f, "{byte:02x}")?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct RouteDistinguisher(pub [u8; 8]);
impl RouteDistinguisher {
pub const ZERO: Self = Self([0u8; 8]);
#[must_use]
pub const fn new(bytes: [u8; 8]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn octets(&self) -> [u8; 8] {
self.0
}
#[must_use]
pub fn rd_type(&self) -> u16 {
u16::from_be_bytes([self.0[0], self.0[1]])
}
}
impl fmt::Display for RouteDistinguisher {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let b = &self.0;
match self.rd_type() {
0 => {
let asn = u16::from_be_bytes([b[2], b[3]]);
let assigned = u32::from_be_bytes([b[4], b[5], b[6], b[7]]);
write!(f, "{asn}:{assigned}")
}
1 => {
let ip = Ipv4Addr::new(b[2], b[3], b[4], b[5]);
let assigned = u16::from_be_bytes([b[6], b[7]]);
write!(f, "{ip}:{assigned}")
}
2 => {
let asn = u32::from_be_bytes([b[2], b[3], b[4], b[5]]);
let assigned = u16::from_be_bytes([b[6], b[7]]);
write!(f, "{asn}:{assigned}")
}
_ => {
write!(f, "0x")?;
for byte in b {
write!(f, "{byte:02x}")?;
}
Ok(())
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct MplsLabel(pub u32);
impl MplsLabel {
#[must_use]
pub const fn new(value: u32) -> Self {
Self(value & 0x00FF_FFFF)
}
#[must_use]
pub const fn value(&self) -> u32 {
self.0
}
#[must_use]
pub const fn as_vni(&self) -> u32 {
self.0
}
#[must_use]
pub const fn as_mpls_label(&self) -> u32 {
self.0 >> 4
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum EvpnIpPrefixValue {
V4(Ipv4Prefix),
V6(Ipv6Prefix),
}
impl fmt::Display for EvpnIpPrefixValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::V4(p) => write!(f, "{}/{}", p.addr, p.len),
Self::V6(p) => write!(f, "{}/{}", p.addr, p.len),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct EvpnEadPerEs {
pub rd: RouteDistinguisher,
pub esi: EthernetSegmentIdentifier,
pub ethernet_tag: EthernetTagId,
pub label: MplsLabel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct EvpnEadPerEvi {
pub rd: RouteDistinguisher,
pub esi: EthernetSegmentIdentifier,
pub ethernet_tag: EthernetTagId,
pub label: MplsLabel,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct EvpnMacIp {
pub rd: RouteDistinguisher,
pub esi: EthernetSegmentIdentifier,
pub ethernet_tag: EthernetTagId,
pub mac: MacAddress,
pub ip: Option<IpAddr>,
pub label1: MplsLabel,
pub label2: Option<MplsLabel>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct EvpnImet {
pub rd: RouteDistinguisher,
pub ethernet_tag: EthernetTagId,
pub originator_ip: IpAddr,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct EvpnEs {
pub rd: RouteDistinguisher,
pub esi: EthernetSegmentIdentifier,
pub originator_ip: IpAddr,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct EvpnIpPrefixRoute {
pub rd: RouteDistinguisher,
pub esi: EthernetSegmentIdentifier,
pub ethernet_tag: EthernetTagId,
pub prefix: EvpnIpPrefixValue,
pub gateway: IpAddr,
pub label: MplsLabel,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum EvpnRoute {
EadPerEs(EvpnEadPerEs),
EadPerEvi(EvpnEadPerEvi),
MacIp(EvpnMacIp),
Imet(EvpnImet),
Es(EvpnEs),
IpPrefix(EvpnIpPrefixRoute),
}
impl EvpnRoute {
#[must_use]
pub const fn route_type(&self) -> u8 {
match self {
Self::EadPerEs(_) | Self::EadPerEvi(_) => 1,
Self::MacIp(_) => 2,
Self::Imet(_) => 3,
Self::Es(_) => 4,
Self::IpPrefix(_) => 5,
}
}
#[must_use]
pub fn key(&self) -> EvpnRouteKey {
match self {
Self::EadPerEs(r) => EvpnRouteKey::EadPerEs {
rd: r.rd,
esi: r.esi,
ethernet_tag: r.ethernet_tag,
},
Self::EadPerEvi(r) => EvpnRouteKey::EadPerEvi {
rd: r.rd,
esi: r.esi,
ethernet_tag: r.ethernet_tag,
},
Self::MacIp(r) => EvpnRouteKey::MacIp {
rd: r.rd,
ethernet_tag: r.ethernet_tag,
mac: r.mac,
ip: r.ip,
},
Self::Imet(r) => EvpnRouteKey::Imet {
rd: r.rd,
ethernet_tag: r.ethernet_tag,
originator_ip: r.originator_ip,
},
Self::Es(r) => EvpnRouteKey::Es {
rd: r.rd,
esi: r.esi,
originator_ip: r.originator_ip,
},
Self::IpPrefix(r) => EvpnRouteKey::IpPrefix {
rd: r.rd,
ethernet_tag: r.ethernet_tag,
prefix: r.prefix,
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EvpnRouteKey {
EadPerEs {
rd: RouteDistinguisher,
esi: EthernetSegmentIdentifier,
ethernet_tag: EthernetTagId,
},
EadPerEvi {
rd: RouteDistinguisher,
esi: EthernetSegmentIdentifier,
ethernet_tag: EthernetTagId,
},
MacIp {
rd: RouteDistinguisher,
ethernet_tag: EthernetTagId,
mac: MacAddress,
ip: Option<IpAddr>,
},
Imet {
rd: RouteDistinguisher,
ethernet_tag: EthernetTagId,
originator_ip: IpAddr,
},
Es {
rd: RouteDistinguisher,
esi: EthernetSegmentIdentifier,
originator_ip: IpAddr,
},
IpPrefix {
rd: RouteDistinguisher,
ethernet_tag: EthernetTagId,
prefix: EvpnIpPrefixValue,
},
}
impl EvpnRouteKey {
#[must_use]
pub const fn route_type(&self) -> u8 {
match self {
Self::EadPerEs { .. } | Self::EadPerEvi { .. } => 1,
Self::MacIp { .. } => 2,
Self::Imet { .. } => 3,
Self::Es { .. } => 4,
Self::IpPrefix { .. } => 5,
}
}
}
fn decode_rd(buf: &[u8]) -> Result<RouteDistinguisher, DecodeError> {
if buf.len() < 8 {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: "EVPN NLRI truncated: expected 8-byte Route Distinguisher".to_string(),
});
}
let mut bytes = [0u8; 8];
bytes.copy_from_slice(&buf[..8]);
Ok(RouteDistinguisher(bytes))
}
fn decode_esi(buf: &[u8]) -> Result<EthernetSegmentIdentifier, DecodeError> {
if buf.len() < 10 {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: "EVPN NLRI truncated: expected 10-byte ESI".to_string(),
});
}
let mut bytes = [0u8; 10];
bytes.copy_from_slice(&buf[..10]);
Ok(EthernetSegmentIdentifier(bytes))
}
fn decode_ethernet_tag(buf: &[u8]) -> Result<EthernetTagId, DecodeError> {
if buf.len() < 4 {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: "EVPN NLRI truncated: expected 4-byte Ethernet Tag".to_string(),
});
}
Ok(EthernetTagId(u32::from_be_bytes([
buf[0], buf[1], buf[2], buf[3],
])))
}
fn decode_mpls_label(buf: &[u8]) -> Result<MplsLabel, DecodeError> {
if buf.len() < 3 {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: "EVPN NLRI truncated: expected 3-byte MPLS label".to_string(),
});
}
let value = (u32::from(buf[0]) << 16) | (u32::from(buf[1]) << 8) | u32::from(buf[2]);
Ok(MplsLabel(value))
}
fn decode_ip_addr(buf: &[u8], len: usize, field: &str) -> Result<IpAddr, DecodeError> {
match len {
4 => {
if buf.len() < 4 {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: format!("EVPN NLRI truncated: expected 4 bytes for {field}"),
});
}
Ok(IpAddr::V4(Ipv4Addr::new(buf[0], buf[1], buf[2], buf[3])))
}
16 => {
if buf.len() < 16 {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: format!("EVPN NLRI truncated: expected 16 bytes for {field}"),
});
}
let mut octets = [0u8; 16];
octets.copy_from_slice(&buf[..16]);
Ok(IpAddr::V6(Ipv6Addr::from(octets)))
}
other => Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: format!("EVPN NLRI {field} length {other} (expected 4 or 16)"),
}),
}
}
fn decode_type1(payload: &[u8]) -> Result<EvpnRoute, DecodeError> {
if payload.len() != 25 {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: format!("EVPN Type 1 payload length {} (expected 25)", payload.len()),
});
}
let rd = decode_rd(&payload[0..8])?;
let esi = decode_esi(&payload[8..18])?;
if esi.is_zero() {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: "EVPN Type 1 EAD route with all-zero ESI (RFC 7432 §7.1)".into(),
});
}
let ethernet_tag = decode_ethernet_tag(&payload[18..22])?;
let label = decode_mpls_label(&payload[22..25])?;
if ethernet_tag.is_max_et() {
Ok(EvpnRoute::EadPerEs(EvpnEadPerEs {
rd,
esi,
ethernet_tag,
label,
}))
} else {
Ok(EvpnRoute::EadPerEvi(EvpnEadPerEvi {
rd,
esi,
ethernet_tag,
label,
}))
}
}
fn decode_type2(payload: &[u8]) -> Result<EvpnRoute, DecodeError> {
if payload.len() < 25 {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: format!(
"EVPN Type 2 payload too short: {} bytes (need at least 25)",
payload.len()
),
});
}
let rd = decode_rd(&payload[0..8])?;
let esi = decode_esi(&payload[8..18])?;
let ethernet_tag = decode_ethernet_tag(&payload[18..22])?;
let mac_addr_len = payload[22];
if mac_addr_len != 48 {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: format!("EVPN Type 2 MAC Addr Length {mac_addr_len} (expected 48)"),
});
}
if payload.len() < 23 + 6 + 1 {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: "EVPN Type 2 truncated before IP Addr Length byte".into(),
});
}
let mac = MacAddress([
payload[23],
payload[24],
payload[25],
payload[26],
payload[27],
payload[28],
]);
let ip_addr_len_bits = payload[29];
let ip_bytes_expected = match ip_addr_len_bits {
0 => 0,
32 => 4,
128 => 16,
other => {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: format!("EVPN Type 2 IP Addr Length {other} bits (expected 0, 32, 128)"),
});
}
};
let ip_start = 30;
if payload.len() < ip_start + ip_bytes_expected + 3 {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: format!(
"EVPN Type 2 truncated: need {} bytes for IP + Label1, have {}",
ip_bytes_expected + 3,
payload.len() - ip_start
),
});
}
let ip = if ip_bytes_expected == 0 {
None
} else {
Some(decode_ip_addr(
&payload[ip_start..ip_start + ip_bytes_expected],
ip_bytes_expected,
"Type 2 IP",
)?)
};
let label1_start = ip_start + ip_bytes_expected;
let label1 = decode_mpls_label(&payload[label1_start..label1_start + 3])?;
let label2_start = label1_start + 3;
let label2 = match payload.len() - label2_start {
0 => None,
3 => Some(decode_mpls_label(&payload[label2_start..label2_start + 3])?),
other => {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: format!(
"EVPN Type 2 trailing bytes {other} (expected 0 or 3 for optional Label2)"
),
});
}
};
Ok(EvpnRoute::MacIp(EvpnMacIp {
rd,
esi,
ethernet_tag,
mac,
ip,
label1,
label2,
}))
}
fn decode_type3(payload: &[u8]) -> Result<EvpnRoute, DecodeError> {
if payload.len() < 13 {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: format!(
"EVPN Type 3 payload too short: {} bytes (need at least 13)",
payload.len()
),
});
}
let rd = decode_rd(&payload[0..8])?;
let ethernet_tag = decode_ethernet_tag(&payload[8..12])?;
let ip_len_bits = payload[12];
let ip_bytes = match ip_len_bits {
32 => 4,
128 => 16,
other => {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: format!("EVPN Type 3 IP Addr Length {other} bits (expected 32 or 128)"),
});
}
};
if payload.len() != 13 + ip_bytes {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: format!(
"EVPN Type 3 payload length {} (expected {})",
payload.len(),
13 + ip_bytes
),
});
}
let originator_ip = decode_ip_addr(&payload[13..], ip_bytes, "Type 3 originator IP")?;
Ok(EvpnRoute::Imet(EvpnImet {
rd,
ethernet_tag,
originator_ip,
}))
}
fn decode_type4(payload: &[u8]) -> Result<EvpnRoute, DecodeError> {
if payload.len() < 19 {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: format!(
"EVPN Type 4 payload too short: {} bytes (need at least 19)",
payload.len()
),
});
}
let rd = decode_rd(&payload[0..8])?;
let esi = decode_esi(&payload[8..18])?;
if esi.is_zero() {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: "EVPN Type 4 ES route with all-zero ESI (RFC 7432 §7.4)".into(),
});
}
let ip_len_bits = payload[18];
let ip_bytes = match ip_len_bits {
32 => 4,
128 => 16,
other => {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: format!("EVPN Type 4 IP Addr Length {other} bits (expected 32 or 128)"),
});
}
};
if payload.len() != 19 + ip_bytes {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: format!(
"EVPN Type 4 payload length {} (expected {})",
payload.len(),
19 + ip_bytes
),
});
}
let originator_ip = decode_ip_addr(&payload[19..], ip_bytes, "Type 4 originator IP")?;
Ok(EvpnRoute::Es(EvpnEs {
rd,
esi,
originator_ip,
}))
}
fn decode_type5(payload: &[u8]) -> Result<EvpnRoute, DecodeError> {
let total = payload.len();
if total != 34 && total != 58 {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: format!("EVPN Type 5 payload length {total} (expected 34 or 58)"),
});
}
let rd = decode_rd(&payload[0..8])?;
let esi = decode_esi(&payload[8..18])?;
let ethernet_tag = decode_ethernet_tag(&payload[18..22])?;
let prefix_len = payload[22];
let is_v6 = total == 58;
let prefix = if is_v6 {
if prefix_len > 128 {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: format!("EVPN Type 5 IPv6 prefix length {prefix_len} > 128"),
});
}
let mut octets = [0u8; 16];
octets.copy_from_slice(&payload[23..39]);
EvpnIpPrefixValue::V6(Ipv6Prefix::new(Ipv6Addr::from(octets), prefix_len))
} else {
if prefix_len > 32 {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: format!("EVPN Type 5 IPv4 prefix length {prefix_len} > 32"),
});
}
let addr = Ipv4Addr::new(payload[23], payload[24], payload[25], payload[26]);
EvpnIpPrefixValue::V4(Ipv4Prefix::new(addr, prefix_len))
};
let (gateway, label_start) = if is_v6 {
let mut octets = [0u8; 16];
octets.copy_from_slice(&payload[39..55]);
(IpAddr::V6(Ipv6Addr::from(octets)), 55)
} else {
(
IpAddr::V4(Ipv4Addr::new(
payload[27],
payload[28],
payload[29],
payload[30],
)),
31,
)
};
let label = decode_mpls_label(&payload[label_start..label_start + 3])?;
Ok(EvpnRoute::IpPrefix(EvpnIpPrefixRoute {
rd,
esi,
ethernet_tag,
prefix,
gateway,
label,
}))
}
pub fn decode_evpn_nlri(mut buf: &[u8]) -> Result<Vec<EvpnRoute>, DecodeError> {
let mut routes = Vec::new();
while !buf.is_empty() {
if buf.len() < 2 {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: "EVPN NLRI truncated: need route-type + length bytes".into(),
});
}
let route_type = buf[0];
let length = usize::from(buf[1]);
if buf.len() < 2 + length {
return Err(DecodeError::MalformedField {
message_type: "UPDATE",
detail: format!(
"EVPN NLRI truncated: route type {route_type} claims length {length}, \
but only {} bytes remain",
buf.len() - 2
),
});
}
let payload = &buf[2..2 + length];
match route_type {
1 => routes.push(decode_type1(payload)?),
2 => routes.push(decode_type2(payload)?),
3 => routes.push(decode_type3(payload)?),
4 => routes.push(decode_type4(payload)?),
5 => routes.push(decode_type5(payload)?),
_ => {}
}
buf = &buf[2 + length..];
}
Ok(routes)
}
fn encode_mpls_label(label: MplsLabel, out: &mut Vec<u8>) {
let v = label.0 & 0x00FF_FFFF;
#[expect(clippy::cast_possible_truncation)]
{
out.push((v >> 16) as u8);
out.push((v >> 8) as u8);
out.push(v as u8);
}
}
fn encode_ip_addr(ip: IpAddr, out: &mut Vec<u8>) {
match ip {
IpAddr::V4(v4) => out.extend_from_slice(&v4.octets()),
IpAddr::V6(v6) => out.extend_from_slice(&v6.octets()),
}
}
fn encode_type1_body(
rd: RouteDistinguisher,
esi: EthernetSegmentIdentifier,
ethernet_tag: EthernetTagId,
label: MplsLabel,
out: &mut Vec<u8>,
) {
out.extend_from_slice(&rd.0);
out.extend_from_slice(&esi.0);
out.extend_from_slice(ðernet_tag.0.to_be_bytes());
encode_mpls_label(label, out);
}
fn encode_type2_body(r: &EvpnMacIp, out: &mut Vec<u8>) {
out.extend_from_slice(&r.rd.0);
out.extend_from_slice(&r.esi.0);
out.extend_from_slice(&r.ethernet_tag.0.to_be_bytes());
out.push(48); out.extend_from_slice(&r.mac.0);
match r.ip {
None => out.push(0),
Some(IpAddr::V4(v4)) => {
out.push(32);
out.extend_from_slice(&v4.octets());
}
Some(IpAddr::V6(v6)) => {
out.push(128);
out.extend_from_slice(&v6.octets());
}
}
encode_mpls_label(r.label1, out);
if let Some(label2) = r.label2 {
encode_mpls_label(label2, out);
}
}
fn encode_type3_body(r: &EvpnImet, out: &mut Vec<u8>) {
out.extend_from_slice(&r.rd.0);
out.extend_from_slice(&r.ethernet_tag.0.to_be_bytes());
match r.originator_ip {
IpAddr::V4(_) => out.push(32),
IpAddr::V6(_) => out.push(128),
}
encode_ip_addr(r.originator_ip, out);
}
fn encode_type4_body(r: &EvpnEs, out: &mut Vec<u8>) {
out.extend_from_slice(&r.rd.0);
out.extend_from_slice(&r.esi.0);
match r.originator_ip {
IpAddr::V4(_) => out.push(32),
IpAddr::V6(_) => out.push(128),
}
encode_ip_addr(r.originator_ip, out);
}
fn encode_type5_body(r: &EvpnIpPrefixRoute, out: &mut Vec<u8>) {
debug_assert!(
matches!(
(&r.prefix, &r.gateway),
(EvpnIpPrefixValue::V4(_), IpAddr::V4(_)) | (EvpnIpPrefixValue::V6(_), IpAddr::V6(_))
),
"EVPN Type 5: gateway family must match prefix family"
);
out.extend_from_slice(&r.rd.0);
out.extend_from_slice(&r.esi.0);
out.extend_from_slice(&r.ethernet_tag.0.to_be_bytes());
match r.prefix {
EvpnIpPrefixValue::V4(p) => {
out.push(p.len);
out.extend_from_slice(&p.addr.octets());
if let IpAddr::V4(gw) = r.gateway {
out.extend_from_slice(&gw.octets());
} else {
out.extend_from_slice(&Ipv4Addr::UNSPECIFIED.octets());
}
}
EvpnIpPrefixValue::V6(p) => {
out.push(p.len);
out.extend_from_slice(&p.addr.octets());
if let IpAddr::V6(gw) = r.gateway {
out.extend_from_slice(&gw.octets());
} else {
out.extend_from_slice(&Ipv6Addr::UNSPECIFIED.octets());
}
}
}
encode_mpls_label(r.label, out);
}
pub fn encode_evpn_nlri(routes: &[EvpnRoute], buf: &mut Vec<u8>) {
for route in routes {
let route_type = route.route_type();
let len_placeholder = buf.len();
buf.push(route_type);
buf.push(0); let body_start = buf.len();
match route {
EvpnRoute::EadPerEs(r) => {
debug_assert!(
r.ethernet_tag.is_max_et(),
"EVPN EAD-per-ES must carry MAX_ET ethernet tag"
);
encode_type1_body(r.rd, r.esi, EthernetTagId::MAX_ET, r.label, buf);
}
EvpnRoute::EadPerEvi(r) => {
debug_assert!(
!r.ethernet_tag.is_max_et(),
"EVPN EAD-per-EVI must not carry MAX_ET ethernet tag"
);
encode_type1_body(r.rd, r.esi, r.ethernet_tag, r.label, buf);
}
EvpnRoute::MacIp(r) => encode_type2_body(r, buf),
EvpnRoute::Imet(r) => encode_type3_body(r, buf),
EvpnRoute::Es(r) => encode_type4_body(r, buf),
EvpnRoute::IpPrefix(r) => encode_type5_body(r, buf),
}
let body_len = buf.len() - body_start;
debug_assert!(
u8::try_from(body_len).is_ok(),
"EVPN NLRI body exceeds 255 bytes"
);
#[expect(clippy::cast_possible_truncation)]
{
buf[len_placeholder + 1] = body_len as u8;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_rd() -> RouteDistinguisher {
RouteDistinguisher([0x00, 0x00, 0xFD, 0xE8, 0x00, 0x00, 0x00, 0x64])
}
fn sample_esi() -> EthernetSegmentIdentifier {
EthernetSegmentIdentifier([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A])
}
fn roundtrip(routes: &[EvpnRoute]) {
let mut buf = Vec::new();
encode_evpn_nlri(routes, &mut buf);
let decoded = decode_evpn_nlri(&buf).expect("decode should succeed");
assert_eq!(routes, decoded.as_slice(), "round-trip mismatch");
}
#[test]
fn rd_display_type0() {
assert_eq!(sample_rd().to_string(), "65000:100");
}
#[test]
fn rd_display_type1() {
let rd = RouteDistinguisher([0x00, 0x01, 10, 0, 0, 1, 0x00, 0x42]);
assert_eq!(rd.to_string(), "10.0.0.1:66");
}
#[test]
fn ethernet_tag_max_et() {
assert!(EthernetTagId::MAX_ET.is_max_et());
assert_eq!(EthernetTagId::MAX_ET.to_string(), "MAX_ET");
assert!(!EthernetTagId(100).is_max_et());
}
#[test]
fn mac_display() {
let mac = MacAddress([0x00, 0x11, 0x22, 0xaa, 0xbb, 0xcc]);
assert_eq!(mac.to_string(), "00:11:22:aa:bb:cc");
}
#[test]
fn mpls_label_vxlan_vni() {
let label = MplsLabel::new(10_000);
assert_eq!(label.as_vni(), 10_000);
}
#[test]
fn roundtrip_type1_per_es() {
roundtrip(&[EvpnRoute::EadPerEs(EvpnEadPerEs {
rd: sample_rd(),
esi: sample_esi(),
ethernet_tag: EthernetTagId::MAX_ET,
label: MplsLabel::new(500),
})]);
}
#[test]
fn roundtrip_type1_per_evi() {
roundtrip(&[EvpnRoute::EadPerEvi(EvpnEadPerEvi {
rd: sample_rd(),
esi: sample_esi(),
ethernet_tag: EthernetTagId(200),
label: MplsLabel::new(10_001),
})]);
}
#[test]
fn roundtrip_type2_mac_only() {
roundtrip(&[EvpnRoute::MacIp(EvpnMacIp {
rd: sample_rd(),
esi: EthernetSegmentIdentifier::ZERO,
ethernet_tag: EthernetTagId(100),
mac: MacAddress([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]),
ip: None,
label1: MplsLabel::new(10_000),
label2: None,
})]);
}
#[test]
fn roundtrip_type2_mac_ipv4_two_labels() {
roundtrip(&[EvpnRoute::MacIp(EvpnMacIp {
rd: sample_rd(),
esi: sample_esi(),
ethernet_tag: EthernetTagId(100),
mac: MacAddress([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]),
ip: Some(IpAddr::V4(Ipv4Addr::new(192, 0, 2, 10))),
label1: MplsLabel::new(10_000),
label2: Some(MplsLabel::new(20_000)),
})]);
}
#[test]
fn roundtrip_type2_mac_ipv6() {
roundtrip(&[EvpnRoute::MacIp(EvpnMacIp {
rd: sample_rd(),
esi: sample_esi(),
ethernet_tag: EthernetTagId(100),
mac: MacAddress([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]),
ip: Some(IpAddr::V6("2001:db8::10".parse().unwrap())),
label1: MplsLabel::new(10_000),
label2: None,
})]);
}
#[test]
fn roundtrip_type3_ipv4() {
roundtrip(&[EvpnRoute::Imet(EvpnImet {
rd: sample_rd(),
ethernet_tag: EthernetTagId(100),
originator_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
})]);
}
#[test]
fn roundtrip_type3_ipv6() {
roundtrip(&[EvpnRoute::Imet(EvpnImet {
rd: sample_rd(),
ethernet_tag: EthernetTagId(100),
originator_ip: IpAddr::V6("2001:db8::1".parse().unwrap()),
})]);
}
#[test]
fn roundtrip_type4_ipv4() {
roundtrip(&[EvpnRoute::Es(EvpnEs {
rd: sample_rd(),
esi: sample_esi(),
originator_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
})]);
}
#[test]
fn roundtrip_type5_ipv4() {
roundtrip(&[EvpnRoute::IpPrefix(EvpnIpPrefixRoute {
rd: sample_rd(),
esi: EthernetSegmentIdentifier::ZERO,
ethernet_tag: EthernetTagId(0),
prefix: EvpnIpPrefixValue::V4(Ipv4Prefix::new(Ipv4Addr::new(10, 100, 0, 0), 24)),
gateway: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
label: MplsLabel::new(20_001),
})]);
}
#[test]
fn roundtrip_type5_ipv6() {
roundtrip(&[EvpnRoute::IpPrefix(EvpnIpPrefixRoute {
rd: sample_rd(),
esi: EthernetSegmentIdentifier::ZERO,
ethernet_tag: EthernetTagId(0),
prefix: EvpnIpPrefixValue::V6(Ipv6Prefix::new("2001:db8:100::".parse().unwrap(), 48)),
gateway: IpAddr::V6(Ipv6Addr::UNSPECIFIED),
label: MplsLabel::new(20_001),
})]);
}
#[test]
fn roundtrip_all_types_one_nlri() {
roundtrip(&[
EvpnRoute::EadPerEs(EvpnEadPerEs {
rd: sample_rd(),
esi: sample_esi(),
ethernet_tag: EthernetTagId::MAX_ET,
label: MplsLabel::new(500),
}),
EvpnRoute::Imet(EvpnImet {
rd: sample_rd(),
ethernet_tag: EthernetTagId(100),
originator_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
}),
EvpnRoute::MacIp(EvpnMacIp {
rd: sample_rd(),
esi: EthernetSegmentIdentifier::ZERO,
ethernet_tag: EthernetTagId(100),
mac: MacAddress([0xaa; 6]),
ip: Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 5))),
label1: MplsLabel::new(10_000),
label2: None,
}),
EvpnRoute::Es(EvpnEs {
rd: sample_rd(),
esi: sample_esi(),
originator_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
}),
EvpnRoute::IpPrefix(EvpnIpPrefixRoute {
rd: sample_rd(),
esi: EthernetSegmentIdentifier::ZERO,
ethernet_tag: EthernetTagId(0),
prefix: EvpnIpPrefixValue::V4(Ipv4Prefix::new(Ipv4Addr::new(192, 168, 0, 0), 24)),
gateway: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
label: MplsLabel::new(20_001),
}),
]);
}
#[test]
fn decode_truncated_nlri_fails() {
let bytes = [
2u8, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
assert!(decode_evpn_nlri(&bytes).is_err());
}
#[test]
fn decode_skips_unknown_route_type() {
let imet = EvpnRoute::Imet(EvpnImet {
rd: sample_rd(),
ethernet_tag: EthernetTagId(100),
originator_ip: IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)),
});
let imet2 = EvpnRoute::Imet(EvpnImet {
rd: sample_rd(),
ethernet_tag: EthernetTagId(200),
originator_ip: IpAddr::V4(Ipv4Addr::new(192, 0, 2, 2)),
});
let mut buf = Vec::new();
encode_evpn_nlri(std::slice::from_ref(&imet), &mut buf);
buf.extend_from_slice(&[99u8, 4, 0xAA, 0xBB, 0xCC, 0xDD]);
encode_evpn_nlri(std::slice::from_ref(&imet2), &mut buf);
let decoded = decode_evpn_nlri(&buf).unwrap();
assert_eq!(decoded.len(), 2, "unknown type should be skipped");
assert!(matches!(decoded[0], EvpnRoute::Imet(_)));
assert!(matches!(decoded[1], EvpnRoute::Imet(_)));
}
#[test]
fn decode_unknown_route_type_truncated_still_fails() {
let bytes = [99u8, 10, 0, 0];
assert!(decode_evpn_nlri(&bytes).is_err());
}
#[test]
fn decode_type1_rejects_zero_esi() {
let mut bytes = vec![1u8, 25];
bytes.extend_from_slice(&[0u8; 8]); bytes.extend_from_slice(&[0u8; 10]); bytes.extend_from_slice(&[0xFF; 4]); bytes.extend_from_slice(&[0, 0, 0]); let err = decode_evpn_nlri(&bytes).unwrap_err();
let DecodeError::MalformedField { detail, .. } = err else {
panic!("expected MalformedField");
};
assert!(detail.contains("Type 1"), "unexpected detail: {detail}");
}
#[test]
fn decode_type4_rejects_zero_esi() {
let mut bytes = vec![4u8, 23];
bytes.extend_from_slice(&[0u8; 8]); bytes.extend_from_slice(&[0u8; 10]); bytes.push(32); bytes.extend_from_slice(&[10, 0, 0, 1]); let err = decode_evpn_nlri(&bytes).unwrap_err();
let DecodeError::MalformedField { detail, .. } = err else {
panic!("expected MalformedField");
};
assert!(detail.contains("Type 4"), "unexpected detail: {detail}");
}
#[test]
fn empty_buffer_decodes_to_empty() {
assert_eq!(decode_evpn_nlri(&[]).unwrap(), Vec::<EvpnRoute>::new());
}
#[test]
fn route_key_discriminates_ead_per_es_vs_per_evi() {
let per_es = EvpnRoute::EadPerEs(EvpnEadPerEs {
rd: sample_rd(),
esi: sample_esi(),
ethernet_tag: EthernetTagId::MAX_ET,
label: MplsLabel::new(500),
});
let per_evi = EvpnRoute::EadPerEvi(EvpnEadPerEvi {
rd: sample_rd(),
esi: sample_esi(),
ethernet_tag: EthernetTagId(200),
label: MplsLabel::new(500),
});
assert_ne!(per_es.key(), per_evi.key());
}
#[test]
fn ead_per_es_encode_round_trips_to_per_es() {
let r = EvpnRoute::EadPerEs(EvpnEadPerEs {
rd: sample_rd(),
esi: sample_esi(),
ethernet_tag: EthernetTagId::MAX_ET,
label: MplsLabel::new(7),
});
let mut buf = Vec::new();
encode_evpn_nlri(std::slice::from_ref(&r), &mut buf);
let decoded = decode_evpn_nlri(&buf).unwrap();
assert_eq!(decoded.len(), 1);
assert!(matches!(decoded[0], EvpnRoute::EadPerEs(_)));
}
#[test]
#[should_panic(expected = "gateway family must match prefix family")]
fn type5_encode_panics_on_family_mismatch_in_debug() {
let r = EvpnRoute::IpPrefix(EvpnIpPrefixRoute {
rd: sample_rd(),
esi: EthernetSegmentIdentifier::ZERO,
ethernet_tag: EthernetTagId(0),
prefix: EvpnIpPrefixValue::V4(Ipv4Prefix::new(Ipv4Addr::new(10, 0, 0, 0), 8)),
gateway: IpAddr::V6(Ipv6Addr::LOCALHOST),
label: MplsLabel::new(100),
});
let mut buf = Vec::new();
encode_evpn_nlri(&[r], &mut buf);
}
}