diff --git a/Cargo.toml b/Cargo.toml index 788b97d0..09e789a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "holo-system", "holo-tools", "holo-utils", + "holo-vrrp", "holo-yang", ] default-members = ["holo-daemon"] diff --git a/README.md b/README.md index 5ac02fed..774a83ed 100644 --- a/README.md +++ b/README.md @@ -211,6 +211,10 @@ Holo supports the following Internet Standards: * RFC 2453 - RIP Version 2 * RFC 4822 - RIPv2 Cryptographic Authentication +##### VRRP + +* RFC 3768 - Virtual Router Redundancy Protocol (VRRP) + ##### IETF YANG implementation coverage | Module | Configuration | State | RPCs | Notifications | Total | @@ -240,6 +244,7 @@ Holo supports the following Internet Standards: | ietf-segment-routing-mpls@2021-05-26 | 62.50% | 0.00% | - | 23.53% | [32.76%](http://westphal.com.br/holo/ietf-segment-routing-mpls.html) | | ietf-segment-routing@2021-05-26 | 100.00% | - | - | - | [100.00%](http://westphal.com.br/holo/ietf-segment-routing.html) | | ietf-system@2014-08-06 | 26.67% | 60.00% | 0.00% | - | [38.24%](http://westphal.com.br/holo/ietf-system@2014-08-06.coverage.md) | +| ietf-vrrp@2018-03-13 | 25.53% | 40.00% | - | 25.00% | [31.73%](http://westphal.com.br/holo/ietf-vrrp@2018-03-13.coverage.md) | ## Funding diff --git a/holo-daemon/Cargo.toml b/holo-daemon/Cargo.toml index d80d39ff..69985145 100644 --- a/holo-daemon/Cargo.toml +++ b/holo-daemon/Cargo.toml @@ -44,6 +44,7 @@ holo-rip = { path = "../holo-rip", optional = true } holo-routing = { path = "../holo-routing", optional = true } holo-system = { path = "../holo-system", optional = true } holo-utils = { path = "../holo-utils" } +holo-vrrp = { path = "../holo-vrrp", optional = true } holo-yang = { path = "../holo-yang" } [build-dependencies] @@ -71,6 +72,7 @@ default = [ "ldp", "ospf", "rip", + "vrrp", ] # Base components @@ -87,6 +89,7 @@ isis = ["dep:holo-isis", "holo-routing/isis"] ldp = ["dep:holo-ldp", "holo-routing/ldp"] ospf = ["dep:holo-ospf", "holo-routing/ospf"] rip = ["dep:holo-rip", "holo-routing/rip"] +vrrp = ["dep:holo-vrrp", "holo-interface/vrrp"] # Other features tokio_console = ["dep:console-subscriber"] diff --git a/holo-daemon/src/northbound/core.rs b/holo-daemon/src/northbound/core.rs index 16558763..ddc1bb5e 100644 --- a/holo-daemon/src/northbound/core.rs +++ b/holo-daemon/src/northbound/core.rs @@ -15,6 +15,7 @@ use holo_northbound::configuration::{CommitPhase, ConfigChange}; use holo_northbound::{ api as papi, CallbackKey, CallbackOp, NbDaemonSender, NbProviderReceiver, }; +use holo_protocol::InstanceShared; use holo_utils::ibus::{IbusReceiver, IbusSender}; use holo_utils::task::TimeoutTask; use holo_utils::yang::SchemaNodeExt; @@ -646,6 +647,12 @@ fn start_providers( let ibus_rx_policy = ibus_tx.subscribe(); let ibus_rx_system = ibus_rx; + let shared = InstanceShared { + db: Some(db), + event_recorder_config: Some(config.event_recorder.clone()), + ..Default::default() + }; + // Start holo-interface. #[cfg(feature = "interface")] { @@ -653,6 +660,7 @@ fn start_providers( provider_tx.clone(), ibus_tx.clone(), ibus_rx_interface, + shared.clone(), ); providers.push(daemon_tx); } @@ -693,13 +701,8 @@ fn start_providers( // Start holo-routing. #[cfg(feature = "routing")] { - let daemon_tx = holo_routing::start( - provider_tx, - ibus_tx, - ibus_rx_routing, - db, - config.event_recorder.clone(), - ); + let daemon_tx = + holo_routing::start(provider_tx, ibus_tx, ibus_rx_routing, shared); providers.push(daemon_tx); } diff --git a/holo-daemon/src/northbound/yang.rs b/holo-daemon/src/northbound/yang.rs index 0ed9fa89..ec4fa62e 100644 --- a/holo-daemon/src/northbound/yang.rs +++ b/holo-daemon/src/northbound/yang.rs @@ -75,6 +75,11 @@ pub(crate) fn create_context() { modules_add::>(&mut modules); modules_add::>(&mut modules); } + #[cfg(feature = "vrrp")] + { + use holo_vrrp::interface::Interface; + modules_add::(&mut modules); + } // Create YANG context and load all required modules and their deviations. let mut yang_ctx = yang::new_context(); diff --git a/holo-interface/Cargo.toml b/holo-interface/Cargo.toml index fce24554..6a4ddbfe 100644 --- a/holo-interface/Cargo.toml +++ b/holo-interface/Cargo.toml @@ -18,6 +18,7 @@ netlink-packet-route.workspace = true netlink-packet-core.workspace = true netlink-packet-utils.workspace = true netlink-sys.workspace = true +regex.workspace = true rtnetlink.workspace = true tokio.workspace = true tracing.workspace = true @@ -25,8 +26,14 @@ yang3.workspace = true libc.workspace = true holo-northbound = { path = "../holo-northbound" } +holo-protocol = { path = "../holo-protocol" } holo-utils = { path = "../holo-utils" } holo-yang = { path = "../holo-yang" } +holo-vrrp = { path = "../holo-vrrp", optional = true } + [lints] workspace = true + +[features] +vrrp = ["holo-vrrp"] diff --git a/holo-interface/src/ibus.rs b/holo-interface/src/ibus.rs index 45d9024f..c3fbb462 100644 --- a/holo-interface/src/ibus.rs +++ b/holo-interface/src/ibus.rs @@ -12,11 +12,11 @@ use holo_utils::southbound::{AddressFlags, AddressMsg, InterfaceUpdateMsg}; use ipnetwork::IpNetwork; use crate::interface::Interface; -use crate::Master; +use crate::{netlink, Master}; // ===== global functions ===== -pub(crate) fn process_msg(master: &mut Master, msg: IbusMsg) { +pub(crate) async fn process_msg(master: &mut Master, msg: IbusMsg) { match msg { IbusMsg::InterfaceDump => { for iface in master.interfaces.iter() { @@ -57,6 +57,50 @@ pub(crate) fn process_msg(master: &mut Master, msg: IbusMsg) { master.interfaces.router_id(), ); } + IbusMsg::MacvlanAdd(msg) => { + if let Some(iface) = master.interfaces.get_by_name(&msg.parent_name) + && let Some(ifindex) = iface.ifindex + { + netlink::macvlan_create( + &master.netlink_handle, + msg.name, + msg.mac_address, + ifindex, + ) + .await; + } + } + IbusMsg::MacvlanDel(ifname) => { + if let Some(iface) = master.interfaces.get_by_name(&ifname) + && let Some(ifindex) = iface.ifindex + { + netlink::iface_delete(&master.netlink_handle, ifindex).await; + } + } + IbusMsg::InterfaceIpAddRequest(msg) => { + if let Some(iface) = master.interfaces.get_by_name(&msg.ifname) + && let Some(ifindex) = iface.ifindex + { + netlink::addr_install( + &master.netlink_handle, + ifindex, + &msg.addr, + ) + .await; + } + } + IbusMsg::InterfaceIpDelRequest(msg) => { + if let Some(iface) = master.interfaces.get_by_name(&msg.ifname) + && let Some(ifindex) = iface.ifindex + { + netlink::addr_uninstall( + &master.netlink_handle, + ifindex, + &msg.addr, + ) + .await; + } + } // Ignore other events. _ => {} } diff --git a/holo-interface/src/interface.rs b/holo-interface/src/interface.rs index 6211bf82..4ef2f4cd 100644 --- a/holo-interface/src/interface.rs +++ b/holo-interface/src/interface.rs @@ -9,6 +9,7 @@ use std::net::{IpAddr, Ipv4Addr}; use bitflags::bitflags; use generational_arena::{Arena, Index}; +use holo_northbound::NbDaemonSender; use holo_utils::ibus::IbusSender; use holo_utils::ip::Ipv4NetworkExt; use holo_utils::southbound::{AddressFlags, InterfaceFlags}; @@ -39,6 +40,7 @@ pub struct Interface { pub addresses: BTreeMap, pub mac_address: [u8; 6], pub owner: Owner, + pub vrrp: Option, } #[derive(Debug)] @@ -123,8 +125,9 @@ impl Interfaces { mtu: None, flags: InterfaceFlags::default(), addresses: Default::default(), - owner: Owner::CONFIG, mac_address: Default::default(), + owner: Owner::CONFIG, + vrrp: None, }; let iface_idx = self.arena.insert(iface); @@ -218,8 +221,9 @@ impl Interfaces { mtu: Some(mtu), flags, addresses: Default::default(), - owner: Owner::SYSTEM, mac_address, + owner: Owner::SYSTEM, + vrrp: None, }; // Notify protocol instances about the interface update. diff --git a/holo-interface/src/lib.rs b/holo-interface/src/lib.rs index f431687e..9c4d0fcb 100644 --- a/holo-interface/src/lib.rs +++ b/holo-interface/src/lib.rs @@ -16,6 +16,7 @@ use holo_northbound::{ process_northbound_msg, NbDaemonReceiver, NbDaemonSender, NbProviderSender, ProviderBase, }; +use holo_protocol::InstanceShared; use holo_utils::ibus::{IbusReceiver, IbusSender}; use tokio::sync::mpsc; use tracing::Instrument; @@ -29,6 +30,8 @@ pub struct Master { pub nb_tx: NbProviderSender, // Internal bus Tx channel. pub ibus_tx: IbusSender, + // Shared data among all protocol instances. + pub shared: InstanceShared, // Netlink socket. pub netlink_handle: rtnetlink::Handle, // List of interfaces. @@ -57,7 +60,7 @@ impl Master { .await; } Ok(msg) = ibus_rx.recv() => { - ibus::process_msg(self, msg); + ibus::process_msg(self, msg).await; } Some((msg, _)) = netlink_rx.next() => { netlink::process_msg(self, msg).await; @@ -73,6 +76,7 @@ pub fn start( nb_tx: NbProviderSender, ibus_tx: IbusSender, ibus_rx: IbusReceiver, + shared: InstanceShared, ) -> NbDaemonSender { let (nb_daemon_tx, nb_daemon_rx) = mpsc::channel(4); @@ -83,6 +87,7 @@ pub fn start( let mut master = Master { nb_tx, ibus_tx, + shared, netlink_handle, interfaces: Default::default(), }; diff --git a/holo-interface/src/netlink.rs b/holo-interface/src/netlink.rs index 139264ee..8e0f84c1 100644 --- a/holo-interface/src/netlink.rs +++ b/holo-interface/src/netlink.rs @@ -26,6 +26,8 @@ use tracing::{error, trace}; use crate::interface::Owner; use crate::Master; +pub const MACVLAN_MODE_BRIDGE: u32 = 4; + pub type NetlinkMonitor = UnboundedReceiver<(NetlinkMessage, SocketAddr)>; @@ -234,6 +236,36 @@ pub(crate) async fn vlan_create( } } +pub(crate) async fn macvlan_create( + handle: &Handle, + name: String, + mac_address: Option<[u8; 6]>, + parent_ifindex: u32, +) { + // Create netlink request + let mut request = handle.link().add().macvlan( + name.clone(), + parent_ifindex, + MACVLAN_MODE_BRIDGE, + ); + + if let Some(address) = mac_address { + request = request.address(address.to_vec()); + } + + // Execute request. + if let Err(error) = request.execute().await { + error!(%parent_ifindex, %name, %error, "Failed to create MacVlan interface"); + } +} + +pub(crate) async fn iface_delete(handle: &Handle, ifindex: u32) { + let request = handle.link().del(ifindex); + if let Err(err) = request.execute().await { + error!(%ifindex, %err, "failed to delete interface."); + } +} + pub(crate) async fn addr_install( handle: &Handle, ifindex: u32, diff --git a/holo-interface/src/northbound/configuration.rs b/holo-interface/src/northbound/configuration.rs index 8acdaec9..d34790f1 100644 --- a/holo-interface/src/northbound/configuration.rs +++ b/holo-interface/src/northbound/configuration.rs @@ -4,21 +4,24 @@ // SPDX-License-Identifier: MIT // -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; use std::net::IpAddr; use std::sync::LazyLock as Lazy; use async_trait::async_trait; use enum_as_inner::EnumAsInner; use holo_northbound::configuration::{ - self, Callbacks, CallbacksBuilder, Provider, ValidationCallbacks, - ValidationCallbacksBuilder, + self, Callbacks, CallbacksBuilder, ConfigChanges, Provider, + ValidationCallbacks, ValidationCallbacksBuilder, }; use holo_northbound::yang::interfaces; +use holo_northbound::{CallbackKey, NbDaemonSender}; +use holo_protocol::spawn_protocol_task; use holo_utils::yang::DataNodeRefExt; use ipnetwork::IpNetwork; use crate::interface::Owner; +use crate::northbound::REGEX_VRRP; use crate::{netlink, Master}; static VALIDATION_CALLBACKS: Lazy = @@ -45,6 +48,7 @@ pub enum Event { VlanCreate(String, u16), AddressInstall(String, IpAddr, u8), AddressUninstall(String, IpAddr, u8), + VrrpStart(String), } // ===== configuration structs ===== @@ -63,10 +67,14 @@ pub struct InterfaceCfg { fn load_callbacks() -> Callbacks { CallbacksBuilder::::default() .path(interfaces::interface::PATH) - .create_apply(|master, args| { + .create_prepare(|master, args| { let ifname = args.dnode.get_string_relative("./name").unwrap(); + master.interfaces.add(ifname.clone()); + + let event_queue = args.event_queue; + event_queue.insert(Event::VrrpStart(ifname)); - master.interfaces.add(ifname); + Ok(()) }) .delete_apply(|_master, args| { let ifname = args.list_entry.into_interface().unwrap(); @@ -302,6 +310,48 @@ impl Provider for Master { Some(&CALLBACKS) } + fn nested_callbacks() -> Option> { + let keys: Vec> = vec![ + #[cfg(feature = "vrrp")] + holo_vrrp::northbound::configuration::CALLBACKS.keys(), + ]; + + Some(keys.concat()) + } + + fn relay_changes( + &self, + changes: ConfigChanges, + ) -> Vec<(ConfigChanges, NbDaemonSender)> { + // Create hash table that maps changes to the appropriate child + // instances. + let mut changes_map: HashMap = HashMap::new(); + for change in changes { + // HACK: parse interface name from VRRP configuration changes. + let caps = REGEX_VRRP.captures(&change.1).unwrap(); + let ifname = caps.get(1).unwrap().as_str().to_owned(); + + // Move configuration change to the appropriate interface bucket. + changes_map.entry(ifname).or_default().push(change); + } + changes_map + .into_iter() + .filter_map(|(ifname, changes)| { + self.interfaces + .get_by_name(&ifname) + .and_then(|iface| iface.vrrp.clone()) + .map(|nb_tx| (changes, nb_tx)) + }) + .collect::>() + } + + fn relay_validation(&self) -> Vec { + self.interfaces + .iter() + .filter_map(|iface| iface.vrrp.clone()) + .collect() + } + async fn process_event(&mut self, event: Event) { match event { Event::InterfaceDelete(ifname) => { @@ -377,6 +427,25 @@ impl Provider for Master { .await; } } + Event::VrrpStart(ifname) => { + #[cfg(feature = "vrrp")] + { + if let Some(iface) = + self.interfaces.get_mut_by_name(&ifname) + { + let vrrp = spawn_protocol_task::< + holo_vrrp::interface::Interface, + >( + ifname, + &self.nb_tx, + &self.ibus_tx, + Default::default(), + self.shared.clone(), + ); + iface.vrrp = Some(vrrp); + } + } + } } } } diff --git a/holo-interface/src/northbound/mod.rs b/holo-interface/src/northbound/mod.rs index dcb2034d..9d4572ff 100644 --- a/holo-interface/src/northbound/mod.rs +++ b/holo-interface/src/northbound/mod.rs @@ -7,8 +7,12 @@ pub mod configuration; pub mod state; +use std::sync::LazyLock as Lazy; + use holo_northbound::rpc::Provider; +use holo_northbound::yang::interfaces; use holo_northbound::ProviderBase; +use regex::Regex; use tracing::{debug_span, Span}; use crate::Master; @@ -36,3 +40,15 @@ impl ProviderBase for Master { // No RPC/Actions to implement. impl Provider for Master {} + +// ===== regular expressions ===== + +// Matches on the protocol type and instance name of a YANG path. +static REGEX_VRRP_STR: Lazy = Lazy::new(|| { + format!( + r"{}\[name='(.+?)'\]/ietf-ip:ipv4/ietf-vrrp:vrrp/*", + interfaces::interface::PATH + ) +}); +pub static REGEX_VRRP: Lazy = + Lazy::new(|| Regex::new(®EX_VRRP_STR).unwrap()); diff --git a/holo-interface/src/northbound/state.rs b/holo-interface/src/northbound/state.rs index 1b9086cd..ab4544cf 100644 --- a/holo-interface/src/northbound/state.rs +++ b/holo-interface/src/northbound/state.rs @@ -4,26 +4,45 @@ // SPDX-License-Identifier: MIT // +use std::borrow::Cow; use std::sync::LazyLock as Lazy; +use enum_as_inner::EnumAsInner; use holo_northbound::state::{ Callbacks, CallbacksBuilder, ListEntryKind, Provider, }; +use holo_northbound::yang::interfaces; +use holo_northbound::{CallbackKey, NbDaemonSender}; +use crate::interface::Interface; use crate::Master; pub static CALLBACKS: Lazy> = Lazy::new(load_callbacks); -#[derive(Debug, Default)] -pub enum ListEntry { +#[derive(Debug, Default, EnumAsInner)] +pub enum ListEntry<'a> { #[default] None, + Interface(&'a Interface), } // ===== callbacks ===== fn load_callbacks() -> Callbacks { - CallbacksBuilder::default().build() + CallbacksBuilder::::default() + .path(interfaces::interface::PATH) + .get_iterate(|master, _args| { + let iter = master.interfaces.iter().map(ListEntry::Interface); + Some(Box::new(iter)) + }) + .get_object(|_master, args| { + use interfaces::interface::Interface; + let iface = args.list_entry.as_interface().unwrap(); + Box::new(Interface { + name: Cow::Borrowed(&iface.name), + }) + }) + .build() } // ===== impl Master ===== @@ -31,13 +50,29 @@ fn load_callbacks() -> Callbacks { impl Provider for Master { const STATE_PATH: &'static str = "/ietf-interfaces:interfaces"; - type ListEntry<'a> = ListEntry; + type ListEntry<'a> = ListEntry<'a>; fn callbacks() -> Option<&'static Callbacks> { Some(&CALLBACKS) } + + fn nested_callbacks() -> Option> { + let keys: Vec> = vec![ + #[cfg(feature = "vrrp")] + holo_vrrp::northbound::state::CALLBACKS.keys(), + ]; + + Some(keys.concat()) + } } // ===== impl ListEntry ===== -impl ListEntryKind for ListEntry {} +impl ListEntryKind for ListEntry<'_> { + fn child_task(&self) -> Option { + match self { + ListEntry::Interface(iface) => iface.vrrp.clone(), + _ => None, + } + } +} diff --git a/holo-routing/src/lib.rs b/holo-routing/src/lib.rs index 31f0bbb2..55bce4fc 100644 --- a/holo-routing/src/lib.rs +++ b/holo-routing/src/lib.rs @@ -18,13 +18,12 @@ use holo_northbound::{ process_northbound_msg, NbDaemonReceiver, NbDaemonSender, NbProviderSender, ProviderBase, }; -use holo_protocol::{event_recorder, InstanceShared}; +use holo_protocol::InstanceShared; use holo_utils::bier::BierCfg; use holo_utils::ibus::{IbusReceiver, IbusSender}; use holo_utils::protocol::Protocol; use holo_utils::southbound::InterfaceFlags; use holo_utils::sr::SrCfg; -use holo_utils::Database; use ipnetwork::IpNetwork; use tokio::sync::mpsc; use tracing::Instrument; @@ -114,17 +113,11 @@ pub fn start( nb_tx: NbProviderSender, ibus_tx: IbusSender, ibus_rx: IbusReceiver, - db: Database, - event_recorder_config: event_recorder::Config, + shared: InstanceShared, ) -> NbDaemonSender { let (nb_daemon_tx, nb_daemon_rx) = mpsc::channel(4); tokio::spawn(async move { - let shared = InstanceShared { - db: Some(db), - event_recorder_config: Some(event_recorder_config), - ..Default::default() - }; let mut master = Master { nb_tx, ibus_tx, diff --git a/holo-tools/Cargo.toml b/holo-tools/Cargo.toml index 2d389ff1..970f6a52 100644 --- a/holo-tools/Cargo.toml +++ b/holo-tools/Cargo.toml @@ -24,6 +24,7 @@ holo-northbound = { path = "../holo-northbound" } holo-ospf = { path = "../holo-ospf", features = ["testing"] } holo-protocol = { path = "../holo-protocol", features = ["testing"] } holo-utils = { path = "../holo-utils", features = ["testing"] } +holo-vrrp = { path = "../holo-vrrp", features = ["testing"] } holo-yang = { path = "../holo-yang" } [lints] diff --git a/holo-tools/src/replay.rs b/holo-tools/src/replay.rs index c0fa52c6..17111a5b 100644 --- a/holo-tools/src/replay.rs +++ b/holo-tools/src/replay.rs @@ -82,5 +82,8 @@ async fn main() { eprintln!("Unsupported protocol type"); std::process::exit(1); } + Protocol::VRRP => { + replay::(filename).await + } } } diff --git a/holo-tools/yang-coverage.sh b/holo-tools/yang-coverage.sh index f90db802..98f17736 100755 --- a/holo-tools/yang-coverage.sh +++ b/holo-tools/yang-coverage.sh @@ -25,4 +25,5 @@ cargo run --bin yang_coverage --\ -m ietf-ospf-sr-mpls\ -m ietf-ospfv3-extended-lsa\ -m ietf-rip\ - -m ietf-system + -m ietf-system\ + -m ietf-vrrp diff --git a/holo-utils/src/ibus.rs b/holo-utils/src/ibus.rs index 1e6aec6b..4ca50ca4 100644 --- a/holo-utils/src/ibus.rs +++ b/holo-utils/src/ibus.rs @@ -17,8 +17,9 @@ use crate::keychain::Keychain; use crate::policy::{MatchSets, Policy}; use crate::protocol::Protocol; use crate::southbound::{ - AddressMsg, BierNbrInstallMsg, BierNbrUninstallMsg, InterfaceUpdateMsg, - LabelInstallMsg, LabelUninstallMsg, RouteKeyMsg, RouteMsg, + AddressMsg, BierNbrInstallMsg, BierNbrUninstallMsg, + InterfaceIpAddRequestMsg, InterfaceIpDelRequestMsg, InterfaceUpdateMsg, + LabelInstallMsg, LabelUninstallMsg, MacvlanAddMsg, RouteKeyMsg, RouteMsg, }; use crate::sr::SrCfg; @@ -64,10 +65,18 @@ pub enum IbusMsg { InterfaceAddressAdd(AddressMsg), // Interface address delete notification. InterfaceAddressDel(AddressMsg), + // Request to add an address to an interface. + InterfaceIpAddRequest(InterfaceIpAddRequestMsg), + // Request to delete an address to an interface. + InterfaceIpDelRequest(InterfaceIpDelRequestMsg), // Keychain update notification. KeychainUpd(Arc), // Keychain delete notification. KeychainDel(String), + // Create a macvlan interface. + MacvlanAdd(MacvlanAddMsg), + // Delete a macvlan interface. + MacvlanDel(String), // Nexthop tracking registration. NexthopTrack(IpAddr), // Nexthop tracking unregistration. diff --git a/holo-utils/src/protocol.rs b/holo-utils/src/protocol.rs index 134f44ec..335f241f 100644 --- a/holo-utils/src/protocol.rs +++ b/holo-utils/src/protocol.rs @@ -26,6 +26,7 @@ pub enum Protocol { RIPV2, RIPNG, STATIC, + VRRP, } // ===== impl Protocol ===== @@ -43,6 +44,7 @@ impl std::fmt::Display for Protocol { Protocol::RIPV2 => write!(f, "ripv2"), Protocol::RIPNG => write!(f, "ripng"), Protocol::STATIC => write!(f, "static"), + Protocol::VRRP => write!(f, "vrrp"), } } } @@ -62,6 +64,7 @@ impl FromStr for Protocol { "ripv2" => Ok(Protocol::RIPV2), "ripng" => Ok(Protocol::RIPNG), "static" => Ok(Protocol::STATIC), + "vrrp" => Ok(Protocol::VRRP), _ => Err(()), } } @@ -80,6 +83,7 @@ impl ToYang for Protocol { Protocol::RIPV2 => "ietf-rip:ripv2".into(), Protocol::RIPNG => "ietf-rip:ripng".into(), Protocol::STATIC => "ietf-routing:static".into(), + Protocol::VRRP => "holo-vrrp:vrrp".into(), } } } @@ -97,6 +101,7 @@ impl TryFromYang for Protocol { "ietf-rip:ripv2" => Some(Protocol::RIPV2), "ietf-rip:ripng" => Some(Protocol::RIPNG), "ietf-routing:static" => Some(Protocol::STATIC), + "holo-vrrp:vrrp" => Some(Protocol::VRRP), _ => None, } } diff --git a/holo-utils/src/southbound.rs b/holo-utils/src/southbound.rs index de2ded95..363c06d8 100644 --- a/holo-utils/src/southbound.rs +++ b/holo-utils/src/southbound.rs @@ -79,6 +79,28 @@ pub struct InterfaceUpdateMsg { pub mac_address: [u8; 6], } +#[derive(Clone, Debug)] +#[derive(Deserialize, Serialize)] +pub struct MacvlanAddMsg { + pub parent_name: String, + pub name: String, + pub mac_address: Option<[u8; 6]>, +} + +#[derive(Clone, Debug)] +#[derive(Deserialize, Serialize)] +pub struct InterfaceIpAddRequestMsg { + pub ifname: String, + pub addr: IpNetwork, +} + +#[derive(Clone, Debug)] +#[derive(Deserialize, Serialize)] +pub struct InterfaceIpDelRequestMsg { + pub ifname: String, + pub addr: IpNetwork, +} + #[derive(Clone, Debug)] #[derive(Deserialize, Serialize)] pub struct AddressMsg { diff --git a/holo-vrrp/Cargo.toml b/holo-vrrp/Cargo.toml new file mode 100644 index 00000000..e14b3e10 --- /dev/null +++ b/holo-vrrp/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "holo-vrrp" +version.workspace = true +authors = ["Paul Wekesa "] +license.workspace = true +edition.workspace = true + +[dependencies] +async-trait.workspace = true +bytes.workspace = true +chrono.workspace = true +enum-as-inner.workspace = true +ipnetwork.workspace = true +libc.workspace = true +nix.workspace = true +serde.workspace = true +serde_json.workspace = true +socket2.workspace = true +tokio.workspace = true +tracing.workspace = true + +holo-northbound = { path = "../holo-northbound" } +holo-protocol = { path = "../holo-protocol" } +holo-utils = { path = "../holo-utils" } +holo-yang = { path = "../holo-yang" } + +[dev-dependencies] +holo-vrrp = { path = ".", features = ["testing"] } +holo-protocol = { path = "../holo-protocol", features = ["testing"] } +holo-utils = { path = "../holo-utils", features = ["testing"] } + +[features] +default = [] +testing = [] diff --git a/holo-vrrp/LICENSE b/holo-vrrp/LICENSE new file mode 100644 index 00000000..374db454 --- /dev/null +++ b/holo-vrrp/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024 The Holo Core Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/holo-vrrp/src/consts.rs b/holo-vrrp/src/consts.rs new file mode 100644 index 00000000..3ea5a33d --- /dev/null +++ b/holo-vrrp/src/consts.rs @@ -0,0 +1,32 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// +// Sponsored by NLnet as part of the Next Generation Internet initiative. +// See: https://nlnet.nl/NGI0 +// + +use std::net::Ipv4Addr; + +// ==== VRRP === + +// valid vrrp versions +pub const VALID_VRRP_VERSIONS: [u8; 1] = [2]; +pub const VRRP_PROTO_NUMBER: i32 = 112; +// maximum size of vrrp header +pub const VRRP_HDR_MAX: usize = 96; +// minimum size of vrrp header. +pub const VRRP_HDR_MIN: usize = 16; +// maximum size of IP + vrrp header. +// For when we use the layer 2 socket +pub const IP_VRRP_HDR_MAX: usize = 130; +pub const VRRP_MULTICAST_ADDRESS: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 18); + +// max size of ip + vrrp header maximum +// number of virtual IP addresses that can be on a VRRP header. +pub const VRRP_IP_COUNT_MAX: usize = 20; + +// ==== IP ==== + +pub const IP_HDR_MIN: usize = 20; diff --git a/holo-vrrp/src/debug.rs b/holo-vrrp/src/debug.rs new file mode 100644 index 00000000..3589d5a3 --- /dev/null +++ b/holo-vrrp/src/debug.rs @@ -0,0 +1,86 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// +// Sponsored by NLnet as part of the Next Generation Internet initiative. +// See: https://nlnet.nl/NGI0 +// + +use std::net::Ipv4Addr; + +use tracing::{debug, debug_span}; + +use crate::instance::fsm; +use crate::packet::VrrpHdr; + +// VRRP debug messages. +#[derive(Debug)] +pub enum Debug<'a> { + // Instances + InstanceCreate(u8), + InstanceDelete(u8), + InstanceStateChange(u8, fsm::Event, fsm::State, fsm::State), + // Network + PacketRx(&'a Ipv4Addr, &'a VrrpHdr), + PacketTx(&'a VrrpHdr), + ArpTx(u8, &'a Ipv4Addr), +} + +// ===== impl Debug ===== + +impl Debug<'_> { + // Log debug message using the tracing API. + pub(crate) fn log(&self) { + match self { + Debug::InstanceCreate(vrid) | Debug::InstanceDelete(vrid) => { + // Parent span(s): vrrp + debug!(%vrid, "{}", self); + } + Debug::InstanceStateChange(vrid, event, old_state, new_state) => { + // Parent span(s): vrrp + debug!(%vrid, ?event, ?old_state, ?new_state, "{}", self); + } + Debug::PacketRx(src, packet) => { + // Parent span(s): vrrp + debug_span!("network").in_scope(|| { + debug_span!("input").in_scope(|| { + let data = serde_json::to_string(&packet).unwrap(); + debug!(%src, %data, "{}", self); + }) + }) + } + Debug::PacketTx(packet) => { + // Parent span(s): vrrp:network:output + let data = serde_json::to_string(&packet).unwrap(); + debug!(%data, "{}", self); + } + Debug::ArpTx(vrid, addr) => { + // Parent span(s): vrrp:network:output + debug!(%vrid, %addr, "{}", self); + } + } + } +} + +impl std::fmt::Display for Debug<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Debug::InstanceCreate(..) => { + write!(f, "instance created") + } + Debug::InstanceDelete(..) => { + write!(f, "instance deleted") + } + Debug::InstanceStateChange(..) => { + write!(f, "instance state change") + } + Debug::PacketRx(..) | Debug::PacketTx(..) => { + write!(f, "packet") + } + Debug::ArpTx(..) => { + write!(f, "gratuitous ARP") + } + } + } +} diff --git a/holo-vrrp/src/error.rs b/holo-vrrp/src/error.rs new file mode 100644 index 00000000..dd82eca5 --- /dev/null +++ b/holo-vrrp/src/error.rs @@ -0,0 +1,223 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// +// Sponsored by NLnet as part of the Next Generation Internet initiative. +// See: https://nlnet.nl/NGI0 +// + +use std::fmt::Debug; +use std::net::{IpAddr, Ipv4Addr}; + +use tracing::{error, warn}; + +use crate::packet::DecodeError; + +// VRRP errors. +#[derive(Debug)] +pub enum Error { + InstanceStartError(u8, IoError), + GlobalError(Ipv4Addr, GlobalError), + VirtualRouterError(Ipv4Addr, VirtualRouterError), +} + +// VRRP I/O errors. +#[derive(Debug)] +pub enum IoError { + SocketError(std::io::Error), + MulticastJoinError(IpAddr, std::io::Error), + MulticastLeaveError(IpAddr, std::io::Error), + RecvError(std::io::Error), + RecvMissingSourceAddr, + SendError(std::io::Error), +} + +// VRRP error that occurred for a packet before it reaches a VRRP router. +#[derive(Debug)] +pub enum GlobalError { + ChecksumError, + IpTtlError, + VersionError, + VridError, +} + +// VRRP error that occurred after a packet reaches a VRRP router. +#[derive(Debug)] +pub enum VirtualRouterError { + AddressListError, + IntervalError, + PacketLengthError, +} + +// ===== impl Error ===== + +impl Error { + pub(crate) fn log(&self) { + match self { + Error::InstanceStartError(vrid, error) => { + error!(%vrid, error = %with_source(error), "{}", self); + } + Error::GlobalError(source, error) => { + warn!(?source, %error, "{}", self); + } + Error::VirtualRouterError(source, error) => { + warn!(?source, %error, "{}", self); + } + } + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InstanceStartError(..) => { + write!(f, "failed to start VRRP instance") + } + Error::GlobalError(_, error) => std::fmt::Display::fmt(error, f), + Error::VirtualRouterError(_, error) => { + std::fmt::Display::fmt(error, f) + } + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::InstanceStartError(_, error) => Some(error), + Error::GlobalError(_, error) => Some(error), + Error::VirtualRouterError(_, error) => Some(error), + } + } +} + +impl From<(Ipv4Addr, DecodeError)> for Error { + fn from((src, error): (Ipv4Addr, DecodeError)) -> Error { + match error { + DecodeError::ChecksumError => { + Error::GlobalError(src, GlobalError::ChecksumError) + } + DecodeError::PacketLengthError { .. } => Error::VirtualRouterError( + src, + VirtualRouterError::PacketLengthError, + ), + } + } +} + +// ===== impl IoError ===== + +impl IoError { + pub(crate) fn log(&self) { + match self { + IoError::SocketError(error) => { + warn!(error = %with_source(error), "{}", self); + } + IoError::MulticastJoinError(addr, error) + | IoError::MulticastLeaveError(addr, error) => { + warn!(?addr, error = %with_source(error), "{}", self); + } + IoError::RecvError(error) | IoError::SendError(error) => { + warn!(error = %with_source(error), "{}", self); + } + IoError::RecvMissingSourceAddr => { + warn!("{}", self); + } + } + } +} + +impl std::fmt::Display for IoError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + IoError::SocketError(..) => { + write!(f, "failed to create raw IP socket") + } + IoError::MulticastJoinError(..) => { + write!(f, "failed to join multicast group") + } + IoError::MulticastLeaveError(..) => { + write!(f, "failed to leave multicast group") + } + IoError::RecvError(..) => { + write!(f, "failed to receive IP packet") + } + IoError::RecvMissingSourceAddr => { + write!( + f, + "failed to retrieve source address from received packet" + ) + } + IoError::SendError(..) => { + write!(f, "failed to send IP packet") + } + } + } +} + +impl std::error::Error for IoError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + IoError::SocketError(error) + | IoError::MulticastJoinError(_, error) + | IoError::MulticastLeaveError(_, error) + | IoError::RecvError(error) + | IoError::SendError(error) => Some(error), + _ => None, + } + } +} + +// ===== impl GlobalError ===== + +impl std::fmt::Display for GlobalError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GlobalError::ChecksumError => { + write!(f, "incorrect checksum received") + } + GlobalError::IpTtlError => { + write!(f, "invalid IP TTL received") + } + GlobalError::VersionError => { + write!(f, "unsupported VRRP version received") + } + GlobalError::VridError => { + write!(f, "invalid VRID received") + } + } + } +} + +impl std::error::Error for GlobalError {} + +// ===== impl VirtualRouterError ===== + +impl std::fmt::Display for VirtualRouterError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + VirtualRouterError::AddressListError => { + write!(f, "address list mismatch") + } + VirtualRouterError::IntervalError => { + write!(f, "advertisement interval mismatch") + } + VirtualRouterError::PacketLengthError => { + write!(f, "invalid packet length") + } + } + } +} + +impl std::error::Error for VirtualRouterError {} + +// ===== global functions ===== + +fn with_source(error: E) -> String { + if let Some(source) = error.source() { + format!("{} ({})", error, with_source(source)) + } else { + error.to_string() + } +} diff --git a/holo-vrrp/src/events.rs b/holo-vrrp/src/events.rs new file mode 100644 index 00000000..9847ec28 --- /dev/null +++ b/holo-vrrp/src/events.rs @@ -0,0 +1,155 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// +// Sponsored by NLnet as part of the Next Generation Internet initiative. +// See: https://nlnet.nl/NGI0 +// + +use std::net::Ipv4Addr; +use std::time::Duration; + +use chrono::Utc; + +use crate::consts::VALID_VRRP_VERSIONS; +use crate::debug::Debug; +use crate::error::{Error, GlobalError, VirtualRouterError}; +use crate::instance::{fsm, MasterReason, VrrpTimer}; +use crate::interface::Interface; +use crate::packet::{DecodeError, DecodeResult, VrrpHdr}; +use crate::tasks; + +// ===== VRRP network packet receipt ===== + +pub(crate) fn process_vrrp_packet( + interface: &mut Interface, + src: Ipv4Addr, + packet: DecodeResult, +) -> Result<(), Error> { + // Check if the packet was decoded successfully. + let packet = match packet { + Ok(packet) => packet, + Err(error) => { + match error { + DecodeError::ChecksumError => { + interface.statistics.checksum_errors += 1; + interface.statistics.discontinuity_time = Utc::now(); + } + DecodeError::PacketLengthError { vrid } => { + if let Some(instance) = interface.instances.get_mut(&vrid) { + instance.state.statistics.pkt_length_errors += 1; + instance.state.statistics.discontinuity_time = + Utc::now(); + } + } + } + return Err(Error::from((src, error))); + } + }; + + // Log received packet. + Debug::PacketRx(&src, &packet).log(); + + // Lookup instance. + let Some((interface, instance)) = interface.get_instance(packet.vrid) + else { + interface.statistics.vrid_errors += 1; + interface.statistics.discontinuity_time = Utc::now(); + return Err(Error::GlobalError(src, GlobalError::VridError)); + }; + + // Update last advertised source address. + instance.state.last_adv_src = Some(src); + + // Sanity checks. + if !VALID_VRRP_VERSIONS.contains(&packet.version) { + interface.statistics.version_errors += 1; + interface.statistics.discontinuity_time = Utc::now(); + let error = GlobalError::VersionError; + return Err(Error::GlobalError(src, error)); + } + if packet.adver_int != instance.config.advertise_interval { + instance.state.statistics.interval_errors += 1; + instance.state.statistics.discontinuity_time = Utc::now(); + let error = VirtualRouterError::IntervalError; + return Err(Error::VirtualRouterError(src, error)); + } + + // Update statistics. + instance.state.statistics.adv_rcvd += 1; + if packet.priority == 0 { + instance.state.statistics.priority_zero_pkts_rcvd += 1; + } + instance.state.statistics.discontinuity_time = Utc::now(); + + // RFC 3768: Section 6.4.2 ("If an ADVERTISEMENT is received") + match instance.state.state { + fsm::State::Initialize => { + unreachable!() + } + fsm::State::Backup => { + if packet.priority == 0 { + let duration = + Duration::from_secs_f32(instance.config.skew_time()); + let task = tasks::master_down_timer( + instance, + duration, + &interface.tx.protocol_input.master_down_timer_tx, + ); + instance.state.timer = VrrpTimer::MasterDownTimer(task); + } else if !instance.config.preempt + || packet.priority >= instance.config.priority + { + instance.timer_reset(); + } + } + fsm::State::Master => { + let primary_addr = interface.system.addresses.first().unwrap().ip(); + if packet.priority == 0 { + instance.send_vrrp_advertisement(primary_addr); + instance.timer_reset(); + } else if packet.priority > instance.config.priority + || (packet.priority == instance.config.priority + && src > primary_addr) + { + instance.change_state( + &interface, + fsm::State::Backup, + fsm::Event::HigherPriorityBackup, + MasterReason::NotMaster, + ); + } + } + } + + Ok(()) +} + +// ====== Master down timer ===== + +pub(crate) fn handle_master_down_timer( + interface: &mut Interface, + vrid: u8, +) -> Result<(), Error> { + // Lookup instance. + let Some((interface, instance)) = interface.get_instance(vrid) else { + return Ok(()); + }; + let Some(src_ip) = interface.system.addresses.first().map(|addr| addr.ip()) + else { + return Ok(()); + }; + + // RFC 3768: Section 6.4.2 ("If the Master_Down_timer fires") + instance.send_vrrp_advertisement(src_ip); + instance.send_gratuitous_arp(); + instance.change_state( + &interface, + fsm::State::Master, + fsm::Event::MasterTimeout, + MasterReason::NoResponse, + ); + + Ok(()) +} diff --git a/holo-vrrp/src/instance.rs b/holo-vrrp/src/instance.rs new file mode 100644 index 00000000..e585ec52 --- /dev/null +++ b/holo-vrrp/src/instance.rs @@ -0,0 +1,496 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// +// Sponsored by NLnet as part of the Next Generation Internet initiative. +// See: https://nlnet.nl/NGI0 +// + +use std::net::Ipv4Addr; +use std::sync::atomic::AtomicU64; +use std::sync::Arc; +use std::time::Duration; + +use chrono::{DateTime, Utc}; +use enum_as_inner::EnumAsInner; +use holo_utils::socket::{AsyncFd, Socket}; +use holo_utils::task::{IntervalTask, Task, TimeoutTask}; +use holo_utils::UnboundedSender; +use tokio::sync::mpsc; + +use crate::consts::{VRRP_MULTICAST_ADDRESS, VRRP_PROTO_NUMBER}; +use crate::debug::Debug; +use crate::error::{Error, IoError}; +use crate::interface::{InterfaceSys, InterfaceView}; +use crate::northbound::configuration::InstanceCfg; +use crate::packet::{ArpHdr, EthernetHdr, Ipv4Hdr, VrrpHdr, VrrpPacket}; +use crate::tasks::messages::output::NetTxPacketMsg; +use crate::{network, southbound, tasks}; + +#[derive(Debug)] +pub struct Instance { + // Virtual Router ID. + pub vrid: u8, + // Instance configuration data. + pub config: InstanceCfg, + // Instance state data. + pub state: InstanceState, + // Macvlan interface. + pub mvlan: InstanceMacvlan, + // Interface raw sockets and Tx/Rx tasks. + pub net: Option, +} + +#[derive(Debug, Default)] +pub struct InstanceState { + pub state: fsm::State, + pub last_event: fsm::Event, + pub new_master_reason: MasterReason, + pub up_time: Option>, + pub timer: VrrpTimer, + pub last_adv_src: Option, + pub statistics: Statistics, +} + +#[derive(Debug)] +pub struct InstanceMacvlan { + // Interface name. + pub name: String, + // Interface system data. + pub system: InterfaceSys, +} + +#[derive(Debug)] +pub struct InstanceNet { + // Raw sockets. + pub socket_vrrp_tx: Arc>, + pub socket_vrrp_rx: Arc>, + pub socket_arp: Arc>, + // Network Tx/Rx tasks. + _net_tx_task: Task<()>, + _vrrp_net_rx_task: Task<()>, + // Network Tx output channel. + pub net_tx_packetp: UnboundedSender, +} + +// Protocol state machine. +pub mod fsm { + #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] + pub enum State { + #[default] + Initialize, + Backup, + Master, + } + + #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] + pub enum Event { + #[default] + None, + Startup, + Shutdown, + HigherPriorityBackup, + MasterTimeout, + InterfaceUp, + InterfaceDown, + NoPrimaryIpAddress, + PrimaryIpAddress, + NoVirtualIpAddresses, + VirtualIpAddresses, + PreemptHoldTimeout, + LowerPriorityMaster, + OwnerPreempt, + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum MasterReason { + #[default] + NotMaster, + Priority, + Preempted, + NoResponse, +} + +#[derive(Debug, Default)] +#[derive(EnumAsInner)] +pub enum VrrpTimer { + #[default] + Null, + AdvTimer(IntervalTask), + MasterDownTimer(TimeoutTask), +} + +#[derive(Debug)] +pub struct Statistics { + pub discontinuity_time: DateTime, + pub master_transitions: u32, + pub adv_rcvd: u64, + pub adv_sent: Arc, + pub interval_errors: u64, + pub priority_zero_pkts_rcvd: u64, + pub priority_zero_pkts_sent: u64, + pub invalid_type_pkts_rcvd: u64, + pub pkt_length_errors: u64, +} + +// ===== impl Instance ===== + +impl Instance { + pub(crate) fn new(vrid: u8) -> Self { + Debug::InstanceCreate(vrid).log(); + + Instance { + vrid, + config: InstanceCfg::default(), + state: InstanceState::default(), + mvlan: InstanceMacvlan::new(vrid), + net: None, + } + } + + pub(crate) fn update(&mut self, interface: &InterfaceView) { + let is_ready = interface.system.ifindex.is_some() + && !interface.system.addresses.is_empty() + && self.mvlan.system.ifindex.is_some(); + if is_ready && self.state.state == fsm::State::Initialize { + self.startup(interface); + } else if !is_ready && self.state.state != fsm::State::Initialize { + self.shutdown(interface); + } + } + + fn startup(&mut self, interface: &InterfaceView) { + match InstanceNet::new(interface, &self.mvlan) { + Ok(net) => { + self.net = Some(net); + if self.config.priority == 255 { + let src_ip = + interface.system.addresses.first().unwrap().ip(); + self.send_vrrp_advertisement(src_ip); + self.send_gratuitous_arp(); + self.change_state( + interface, + fsm::State::Master, + fsm::Event::Startup, + MasterReason::Priority, + ); + } else { + self.change_state( + interface, + fsm::State::Backup, + fsm::Event::Startup, + MasterReason::NotMaster, + ); + } + } + Err(error) => { + Error::InstanceStartError(self.vrid, error).log(); + } + } + } + + pub(crate) fn shutdown(&mut self, interface: &InterfaceView) { + if self.state.state == fsm::State::Master { + // Send an advertisement with Priority = 0. + // TODO + } + + // Transition to the Initialize state. + self.change_state( + interface, + fsm::State::Initialize, + fsm::Event::Shutdown, + MasterReason::NotMaster, + ); + + // Close network sockets and tasks. + self.net = None; + } + + pub(crate) fn change_state( + &mut self, + interface: &InterfaceView, + state: fsm::State, + event: fsm::Event, + new_master_reason: MasterReason, + ) { + if self.state.state == state { + return; + } + + // Log the state transition. + Debug::InstanceStateChange(self.vrid, event, self.state.state, state) + .log(); + + match (self.state.state, state) { + (fsm::State::Initialize, _) => { + // Set the up-time to the current time. + self.state.up_time = Some(Utc::now()); + } + (_, fsm::State::Initialize) => { + // Reset state attributes. + self.state.up_time = None; + self.state.last_adv_src = None; + } + (_, fsm::State::Backup) => { + // Remove virtual IPs from the macvlan interface. + for addr in &self.config.virtual_addresses { + southbound::tx::ip_addr_del( + &interface.tx.ibus, + &self.mvlan.name, + *addr, + ); + } + } + (_, fsm::State::Master) => { + // Add virtual IPs to the macvlan interface. + for addr in &self.config.virtual_addresses { + southbound::tx::ip_addr_add( + &interface.tx.ibus, + &self.mvlan.name, + *addr, + ); + } + } + } + + // Update state and initialize the corresponding timer. + self.state.state = state; + self.state.last_event = event; + self.state.new_master_reason = new_master_reason; + self.timer_set(interface); + } + + pub(crate) fn timer_set(&mut self, interface: &InterfaceView) { + match self.state.state { + fsm::State::Initialize => { + self.state.timer = VrrpTimer::Null; + } + fsm::State::Backup => { + let duration = Duration::from_secs( + self.config.master_down_interval() as u64, + ); + let task = tasks::master_down_timer( + self, + duration, + &interface.tx.protocol_input.master_down_timer_tx, + ); + self.state.timer = VrrpTimer::MasterDownTimer(task); + } + fsm::State::Master => { + let src_ip = interface.system.addresses.first().unwrap().ip(); + let net = self.net.as_ref().unwrap(); + let task = tasks::advertisement_interval( + self, + src_ip, + &net.net_tx_packetp, + ); + self.state.timer = VrrpTimer::AdvTimer(task); + } + } + } + + pub(crate) fn timer_reset(&mut self) { + match &mut self.state.timer { + VrrpTimer::AdvTimer(t) => { + t.reset(Some(Duration::from_secs( + self.config.advertise_interval as u64, + ))); + } + VrrpTimer::MasterDownTimer(t) => { + t.reset(Some(Duration::from_secs( + self.config.master_down_interval() as u64, + ))); + } + _ => {} + } + } + + pub(crate) fn generate_vrrp_packet(&self) -> VrrpHdr { + let mut ip_addresses: Vec = vec![]; + for addr in &self.config.virtual_addresses { + ip_addresses.push(addr.ip()); + } + + let mut packet = VrrpHdr { + version: 2, + hdr_type: 1, + vrid: self.vrid, + priority: self.config.priority, + count_ip: self.config.virtual_addresses.len() as u8, + auth_type: 0, + adver_int: self.config.advertise_interval, + checksum: 0, + ip_addresses, + auth_data: 0, + auth_data2: 0, + }; + packet.generate_checksum(); + packet + } + + pub(crate) fn generate_ipv4_packet( + &self, + src_address: Ipv4Addr, + ) -> Ipv4Hdr { + // 36 bytes (20 IP + 16 vrrp) + // we add 36 to: + // 4 * (no of virtual IPs) -> since the number of + // virtual IPs makes the length of the header variable + let total_length = + (36 + (4 * self.config.virtual_addresses.len())) as u16; + + Ipv4Hdr { + version: 4, + ihl: 5, + tos: 0xc0, + total_length, + identification: 0x0007, + flags: 0x00, + offset: 0x00, + ttl: 255, + protocol: VRRP_PROTO_NUMBER as u8, + checksum: 0x00, + src_address, + dst_address: VRRP_MULTICAST_ADDRESS, + options: None, + padding: None, + } + } + + pub(crate) fn send_vrrp_advertisement(&mut self, src_ip: Ipv4Addr) { + let packet = VrrpPacket { + ip: self.generate_ipv4_packet(src_ip), + vrrp: self.generate_vrrp_packet(), + }; + let msg = NetTxPacketMsg::Vrrp { packet }; + let net = self.net.as_ref().unwrap(); + let _ = net.net_tx_packetp.send(msg); + } + + pub(crate) fn send_gratuitous_arp(&self) { + // Send a gratuitous for each of the virtual IP addresses. + let eth_hdr = EthernetHdr { + ethertype: libc::ETH_P_ARP as _, + dst_mac: [0xff; 6], + src_mac: self.mvlan.system.mac_address, + }; + for addr in &self.config.virtual_addresses { + let arp_hdr = ArpHdr { + hw_type: 1, + proto_type: libc::ETH_P_IP as _, + // MAC address length + hw_length: 6, + proto_length: 4, + operation: 1, + // Sender HW address is virtual MAC + // https://datatracker.ietf.org/doc/html/rfc3768#section-7.3 + sender_hw_address: self.mvlan.system.mac_address, + sender_proto_address: addr.ip(), + target_hw_address: [0xff; 6], + target_proto_address: addr.ip(), + }; + + let msg = NetTxPacketMsg::Arp { + vrid: self.vrid, + ifindex: self.mvlan.system.ifindex.unwrap(), + eth_hdr, + arp_hdr, + }; + let net = self.net.as_ref().unwrap(); + let _ = net.net_tx_packetp.send(msg); + } + } +} + +impl Drop for Instance { + fn drop(&mut self) { + Debug::InstanceDelete(self.vrid).log(); + } +} + +// ==== impl InstanceMacvlan ==== + +impl InstanceMacvlan { + pub(crate) fn new(vrid: u8) -> Self { + let name = format!("mvlan-vrrp-{}", vrid); + Self { + name, + system: InterfaceSys::default(), + } + } +} + +// ==== impl InstanceNet ==== + +impl InstanceNet { + pub(crate) fn new( + parent_iface: &InterfaceView, + mvlan: &InstanceMacvlan, + ) -> Result { + let instance_channels_tx = &parent_iface.tx; + + // Create raw sockets. + let socket_vrrp_rx = network::socket_vrrp_rx(parent_iface) + .map_err(IoError::SocketError) + .and_then(|socket| { + AsyncFd::new(socket).map_err(IoError::SocketError) + }) + .map(Arc::new)?; + let socket_vrrp_tx = network::socket_vrrp_tx(mvlan) + .map_err(IoError::SocketError) + .and_then(|socket| { + AsyncFd::new(socket).map_err(IoError::SocketError) + }) + .map(Arc::new)?; + let socket_arp = network::socket_arp(&mvlan.name) + .map_err(IoError::SocketError) + .and_then(|socket| { + AsyncFd::new(socket).map_err(IoError::SocketError) + }) + .map(Arc::new)?; + + // Start network Tx/Rx tasks. + let (net_tx_packetp, net_tx_packetc) = mpsc::unbounded_channel(); + let net_tx_task = tasks::net_tx( + socket_vrrp_tx.clone(), + socket_arp.clone(), + net_tx_packetc, + #[cfg(feature = "testing")] + &instance_channels_tx.protocol_output, + ); + let vrrp_net_rx_task = tasks::vrrp_net_rx( + socket_vrrp_rx.clone(), + &instance_channels_tx.protocol_input.vrrp_net_packet_tx, + ); + + Ok(Self { + socket_vrrp_tx, + socket_vrrp_rx, + socket_arp, + _net_tx_task: net_tx_task, + _vrrp_net_rx_task: vrrp_net_rx_task, + net_tx_packetp, + }) + } +} + +// ===== impl Statistics ===== + +impl Default for Statistics { + fn default() -> Self { + Statistics { + discontinuity_time: Utc::now(), + master_transitions: 0, + adv_rcvd: 0, + adv_sent: Arc::new(AtomicU64::new(0)), + interval_errors: 0, + priority_zero_pkts_rcvd: 0, + priority_zero_pkts_sent: 0, + invalid_type_pkts_rcvd: 0, + pkt_length_errors: 0, + } + } +} diff --git a/holo-vrrp/src/interface.rs b/holo-vrrp/src/interface.rs new file mode 100644 index 00000000..88b6d6d5 --- /dev/null +++ b/holo-vrrp/src/interface.rs @@ -0,0 +1,256 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// +// Sponsored by NLnet as part of the Next Generation Internet initiative. +// See: https://nlnet.nl/NGI0 +// + +use std::collections::{BTreeMap, BTreeSet}; + +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use holo_protocol::{ + InstanceChannelsTx, InstanceShared, MessageReceiver, ProtocolInstance, +}; +use holo_utils::ibus::IbusMsg; +use holo_utils::ip::AddressFamily; +use holo_utils::protocol::Protocol; +use holo_utils::southbound::InterfaceFlags; +use holo_utils::{Receiver, Sender}; +use ipnetwork::Ipv4Network; +use tokio::sync::mpsc; + +use crate::error::Error; +use crate::instance::Instance; +use crate::tasks::messages::input::{MasterDownTimerMsg, VrrpNetRxPacketMsg}; +use crate::tasks::messages::{ProtocolInputMsg, ProtocolOutputMsg}; +use crate::{events, southbound}; + +#[derive(Debug)] +pub struct Interface { + // Interface name. + pub name: String, + // Interface system data. + pub system: InterfaceSys, + // Interface VRRP instances. + pub instances: BTreeMap, + // Global statistics. + pub statistics: Statistics, + // Tx channels. + pub tx: InstanceChannelsTx, + // Shared data. + pub shared: InstanceShared, +} + +#[derive(Debug, Default)] +pub struct InterfaceSys { + // Interface flags. + pub flags: InterfaceFlags, + // Interface index. + pub ifindex: Option, + // Interface IPv4 addresses. + pub addresses: BTreeSet, + // interface MAC Address + pub mac_address: [u8; 6], +} + +#[derive(Debug, Default)] +pub struct Statistics { + pub discontinuity_time: DateTime, + pub checksum_errors: u64, + pub version_errors: u64, + pub vrid_errors: u64, + pub ip_ttl_errors: u64, +} + +#[derive(Clone, Debug)] +pub struct ProtocolInputChannelsTx { + // VRRP packet Rx event. + pub vrrp_net_packet_tx: Sender, + // Master down timer. + pub master_down_timer_tx: Sender, +} + +#[derive(Debug)] +pub struct ProtocolInputChannelsRx { + // VRRP packet Rx event. + pub vrrp_net_packet_rx: Receiver, + // Master down timer. + pub master_down_timer_rx: Receiver, +} + +pub struct InterfaceView<'a> { + pub name: &'a str, + pub system: &'a mut InterfaceSys, + pub statistics: &'a mut Statistics, + pub tx: &'a InstanceChannelsTx, + pub shared: &'a InstanceShared, +} + +// ===== impl Interface ===== + +impl Interface { + pub(crate) fn get_instance( + &mut self, + vrid: u8, + ) -> Option<(InterfaceView<'_>, &mut Instance)> { + self.instances.get_mut(&vrid).map(|instance| { + ( + InterfaceView { + name: &self.name, + system: &mut self.system, + statistics: &mut self.statistics, + tx: &self.tx, + shared: &self.shared, + }, + instance, + ) + }) + } + + pub(crate) fn iter_instances( + &mut self, + ) -> (InterfaceView<'_>, impl Iterator) { + ( + InterfaceView { + name: &self.name, + system: &mut self.system, + statistics: &mut self.statistics, + tx: &self.tx, + shared: &self.shared, + }, + self.instances.values_mut(), + ) + } + + pub(crate) fn as_view(&mut self) -> InterfaceView<'_> { + InterfaceView { + name: &self.name, + system: &mut self.system, + statistics: &mut self.statistics, + tx: &self.tx, + shared: &self.shared, + } + } +} + +#[async_trait] +impl ProtocolInstance for Interface { + const PROTOCOL: Protocol = Protocol::VRRP; + + type ProtocolInputMsg = ProtocolInputMsg; + type ProtocolOutputMsg = ProtocolOutputMsg; + type ProtocolInputChannelsTx = ProtocolInputChannelsTx; + type ProtocolInputChannelsRx = ProtocolInputChannelsRx; + + async fn new( + name: String, + shared: InstanceShared, + tx: InstanceChannelsTx, + ) -> Interface { + Interface { + name, + system: Default::default(), + instances: Default::default(), + statistics: Default::default(), + tx, + shared, + } + } + + async fn init(&mut self) { + // Request system information about the interface. + let _ = self.tx.ibus.send(IbusMsg::InterfaceQuery { + ifname: self.name.clone(), + af: Some(AddressFamily::Ipv4), + }); + } + + async fn process_ibus_msg(&mut self, msg: IbusMsg) { + if let Err(error) = process_ibus_msg(self, msg).await { + error.log(); + } + } + + fn process_protocol_msg(&mut self, msg: ProtocolInputMsg) { + if let Err(error) = match msg { + // Received network packet. + ProtocolInputMsg::VrrpNetRxPacket(msg) => { + events::process_vrrp_packet(self, msg.src, msg.packet) + } + // Master down timer. + ProtocolInputMsg::MasterDownTimer(msg) => { + events::handle_master_down_timer(self, msg.vrid) + } + } { + error.log(); + } + } + + fn protocol_input_channels( + ) -> (ProtocolInputChannelsTx, ProtocolInputChannelsRx) { + let (vrrp_net_packet_rxp, vrrp_net_packet_rxc) = mpsc::channel(4); + let (master_down_timerp, master_down_timerc) = mpsc::channel(4); + + let tx = ProtocolInputChannelsTx { + vrrp_net_packet_tx: vrrp_net_packet_rxp, + master_down_timer_tx: master_down_timerp, + }; + let rx = ProtocolInputChannelsRx { + vrrp_net_packet_rx: vrrp_net_packet_rxc, + master_down_timer_rx: master_down_timerc, + }; + + (tx, rx) + } + + #[cfg(feature = "testing")] + fn test_dir() -> String { + format!("{}/tests/conformance", env!("CARGO_MANIFEST_DIR"),) + } +} + +// ===== impl ProtocolInputChannelsRx ===== + +#[async_trait] +impl MessageReceiver for ProtocolInputChannelsRx { + async fn recv(&mut self) -> Option { + tokio::select! { + biased; + msg = self.vrrp_net_packet_rx.recv() => { + msg.map(ProtocolInputMsg::VrrpNetRxPacket) + } + msg = self.master_down_timer_rx.recv() => { + msg.map(ProtocolInputMsg::MasterDownTimer) + } + } + } +} + +// ===== helper functions ===== + +async fn process_ibus_msg( + interface: &mut Interface, + msg: IbusMsg, +) -> Result<(), Error> { + match msg { + // Interface update notification. + IbusMsg::InterfaceUpd(msg) => { + southbound::rx::process_iface_update(interface, msg); + } + // Interface address addition notification. + IbusMsg::InterfaceAddressAdd(msg) => { + southbound::rx::process_addr_add(interface, msg); + } + // Interface address delete notification. + IbusMsg::InterfaceAddressDel(msg) => { + southbound::rx::process_addr_del(interface, msg); + } + // Ignore other events. + _ => {} + } + + Ok(()) +} diff --git a/holo-vrrp/src/lib.rs b/holo-vrrp/src/lib.rs new file mode 100644 index 00000000..34fe3bc0 --- /dev/null +++ b/holo-vrrp/src/lib.rs @@ -0,0 +1,26 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// +// Sponsored by NLnet as part of the Next Generation Internet initiative. +// See: https://nlnet.nl/NGI0 +// + +#![cfg_attr( + feature = "testing", + allow(dead_code, unused_variables, unused_imports) +)] +#![feature(let_chains)] + +pub mod consts; +pub mod debug; +pub mod error; +pub mod events; +pub mod instance; +pub mod interface; +pub mod network; +pub mod northbound; +pub mod packet; +pub mod southbound; +pub mod tasks; diff --git a/holo-vrrp/src/network.rs b/holo-vrrp/src/network.rs new file mode 100644 index 00000000..85a61237 --- /dev/null +++ b/holo-vrrp/src/network.rs @@ -0,0 +1,272 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// +// Sponsored by NLnet as part of the Next Generation Internet initiative. +// See: https://nlnet.nl/NGI0 +// + +use std::io::IoSlice; +use std::os::fd::AsRawFd; +use std::sync::Arc; + +use bytes::BufMut; +use holo_utils::socket::{AsyncFd, Socket, SocketExt}; +use holo_utils::{capabilities, Sender, UnboundedReceiver}; +use libc::{AF_PACKET, ETH_P_ARP}; +use nix::sys::socket::{self, LinkAddr, SockaddrIn, SockaddrLike}; +use socket2::{Domain, Protocol, Type}; +use tokio::sync::mpsc::error::SendError; + +use crate::consts::{VRRP_MULTICAST_ADDRESS, VRRP_PROTO_NUMBER}; +use crate::debug::Debug; +use crate::error::IoError; +use crate::instance::InstanceMacvlan; +use crate::interface::InterfaceView; +use crate::packet::{ArpHdr, EthernetHdr, Ipv4Hdr, VrrpHdr, VrrpPacket}; +use crate::tasks::messages::input::VrrpNetRxPacketMsg; +use crate::tasks::messages::output::NetTxPacketMsg; + +// ===== global functions ===== + +pub(crate) fn socket_vrrp_tx( + mvlan: &InstanceMacvlan, +) -> Result { + #[cfg(not(feature = "testing"))] + { + let socket = capabilities::raise(|| { + Socket::new( + Domain::IPV4, + Type::RAW, + Some(Protocol::from(VRRP_PROTO_NUMBER)), + ) + })?; + socket.set_nonblocking(true)?; + socket.set_reuse_address(true)?; + socket.set_multicast_ifindex_v4(mvlan.system.ifindex.unwrap())?; + socket.set_header_included(true)?; + socket.set_multicast_ttl_v4(255)?; + socket.set_tos(libc::IPTOS_PREC_INTERNETCONTROL as u32)?; + capabilities::raise(|| { + socket.bind_device(Some(mvlan.name.as_bytes())) + })?; + + Ok(socket) + } + #[cfg(feature = "testing")] + { + Ok(Socket {}) + } +} + +pub(crate) fn socket_vrrp_rx( + interface: &InterfaceView, +) -> Result { + #[cfg(not(feature = "testing"))] + { + let socket = capabilities::raise(|| { + Socket::new( + Domain::IPV4, + Type::RAW, + Some(Protocol::from(VRRP_PROTO_NUMBER)), + ) + })?; + capabilities::raise(|| { + socket.bind_device(Some(interface.name.as_bytes())) + })?; + socket.set_nonblocking(true)?; + socket.join_multicast_v4( + &VRRP_MULTICAST_ADDRESS, + &interface.system.addresses.first().unwrap().ip(), + )?; + + Ok(socket) + } + #[cfg(feature = "testing")] + { + Ok(Socket {}) + } +} + +pub(crate) fn socket_arp(ifname: &str) -> Result { + #[cfg(not(feature = "testing"))] + { + let socket = capabilities::raise(|| { + Socket::new( + Domain::PACKET, + Type::RAW, + Some(Protocol::from(ETH_P_ARP)), + ) + })?; + capabilities::raise(|| socket.bind_device(Some(ifname.as_bytes())))?; + socket.set_broadcast(true)?; + + Ok(socket) + } + #[cfg(feature = "testing")] + { + Ok(Socket {}) + } +} + +#[cfg(not(feature = "testing"))] +async fn send_packet_vrrp( + socket: &AsyncFd, + packet: VrrpPacket, +) -> Result { + Debug::PacketTx(&packet.vrrp).log(); + + // Encode packet. + let buf = packet.encode(); + + // Send packet. + let iov = [IoSlice::new(&buf)]; + let sockaddr: SockaddrIn = + std::net::SocketAddrV4::new(VRRP_MULTICAST_ADDRESS, 0).into(); + socket + .async_io(tokio::io::Interest::WRITABLE, |socket| { + socket::sendmsg( + socket.as_raw_fd(), + &iov, + &[], + socket::MsgFlags::empty(), + Some(&sockaddr), + ) + .map_err(|errno| errno.into()) + }) + .await + .map_err(IoError::SendError) +} + +#[cfg(not(feature = "testing"))] +async fn send_packet_arp( + socket: &AsyncFd, + vrid: u8, + ifindex: u32, + eth_hdr: EthernetHdr, + arp_hdr: ArpHdr, +) -> Result { + Debug::ArpTx(vrid, &arp_hdr.sender_proto_address).log(); + + // Encode packet. + let mut buf = eth_hdr.encode(); + buf.put(arp_hdr.encode()); + + // Send packet. + let iov = [IoSlice::new(&buf)]; + let mut sll = libc::sockaddr_ll { + sll_family: AF_PACKET as u16, + sll_protocol: (libc::ETH_P_ARP as u16).to_be(), + sll_ifindex: ifindex as i32, + sll_hatype: 0, + sll_pkttype: 0, + sll_halen: 6, + sll_addr: [0; 8], + }; + sll.sll_addr[..6].copy_from_slice(ð_hdr.dst_mac); + let sll_len = size_of_val(&sll) as libc::socklen_t; + let sockaddr = unsafe { + LinkAddr::from_raw(&sll as *const _ as *const _, Some(sll_len)) + } + .unwrap(); + socket + .async_io(tokio::io::Interest::WRITABLE, |socket| { + socket::sendmsg( + socket.as_raw_fd(), + &iov, + &[], + socket::MsgFlags::empty(), + Some(&sockaddr), + ) + .map_err(|errno| errno.into()) + }) + .await + .map_err(IoError::SendError) +} + +#[cfg(not(feature = "testing"))] +pub(crate) async fn write_loop( + socket_vrrp: Arc>, + socket_arp: Arc>, + mut net_tx_packetc: UnboundedReceiver, +) { + while let Some(msg) = net_tx_packetc.recv().await { + match msg { + NetTxPacketMsg::Vrrp { packet } => { + if let Err(error) = send_packet_vrrp(&socket_vrrp, packet).await + { + error.log(); + } + } + NetTxPacketMsg::Arp { + vrid, + ifindex, + eth_hdr, + arp_hdr, + } => { + if let Err(error) = send_packet_arp( + &socket_arp, + vrid, + ifindex, + eth_hdr, + arp_hdr, + ) + .await + { + error.log(); + } + } + } + } +} + +#[cfg(not(feature = "testing"))] +pub(crate) async fn vrrp_read_loop( + socket_vrrp: Arc>, + vrrp_net_packet_rxp: Sender, +) -> Result<(), SendError> { + let mut buf = [0u8; 16384]; + loop { + match socket_vrrp + .async_io(tokio::io::Interest::READABLE, |socket| { + match socket::recv( + socket.as_raw_fd(), + &mut buf, + socket::MsgFlags::empty(), + ) { + Ok(msg) => { + let data = &buf[0..msg]; + + // Since IP header length is given in number of words + // (4 bytes per word), we multiply by 4 to get the actual + // number of bytes. + let ip_header_len = ((data[0] & 0x0f) * 4) as usize; + + let ip_pkt = + Ipv4Hdr::decode(&data[0..ip_header_len]).unwrap(); + let vrrp_pkt = VrrpHdr::decode(&data[ip_header_len..]); + Ok((ip_pkt.src_address, vrrp_pkt)) + } + Err(errno) => Err(errno.into()), + } + }) + .await + { + Ok((src, vrrp_pkt)) => { + let msg = VrrpNetRxPacketMsg { + src, + packet: vrrp_pkt, + }; + vrrp_net_packet_rxp.send(msg).await.unwrap(); + } + Err(error) if error.kind() == std::io::ErrorKind::Interrupted => { + // Retry if the syscall was interrupted (EINTR). + continue; + } + Err(error) => { + IoError::RecvError(error).log(); + } + } + } +} diff --git a/holo-vrrp/src/northbound/configuration.rs b/holo-vrrp/src/northbound/configuration.rs new file mode 100644 index 00000000..bd853b1e --- /dev/null +++ b/holo-vrrp/src/northbound/configuration.rs @@ -0,0 +1,277 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// +// Sponsored by NLnet as part of the Next Generation Internet initiative. +// See: https://nlnet.nl/NGI0 +// + +use std::collections::BTreeSet; +use std::sync::LazyLock as Lazy; + +use async_trait::async_trait; +use enum_as_inner::EnumAsInner; +use holo_northbound::configuration::{ + Callbacks, CallbacksBuilder, Provider, ValidationCallbacks, + ValidationCallbacksBuilder, +}; +use holo_northbound::yang::interfaces; +use holo_utils::yang::DataNodeRefExt; +use ipnetwork::Ipv4Network; + +use crate::instance::{fsm, Instance}; +use crate::interface::Interface; +use crate::southbound; + +#[derive(Debug, Default, EnumAsInner)] +pub enum ListEntry { + #[default] + None, + Vrid(u8), + VirtualIpv4Addr(u8, Ipv4Network), +} + +#[derive(Debug)] +pub enum Resource {} + +#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum Event { + InstanceStart { vrid: u8 }, + InstanceDelete { vrid: u8 }, + VirtualAddressCreate { vrid: u8, addr: Ipv4Network }, + VirtualAddressDelete { vrid: u8, addr: Ipv4Network }, + ResetTimer { vrid: u8 }, +} + +pub static VALIDATION_CALLBACKS: Lazy = + Lazy::new(load_validation_callbacks); +pub static CALLBACKS: Lazy> = Lazy::new(load_callbacks); + +// ===== configuration structs ===== + +#[derive(Debug)] +pub struct InstanceCfg { + pub log_state_change: bool, + pub preempt: bool, + pub priority: u8, + pub advertise_interval: u8, + pub virtual_addresses: BTreeSet, +} + +// ===== callbacks ===== + +fn load_callbacks() -> Callbacks { + CallbacksBuilder::::default() + .path(interfaces::interface::ipv4::vrrp::vrrp_instance::PATH) + .create_apply(|interface, args| { + let vrid = args.dnode.get_u8_relative("./vrid").unwrap(); + let mut instance = Instance::new(vrid); + instance.state.last_event = fsm::Event::Startup; + interface.instances.insert(vrid, instance); + + let event_queue = args.event_queue; + event_queue.insert(Event::InstanceStart { vrid }); + }) + .delete_apply(|_interface, args| { + let vrid = args.list_entry.into_vrid().unwrap(); + + let event_queue = args.event_queue; + event_queue.insert(Event::InstanceDelete { vrid }); + }) + .lookup(|_instance, _list_entry, dnode| { + let vrid = dnode.get_u8_relative("./vrid").unwrap(); + ListEntry::Vrid(vrid) + }) + .path(interfaces::interface::ipv4::vrrp::vrrp_instance::version::PATH) + .modify_apply(|_interface, _args| { + // Nothing to do. + }) + .path(interfaces::interface::ipv4::vrrp::vrrp_instance::log_state_change::PATH) + .modify_apply(|interface, args| { + let vrid = args.list_entry.into_vrid().unwrap(); + let instance = interface.instances.get_mut(&vrid).unwrap(); + + let log_state_change = args.dnode.get_bool(); + instance.config.log_state_change = log_state_change; + }) + .path(interfaces::interface::ipv4::vrrp::vrrp_instance::preempt::enabled::PATH) + .modify_apply(|interface, args| { + let vrid = args.list_entry.into_vrid().unwrap(); + let instance = interface.instances.get_mut(&vrid).unwrap(); + + let preempt = args.dnode.get_bool(); + instance.config.preempt = preempt; + }) + .path(interfaces::interface::ipv4::vrrp::vrrp_instance::priority::PATH) + .modify_apply(|interface, args| { + let vrid = args.list_entry.into_vrid().unwrap(); + let instance = interface.instances.get_mut(&vrid).unwrap(); + + let priority = args.dnode.get_u8(); + instance.config.priority = priority; + + let event_queue = args.event_queue; + event_queue.insert(Event::ResetTimer { vrid }); + }) + .path(interfaces::interface::ipv4::vrrp::vrrp_instance::advertise_interval_sec::PATH) + .modify_apply(|interface, args| { + let vrid = args.list_entry.into_vrid().unwrap(); + let instance = interface.instances.get_mut(&vrid).unwrap(); + + let advertise_interval = args.dnode.get_u8(); + instance.config.advertise_interval = advertise_interval; + }) + .delete_apply(|_interface, _args| { + // Nothing to do. + }) + .path(interfaces::interface::ipv4::vrrp::vrrp_instance::virtual_ipv4_addresses::virtual_ipv4_address::PATH) + .create_apply(|interface, args| { + let vrid = args.list_entry.into_vrid().unwrap(); + let instance = interface.instances.get_mut(&vrid).unwrap(); + + let addr = args.dnode.get_prefix4_relative("ipv4-address").unwrap(); + instance.config.virtual_addresses.insert(addr); + + let event_queue = args.event_queue; + event_queue.insert(Event::VirtualAddressCreate { vrid, addr }); + }) + .delete_apply(|interface, args| { + let (vrid, addr) = args.list_entry.into_virtual_ipv4_addr().unwrap(); + let instance = interface.instances.get_mut(&vrid).unwrap(); + + instance.config.virtual_addresses.remove(&addr); + + let event_queue = args.event_queue; + event_queue.insert(Event::VirtualAddressDelete { vrid, addr }); + }) + .lookup(|_interface, list_entry, dnode| { + let vrid = list_entry.into_vrid().unwrap(); + let addr = dnode.get_prefix4_relative("ipv4-address").unwrap(); + ListEntry::VirtualIpv4Addr(vrid, addr) + }) + .build() +} + +fn load_validation_callbacks() -> ValidationCallbacks { + ValidationCallbacksBuilder::default() + .path(interfaces::interface::ipv4::vrrp::vrrp_instance::version::PATH) + .validate(|args| { + let version = args.dnode.get_string(); + if version != "ietf-vrrp:vrrp-v2" { + return Err("unsupported VRRP version".to_string()); + } + + Ok(()) + }) + .build() +} + +// ===== impl Interface ===== + +#[async_trait] +impl Provider for Interface { + type ListEntry = ListEntry; + type Event = Event; + type Resource = Resource; + + fn validation_callbacks() -> Option<&'static ValidationCallbacks> { + Some(&VALIDATION_CALLBACKS) + } + + fn callbacks() -> Option<&'static Callbacks> { + Some(&CALLBACKS) + } + + async fn process_event(&mut self, event: Event) { + match event { + Event::InstanceStart { vrid } => { + let (interface, instance) = self.get_instance(vrid).unwrap(); + + // Create macvlan interface. + let virtual_mac_addr: [u8; 6] = + [0x00, 0x00, 0x5e, 0x00, 0x01, vrid]; + southbound::tx::mvlan_create( + &interface.tx.ibus, + interface.name.to_owned(), + instance.mvlan.name.clone(), + virtual_mac_addr, + ); + } + Event::InstanceDelete { vrid } => { + let mut instance = self.instances.remove(&vrid).unwrap(); + let interface = self.as_view(); + + // Shut down the instance. + instance.shutdown(&interface); + + // Delete macvlan interface. + southbound::tx::mvlan_delete( + &interface.tx.ibus, + &instance.mvlan.name, + ); + } + Event::VirtualAddressCreate { vrid, addr } => { + let (interface, instance) = self.get_instance(vrid).unwrap(); + + if instance.state.state == fsm::State::Master { + southbound::tx::ip_addr_add( + &interface.tx.ibus, + &instance.mvlan.name, + addr, + ); + instance.timer_set(&interface); + } + } + Event::VirtualAddressDelete { vrid, addr } => { + let (interface, instance) = self.get_instance(vrid).unwrap(); + + if instance.state.state == fsm::State::Master { + southbound::tx::ip_addr_del( + &interface.tx.ibus, + &instance.mvlan.name, + addr, + ); + instance.timer_set(&interface); + } + } + Event::ResetTimer { vrid } => { + let (_, instance) = self.get_instance(vrid).unwrap(); + instance.timer_reset(); + } + } + } +} + +// ===== configuration helpers ===== + +impl InstanceCfg { + pub(crate) const fn master_down_interval(&self) -> u32 { + (3 * self.advertise_interval as u32) + self.skew_time() as u32 + } + + pub(crate) const fn skew_time(&self) -> f32 { + (256_f32 - self.priority as f32) / 256_f32 + } +} + +// ===== configuration defaults ===== + +impl Default for InstanceCfg { + fn default() -> InstanceCfg { + use interfaces::interface::ipv4::vrrp; + + let log_state_change = vrrp::vrrp_instance::log_state_change::DFLT; + let preempt = vrrp::vrrp_instance::preempt::enabled::DFLT; + let priority = vrrp::vrrp_instance::priority::DFLT; + let advertise_interval = + vrrp::vrrp_instance::advertise_interval_sec::DFLT; + InstanceCfg { + log_state_change, + preempt, + priority, + advertise_interval, + virtual_addresses: Default::default(), + } + } +} diff --git a/holo-vrrp/src/northbound/mod.rs b/holo-vrrp/src/northbound/mod.rs new file mode 100644 index 00000000..e6440663 --- /dev/null +++ b/holo-vrrp/src/northbound/mod.rs @@ -0,0 +1,37 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// +// Sponsored by NLnet as part of the Next Generation Internet initiative. +// See: https://nlnet.nl/NGI0 +// + +pub mod configuration; +pub mod notification; +pub mod state; +pub mod yang; + +use holo_northbound::ProviderBase; +use tracing::{debug_span, Span}; + +use crate::interface::Interface; + +// ===== impl Interface ===== + +impl ProviderBase for Interface { + fn yang_modules() -> &'static [&'static str] { + &["ietf-vrrp", "holo-vrrp"] + } + + fn top_level_node(&self) -> String { + "/ietf-interfaces:interfaces".to_owned() + } + + fn debug_span(interface: &str) -> Span { + debug_span!("vrrp", %interface) + } +} + +// No RPC/Actions to implement. +impl holo_northbound::rpc::Provider for Interface {} diff --git a/holo-vrrp/src/northbound/notification.rs b/holo-vrrp/src/northbound/notification.rs new file mode 100644 index 00000000..937245cd --- /dev/null +++ b/holo-vrrp/src/northbound/notification.rs @@ -0,0 +1,33 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// +// Sponsored by NLnet as part of the Next Generation Internet initiative. +// See: https://nlnet.nl/NGI0 +// + +use std::borrow::Cow; +use std::net::Ipv4Addr; + +use holo_northbound::{notification, yang, NbProviderSender}; +use holo_yang::ToYang; + +use crate::instance::MasterReason; + +// ===== global functions ===== + +#[expect(unused)] +pub(crate) fn new_master_event( + nb_tx: &NbProviderSender, + addr: Ipv4Addr, + reason: MasterReason, +) { + use yang::vrrp_new_master_event::{self, VrrpNewMasterEvent}; + + let data = VrrpNewMasterEvent { + master_ip_address: Some(Cow::Owned(addr.into())), + new_master_reason: Some(reason.to_yang()), + }; + notification::send(nb_tx, vrrp_new_master_event::PATH, data); +} diff --git a/holo-vrrp/src/northbound/state.rs b/holo-vrrp/src/northbound/state.rs new file mode 100644 index 00000000..e9606327 --- /dev/null +++ b/holo-vrrp/src/northbound/state.rs @@ -0,0 +1,95 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// +// Sponsored by NLnet as part of the Next Generation Internet initiative. +// See: https://nlnet.nl/NGI0 +// + +use std::borrow::Cow; +use std::sync::atomic::Ordering; +use std::sync::LazyLock as Lazy; + +use enum_as_inner::EnumAsInner; +use holo_northbound::state::{ + Callbacks, CallbacksBuilder, ListEntryKind, Provider, +}; +use holo_northbound::yang::interfaces; +use holo_utils::option::OptionExt; +use holo_yang::ToYang; + +use crate::instance::Instance; +use crate::interface::Interface; + +pub static CALLBACKS: Lazy> = Lazy::new(load_callbacks); + +#[derive(Debug, Default, EnumAsInner)] +pub enum ListEntry<'a> { + #[default] + None, + Instance(u8, &'a Instance), +} + +// ===== callbacks ===== + +fn load_callbacks() -> Callbacks { + CallbacksBuilder::::default() + .path(interfaces::interface::ipv4::vrrp::vrrp_instance::PATH) + .get_iterate(|interface, _args| { + let iter = interface.instances.iter().map(|(vrid, instance)| ListEntry::Instance(*vrid, instance)); + Some(Box::new(iter)) + }) + .get_object(|_interface, args| { + use interfaces::interface::ipv4::vrrp::vrrp_instance::VrrpInstance; + let (vrid, instance) = args.list_entry.as_instance().unwrap(); + Box::new(VrrpInstance { + vrid: *vrid, + state: Some(instance.state.state.to_yang()), + // TODO + is_owner: None, + last_adv_source: instance.state.last_adv_src.map(std::convert::Into::into).map(Cow::Owned).ignore_in_testing(), + up_datetime: instance.state.up_time.as_ref().map(Cow::Borrowed).ignore_in_testing(), + master_down_interval: instance.state.timer.as_master_down_timer().map(|task| task.remaining().as_millis() as u32 / 10).ignore_in_testing(), + // TODO + skew_time: None, + last_event: Some(instance.state.last_event.to_yang()).ignore_in_testing(), + new_master_reason: Some(instance.state.new_master_reason.to_yang()), + }) + }) + .path(interfaces::interface::ipv4::vrrp::vrrp_instance::statistics::PATH) + .get_object(|_interface, args| { + use interfaces::interface::ipv4::vrrp::vrrp_instance::statistics::Statistics; + let (_, instance) = args.list_entry.as_instance().unwrap(); + let statistics = &instance.state.statistics; + Box::new(Statistics { + discontinuity_datetime: Some(Cow::Borrowed(&statistics.discontinuity_time)).ignore_in_testing(), + master_transitions: Some(statistics.master_transitions).ignore_in_testing(), + advertisement_rcvd: Some(statistics.adv_rcvd).ignore_in_testing(), + advertisement_sent: Some(statistics.adv_sent.load(Ordering::Relaxed)).ignore_in_testing(), + interval_errors: Some(statistics.interval_errors).ignore_in_testing(), + priority_zero_pkts_rcvd: Some(statistics.priority_zero_pkts_rcvd).ignore_in_testing(), + priority_zero_pkts_sent: Some(statistics.priority_zero_pkts_sent).ignore_in_testing(), + invalid_type_pkts_rcvd: Some(statistics.invalid_type_pkts_rcvd).ignore_in_testing(), + packet_length_errors: Some(statistics.pkt_length_errors).ignore_in_testing(), + }) + }) + .build() +} + +// ===== impl Interface ===== + +impl Provider for Interface { + // TODO + const STATE_PATH: &'static str = ""; + + type ListEntry<'a> = ListEntry<'a>; + + fn callbacks() -> Option<&'static Callbacks> { + Some(&CALLBACKS) + } +} + +// ===== impl ListEntry ===== + +impl ListEntryKind for ListEntry<'_> {} diff --git a/holo-vrrp/src/northbound/yang.rs b/holo-vrrp/src/northbound/yang.rs new file mode 100644 index 00000000..548e1748 --- /dev/null +++ b/holo-vrrp/src/northbound/yang.rs @@ -0,0 +1,80 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// +// Sponsored by NLnet as part of the Next Generation Internet initiative. +// See: https://nlnet.nl/NGI0 +// + +use std::borrow::Cow; + +use holo_yang::ToYang; + +use crate::instance::{fsm, MasterReason}; + +// ===== ToYang implementations ===== + +impl ToYang for fsm::State { + fn to_yang(&self) -> Cow<'static, str> { + match self { + fsm::State::Initialize => "ietf-vrrp:initialize".into(), + fsm::State::Backup => "ietf-vrrp:backup".into(), + fsm::State::Master => "ietf-vrrp:master".into(), + } + } +} + +impl ToYang for fsm::Event { + fn to_yang(&self) -> Cow<'static, str> { + match self { + fsm::Event::None => "ietf-vrrp:vrrp-event-none".into(), + fsm::Event::Startup => "ietf-vrrp:vrrp-event-startup".into(), + fsm::Event::Shutdown => "ietf-vrrp:vrrp-event-shutdown".into(), + fsm::Event::HigherPriorityBackup => { + "ietf-vrrp:vrrp-event-higher-priority-backup".into() + } + fsm::Event::MasterTimeout => { + "ietf-vrrp:vrrp-event-master-timeout".into() + } + fsm::Event::InterfaceUp => { + "ietf-vrrp:vrrp-event-interface-up".into() + } + fsm::Event::InterfaceDown => { + "ietf-vrrp:vrrp-event-interface-down".into() + } + fsm::Event::NoPrimaryIpAddress => { + "ietf-vrrp:vrrp-event-no-primary-ip-address".into() + } + fsm::Event::PrimaryIpAddress => { + "ietf-vrrp:vrrp-event-primary-ip-address".into() + } + fsm::Event::NoVirtualIpAddresses => { + "ietf-vrrp:vrrp-event-no-virtual-ip-addresses".into() + } + fsm::Event::VirtualIpAddresses => { + "ietf-vrrp:vrrp-event-virtual-ip-addresses".into() + } + fsm::Event::PreemptHoldTimeout => { + "ietf-vrrp:vrrp-event-preempt-hold-timeout".into() + } + fsm::Event::LowerPriorityMaster => { + "ietf-vrrp:vrrp-event-lower-priority-master".into() + } + fsm::Event::OwnerPreempt => { + "ietf-vrrp:vrrp-event-owner-preempt".into() + } + } + } +} + +impl ToYang for MasterReason { + fn to_yang(&self) -> Cow<'static, str> { + match self { + MasterReason::NotMaster => "not-master".into(), + MasterReason::Priority => "priority".into(), + MasterReason::Preempted => "preempted".into(), + MasterReason::NoResponse => "no-response".into(), + } + } +} diff --git a/holo-vrrp/src/packet.rs b/holo-vrrp/src/packet.rs new file mode 100644 index 00000000..01688dc4 --- /dev/null +++ b/holo-vrrp/src/packet.rs @@ -0,0 +1,405 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// +// Sponsored by NLnet as part of the Next Generation Internet initiative. +// See: https://nlnet.nl/NGI0 +// + +use std::net::Ipv4Addr; + +use bytes::{Buf, BufMut, Bytes, BytesMut}; +use holo_utils::bytes::{BytesExt, BytesMutExt}; +use serde::{Deserialize, Serialize}; + +use crate::consts::*; + +// Type aliases. +pub type DecodeResult = Result; + +// +// VRRP Packet Format. +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |Version| Type | Virtual Rtr ID| Priority | Count IP Addrs| +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Auth Type | Adver Int | Checksum | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | IP Address (1) | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | . | +// | . | +// | . | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | IP Address (n) | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Authentication Data (1) | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Authentication Data (2) | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// +#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Deserialize, Serialize)] +pub struct VrrpHdr { + pub version: u8, + pub hdr_type: u8, + pub vrid: u8, + pub priority: u8, + pub count_ip: u8, + pub auth_type: u8, + pub adver_int: u8, + pub checksum: u16, + pub ip_addresses: Vec, + // The following two are only used for backward compatibility. + pub auth_data: u32, + pub auth_data2: u32, +} + +// +// IP packet header +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |Version| IHL |Type of Service| Total Length | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Identification |Flags| Fragment Offset | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Time to Live | Protocol | Header Checksum | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Source Address | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Destination Address | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Options | Padding | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// +#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Deserialize, Serialize)] +pub struct Ipv4Hdr { + pub version: u8, + pub ihl: u8, + pub tos: u8, + pub total_length: u16, + pub identification: u16, + pub flags: u8, + pub offset: u16, + pub ttl: u8, + pub protocol: u8, + pub checksum: u16, + pub src_address: Ipv4Addr, + pub dst_address: Ipv4Addr, + pub options: Option, + pub padding: Option, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Deserialize, Serialize)] +pub struct EthernetHdr { + pub dst_mac: [u8; 6], + pub src_mac: [u8; 6], + pub ethertype: u16, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Deserialize, Serialize)] +pub struct ArpHdr { + pub hw_type: u16, + pub proto_type: u16, + pub hw_length: u8, + pub proto_length: u8, + pub operation: u16, + pub sender_hw_address: [u8; 6], + pub sender_proto_address: Ipv4Addr, + pub target_hw_address: [u8; 6], + pub target_proto_address: Ipv4Addr, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Deserialize, Serialize)] +pub struct VrrpPacket { + pub ip: Ipv4Hdr, + pub vrrp: VrrpHdr, +} + +#[derive(Debug, Eq, PartialEq)] +#[derive(Deserialize, Serialize)] +pub enum DecodeError { + ChecksumError, + PacketLengthError { vrid: u8 }, +} + +// ===== impl Packet ===== + +impl VrrpHdr { + // Encodes VRRP packet into a bytes buffer. + pub fn encode(&self) -> BytesMut { + let mut buf = BytesMut::with_capacity(114); + let ver_type = (self.version << 4) | self.hdr_type; + buf.put_u8(ver_type); + buf.put_u8(self.vrid); + buf.put_u8(self.priority); + buf.put_u8(self.count_ip); + buf.put_u8(self.auth_type); + buf.put_u8(self.adver_int); + buf.put_u16(self.checksum); + for addr in &self.ip_addresses { + buf.put_ipv4(addr); + } + + buf.put_u32(self.auth_data); + buf.put_u32(self.auth_data2); + buf + } + + // Decodes VRRP packet from a bytes buffer. + pub fn decode(data: &[u8]) -> DecodeResult { + let pkt_size = data.len(); + + let mut buf: Bytes = Bytes::copy_from_slice(data); + let ver_type = buf.get_u8(); + let version = ver_type >> 4; + let hdr_type = ver_type & 0x0F; + let vrid = buf.get_u8(); + let priority = buf.get_u8(); + let count_ip = buf.get_u8(); + let auth_type = buf.get_u8(); + let adver_int = buf.get_u8(); + + if !(VRRP_HDR_MIN..=VRRP_HDR_MAX).contains(&pkt_size) + || count_ip as usize > VRRP_IP_COUNT_MAX + || (count_ip * 4) + 16 != pkt_size as u8 + { + return Err(DecodeError::PacketLengthError { vrid }); + } + + let checksum = buf.get_u16(); + + // confirm checksum. checksum position is the third item in 16 bit words + let calculated_checksum = checksum::calculate(data, 3); + if calculated_checksum != checksum { + return Err(DecodeError::ChecksumError); + } + + let mut ip_addresses: Vec = vec![]; + for _ in 0..count_ip { + ip_addresses.push(buf.get_ipv4()); + } + + let auth_data = buf.get_u32(); + let auth_data2 = buf.get_u32(); + + Ok(Self { + version, + hdr_type, + vrid, + priority, + count_ip, + auth_type, + adver_int, + checksum, + ip_addresses, + auth_data, + auth_data2, + }) + } + + pub fn generate_checksum(&mut self) { + self.checksum = checksum::calculate(self.encode().chunk(), 3); + } +} + +impl Ipv4Hdr { + pub fn encode(&self) -> BytesMut { + let mut buf = BytesMut::new(); + + // ver_ihl -> version[4 bits] + ihl[4 bits] + buf.put_u8(self.version << 4 | self.ihl); + buf.put_u8(self.tos); + buf.put_u16(self.total_length); + buf.put_u16(self.identification); + + // flag_off -> flags[4 bits] + offset[12 bits] + let flag_off: u16 = ((self.flags as u16) << 12) | self.offset; + buf.put_u16(flag_off); + buf.put_u8(self.ttl); + buf.put_u8(self.protocol); + buf.put_u16(self.checksum); + buf.put_ipv4(&self.src_address); + buf.put_ipv4(&self.dst_address); + + // the header length for IP is between 20 and 24 + // when 24, the options and padding fields are present. + if let (Some(options), Some(padding)) = (self.options, self.padding) { + let opt_pad: u32 = (options << 8) | (padding as u32); + buf.put_u32(opt_pad); + } + buf + } + + pub fn decode(data: &[u8]) -> DecodeResult { + let mut buf = Bytes::copy_from_slice(data); + + // ver_ihl -> version[4 bits] + ihl[4 bits] + let ver_ihl = buf.get_u8(); + let version = ver_ihl >> 4; + let ihl = ver_ihl & 0x0F; + + let tos = buf.get_u8(); + let total_length = buf.get_u16(); + let identification = buf.get_u16(); + + // flag_off -> flags[4 bits] + offset[12 bits] + let flag_off = buf.get_u16(); + let flags: u8 = (flag_off >> 12) as u8; + let offset: u16 = flag_off & 0xFFF; + + let ttl = buf.get_u8(); + let protocol = buf.get_u8(); + let checksum = buf.get_u16(); + // confirm checksum. checksum position is the 5th 16 bit word + let _calculated_checksum = checksum::calculate(data, 5); + + let src_address = buf.get_ipv4(); + let dst_address = buf.get_ipv4(); + + let mut options: Option = None; + let mut padding: Option = None; + + if ihl > IP_HDR_MIN as u8 { + let opt_pad = buf.get_u32(); + options = Some(opt_pad >> 8); + padding = Some((opt_pad & 0xFF) as u8); + } + Ok(Self { + version, + ihl, + tos, + total_length, + identification, + flags, + offset, + ttl, + protocol, + checksum, + src_address, + dst_address, + options, + padding, + }) + } +} + +impl EthernetHdr { + pub fn encode(&self) -> BytesMut { + let mut buf = BytesMut::new(); + self.dst_mac.iter().for_each(|i| buf.put_u8(*i)); + self.src_mac.iter().for_each(|i| buf.put_u8(*i)); + buf.put_u16(self.ethertype); + buf + } + + pub fn decode(data: &[u8]) -> DecodeResult { + let dst_mac = &data[0..6].try_into(); + let dst_mac: [u8; 6] = dst_mac.unwrap(); + + let src_mac = &data[6..12].try_into(); + let src_mac: [u8; 6] = src_mac.unwrap(); + + Ok(Self { + dst_mac, + src_mac, + ethertype: libc::ETH_P_IP as _, + }) + } +} + +impl VrrpPacket { + pub fn encode(&self) -> BytesMut { + let mut buf = BytesMut::with_capacity(IP_VRRP_HDR_MAX); + buf.put(self.ip.encode()); + buf.put(self.vrrp.encode()); + buf + } +} + +impl ArpHdr { + pub fn encode(&self) -> BytesMut { + let mut buf = BytesMut::with_capacity(28); + buf.put_u16(self.hw_type); + buf.put_u16(self.proto_type); + buf.put_u8(self.hw_length); + buf.put_u8(self.proto_length); + buf.put_u16(self.operation); + buf.put_slice(&self.sender_hw_address); + buf.put_ipv4(&self.sender_proto_address); + buf.put_slice(&self.target_hw_address); + buf.put_ipv4(&self.target_proto_address); + buf + } + + pub fn decode(data: &[u8]) -> DecodeResult { + let mut buf = Bytes::copy_from_slice(data); + let mut sender_hw_address: [u8; 6] = [0; 6]; + let mut target_hw_address: [u8; 6] = [0; 6]; + + let hw_type = buf.get_u16(); + let proto_type = buf.get_u16(); + let hw_length = buf.get_u8(); + let proto_length = buf.get_u8(); + let operation = buf.get_u16(); + buf.copy_to_slice(&mut sender_hw_address); + let sender_proto_address = buf.get_ipv4(); + buf.copy_to_slice(&mut target_hw_address); + let target_proto_address = buf.get_ipv4(); + + Ok(Self { + hw_type, + proto_type, + hw_length, + proto_length, + operation, + sender_hw_address, + sender_proto_address, + target_hw_address, + target_proto_address, + }) + } +} + +pub mod checksum { + pub fn calculate(data: &[u8], checksum_position: usize) -> u16 { + let mut result: u16 = 0; + + // since data is in u8's, we need pairs of the data to get u16 + for (i, pair) in data.chunks(2).enumerate() { + // the fifth pair is the checksum field, which is ignored + if i == checksum_position { + continue; + } + + result = + add_values(result, ((pair[0] as u16) << 8) | pair[1] as u16); + } + + // do a one's complement to get the sum + !result + } + + fn add_values(mut first: u16, mut second: u16) -> u16 { + let mut carry: u32 = 10; + let mut result: u16 = 0; + + while carry != 0 { + let tmp_res = first as u32 + second as u32; + result = (tmp_res & 0xFFFF) as u16; + carry = tmp_res >> 16; + first = result; + second = carry as u16; + } + result + } +} diff --git a/holo-vrrp/src/southbound/mod.rs b/holo-vrrp/src/southbound/mod.rs new file mode 100644 index 00000000..80635437 --- /dev/null +++ b/holo-vrrp/src/southbound/mod.rs @@ -0,0 +1,11 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// +// Sponsored by NLnet as part of the Next Generation Internet initiative. +// See: https://nlnet.nl/NGI0 +// + +pub mod rx; +pub mod tx; diff --git a/holo-vrrp/src/southbound/rx.rs b/holo-vrrp/src/southbound/rx.rs new file mode 100644 index 00000000..17df9364 --- /dev/null +++ b/holo-vrrp/src/southbound/rx.rs @@ -0,0 +1,71 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// +// Sponsored by NLnet as part of the Next Generation Internet initiative. +// See: https://nlnet.nl/NGI0 +// + +use holo_utils::southbound::{AddressMsg, InterfaceUpdateMsg}; +use ipnetwork::IpNetwork; + +use crate::interface::Interface; + +// ===== global functions ===== + +pub(crate) fn process_iface_update( + interface: &mut Interface, + msg: InterfaceUpdateMsg, +) { + let (interface, mut instances) = interface.iter_instances(); + + // Handle updates for the primary VRRP interface. + if msg.ifname == interface.name { + interface.system.flags = msg.flags; + interface.system.ifindex = Some(msg.ifindex); + interface.system.mac_address = msg.mac_address; + for instance in instances { + instance.update(&interface); + } + return; + } + + // Handle updates for VRRP macvlan interfaces. + if let Some(instance) = + instances.find(|instance| msg.ifname == instance.mvlan.name) + { + instance.mvlan.system.flags = msg.flags; + instance.mvlan.system.ifindex = Some(msg.ifindex); + instance.mvlan.system.mac_address = msg.mac_address; + instance.update(&interface); + } +} + +pub(crate) fn process_addr_add(interface: &mut Interface, msg: AddressMsg) { + let (interface, instances) = interface.iter_instances(); + + // Handle address updates for the primary VRRP interface. + if msg.ifname == interface.name { + if let IpNetwork::V4(addr) = msg.addr { + interface.system.addresses.insert(addr); + for instance in instances { + instance.update(&interface); + } + } + } +} + +pub(crate) fn process_addr_del(interface: &mut Interface, msg: AddressMsg) { + let (interface, instances) = interface.iter_instances(); + + // Handle address updates for the primary VRRP interface. + if msg.ifname == interface.name { + if let IpNetwork::V4(addr) = msg.addr { + interface.system.addresses.remove(&addr); + for instance in instances { + instance.update(&interface); + } + } + } +} diff --git a/holo-vrrp/src/southbound/tx.rs b/holo-vrrp/src/southbound/tx.rs new file mode 100644 index 00000000..2531f995 --- /dev/null +++ b/holo-vrrp/src/southbound/tx.rs @@ -0,0 +1,56 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// +// Sponsored by NLnet as part of the Next Generation Internet initiative. +// See: https://nlnet.nl/NGI0 +// + +use holo_utils::ibus::{IbusMsg, IbusSender}; +use holo_utils::southbound::{ + InterfaceIpAddRequestMsg, InterfaceIpDelRequestMsg, MacvlanAddMsg, +}; +use ipnetwork::IpNetwork; + +pub(crate) fn mvlan_create( + ibus_tx: &IbusSender, + parent_name: String, + name: String, + mac_address: [u8; 6], +) { + let msg = MacvlanAddMsg { + parent_name, + name, + mac_address: Some(mac_address), + }; + let _ = ibus_tx.send(IbusMsg::MacvlanAdd(msg)); +} + +pub(crate) fn mvlan_delete(ibus_tx: &IbusSender, name: impl Into) { + let _ = ibus_tx.send(IbusMsg::MacvlanDel(name.into())); +} + +pub(crate) fn ip_addr_add( + ibus_tx: &IbusSender, + ifname: impl Into, + addr: impl Into, +) { + let msg = InterfaceIpAddRequestMsg { + ifname: ifname.into(), + addr: addr.into(), + }; + let _ = ibus_tx.send(IbusMsg::InterfaceIpAddRequest(msg)); +} + +pub(crate) fn ip_addr_del( + ibus_tx: &IbusSender, + ifname: impl Into, + addr: impl Into, +) { + let msg = InterfaceIpDelRequestMsg { + ifname: ifname.into(), + addr: addr.into(), + }; + let _ = ibus_tx.send(IbusMsg::InterfaceIpDelRequest(msg)); +} diff --git a/holo-vrrp/src/tasks.rs b/holo-vrrp/src/tasks.rs new file mode 100644 index 00000000..fdc8aed4 --- /dev/null +++ b/holo-vrrp/src/tasks.rs @@ -0,0 +1,235 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// +// Sponsored by NLnet as part of the Next Generation Internet initiative. +// See: https://nlnet.nl/NGI0 +// + +use std::net::Ipv4Addr; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::time::Duration; + +use holo_utils::socket::{AsyncFd, Socket}; +use holo_utils::task::{IntervalTask, Task, TimeoutTask}; +use holo_utils::{Sender, UnboundedReceiver, UnboundedSender}; +use messages::input::MasterDownTimerMsg; +use messages::output::NetTxPacketMsg; +use tracing::{debug_span, Instrument}; + +use crate::instance::Instance; +use crate::network; +use crate::packet::VrrpPacket; + +// +// VRRP tasks diagram: +// +--------------+ +// | northbound | +// +--------------+ +// | ^ +// | | +// northbound_rx (1x) V | (1x) northbound_tx +// +--------------+ +// | | +// vrrp_net_rx (Nx) -> | instance | -> (Nx) net_tx +// master_down_timer (Nx) -> | | -> (Nx) advertisement_interval +// | | +// +--------------+ +// ibus_tx (1x) | ^ (1x) ibus_rx +// | | +// V | +// +--------------+ +// | ibus | +// +--------------+ +// + +// VRRP inter-task message types. +pub mod messages { + use serde::{Deserialize, Serialize}; + + use crate::packet::{DecodeError, VrrpHdr}; + + // Type aliases. + pub type ProtocolInputMsg = input::ProtocolMsg; + pub type ProtocolOutputMsg = output::ProtocolMsg; + + // Input messages (child task -> main task). + pub mod input { + use std::net::Ipv4Addr; + + use super::*; + + #[derive(Debug, Deserialize, Serialize)] + pub enum ProtocolMsg { + VrrpNetRxPacket(VrrpNetRxPacketMsg), + MasterDownTimer(MasterDownTimerMsg), + } + + #[derive(Debug, Deserialize, Serialize)] + pub struct VrrpNetRxPacketMsg { + pub src: Ipv4Addr, + pub packet: Result, + } + + #[derive(Debug, Deserialize, Serialize)] + pub struct MasterDownTimerMsg { + pub vrid: u8, + } + } + + // Output messages (main task -> child task). + pub mod output { + use super::*; + use crate::packet::{ArpHdr, EthernetHdr, VrrpPacket}; + + #[derive(Debug, Serialize)] + pub enum ProtocolMsg { + NetTxPacket(NetTxPacketMsg), + } + + #[derive(Clone, Debug, Serialize)] + pub enum NetTxPacketMsg { + Vrrp { + packet: VrrpPacket, + }, + Arp { + vrid: u8, + ifindex: u32, + eth_hdr: EthernetHdr, + arp_hdr: ArpHdr, + }, + } + } +} + +// ===== VRRP tasks ===== + +// Network Rx task. +pub(crate) fn vrrp_net_rx( + socket_vrrp: Arc>, + net_packet_rxp: &Sender, +) -> Task<()> { + #[cfg(not(feature = "testing"))] + { + let span1 = debug_span!("network"); + let _span1_guard = span1.enter(); + let span2 = debug_span!("input"); + let _span2_guard = span2.enter(); + + let net_packet_rxp = net_packet_rxp.clone(); + + let span = tracing::span::Span::current(); + Task::spawn( + async move { + let _span_enter = span.enter(); + let _ = + network::vrrp_read_loop(socket_vrrp, net_packet_rxp).await; + } + .in_current_span(), + ) + } + #[cfg(feature = "testing")] + { + Task::spawn(async move { std::future::pending().await }) + } +} + +// Network Tx task. +#[allow(unused_mut)] +pub(crate) fn net_tx( + socket_vrrp: Arc>, + socket_arp: Arc>, + mut net_packet_txc: UnboundedReceiver, + #[cfg(feature = "testing")] proto_output_tx: &Sender< + messages::ProtocolOutputMsg, + >, +) -> Task<()> { + #[cfg(not(feature = "testing"))] + { + let span1 = debug_span!("network"); + let _span1_guard = span1.enter(); + let span2 = debug_span!("output"); + let _span2_guard = span2.enter(); + + let span = tracing::span::Span::current(); + Task::spawn( + async move { + let _span_enter = span.enter(); + network::write_loop(socket_vrrp, socket_arp, net_packet_txc) + .await; + } + .in_current_span(), + ) + } + #[cfg(feature = "testing")] + { + let proto_output_tx = proto_output_tx.clone(); + Task::spawn(async move { + // Relay message to the test framework. + while let Some(msg) = net_packet_txc.recv().await { + let msg = messages::ProtocolOutputMsg::NetTxPacket(msg); + let _ = proto_output_tx.send(msg).await; + } + }) + } +} + +// Master down timer. +pub(crate) fn master_down_timer( + instance: &mut Instance, + duration: Duration, + master_down_timer_rx: &Sender, +) -> TimeoutTask { + #[cfg(not(feature = "testing"))] + { + let vrid = instance.vrid; + let master_down_timer_rx = master_down_timer_rx.clone(); + + TimeoutTask::new(duration, move || async move { + let _ = master_down_timer_rx + .send(messages::input::MasterDownTimerMsg { vrid }) + .await; + }) + } + #[cfg(feature = "testing")] + { + TimeoutTask {} + } +} + +// Advertisement interval. +pub(crate) fn advertisement_interval( + instance: &Instance, + src_ip: Ipv4Addr, + net_tx_packetp: &UnboundedSender, +) -> IntervalTask { + #[cfg(not(feature = "testing"))] + { + let packet = VrrpPacket { + ip: instance.generate_ipv4_packet(src_ip), + vrrp: instance.generate_vrrp_packet(), + }; + let adv_sent = instance.state.statistics.adv_sent.clone(); + let net_tx_packetp = net_tx_packetp.clone(); + IntervalTask::new( + Duration::from_secs(instance.config.advertise_interval as u64), + true, + move || { + let adv_sent = adv_sent.clone(); + let packet = packet.clone(); + let net_tx_packetp = net_tx_packetp.clone(); + async move { + adv_sent.fetch_add(1, Ordering::Relaxed); + let msg = NetTxPacketMsg::Vrrp { packet }; + let _ = net_tx_packetp.send(msg); + } + }, + ) + } + #[cfg(feature = "testing")] + { + IntervalTask {} + } +} diff --git a/holo-vrrp/tests/mod.rs b/holo-vrrp/tests/mod.rs new file mode 100644 index 00000000..62352e6c --- /dev/null +++ b/holo-vrrp/tests/mod.rs @@ -0,0 +1,10 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// +// Sponsored by NLnet as part of the Next Generation Internet initiative. +// See: https://nlnet.nl/NGI0 +// + +mod packet; diff --git a/holo-vrrp/tests/packet/mod.rs b/holo-vrrp/tests/packet/mod.rs new file mode 100644 index 00000000..b0f81f81 --- /dev/null +++ b/holo-vrrp/tests/packet/mod.rs @@ -0,0 +1,139 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// +// Sponsored by NLnet as part of the Next Generation Internet initiative. +// See: https://nlnet.nl/NGI0 +// + +use std::net::Ipv4Addr; +use std::sync::LazyLock; + +use holo_vrrp::consts::{VRRP_MULTICAST_ADDRESS, VRRP_PROTO_NUMBER}; +use holo_vrrp::packet::{EthernetHdr, Ipv4Hdr, VrrpHdr}; + +static VRRPHDR: LazyLock<(Vec, VrrpHdr)> = LazyLock::new(|| { + ( + vec![ + 0x21, 0x33, 0x1e, 0x01, 0x00, 0x01, 0xb5, 0xc5, 0x0a, 0x00, 0x01, + 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ], + VrrpHdr { + version: 2, + hdr_type: 1, + vrid: 51, + priority: 30, + count_ip: 1, + auth_type: 0, + adver_int: 1, + checksum: 0xb5c5, + ip_addresses: vec![Ipv4Addr::new(10, 0, 1, 5)], + auth_data: 0, + auth_data2: 0, + }, + ) +}); + +static IPV4HDR: LazyLock<(Vec, Ipv4Hdr)> = LazyLock::new(|| { + ( + vec![ + 0x45, 0xc0, 0x00, 0x28, 0x08, 0x9d, 0x00, 0x00, 0xff, 0x70, 0xad, + 0x4b, 0xc0, 0xa8, 0x64, 0x02, 0xe0, 0x00, 0x00, 0x12, + ], + Ipv4Hdr { + version: 4, + ihl: 5, + tos: 0xc0, + total_length: 40, + identification: 0x089d, + flags: 0, + offset: 0, + ttl: 255, + protocol: VRRP_PROTO_NUMBER as u8, + checksum: 0xad4b, + src_address: Ipv4Addr::new(192, 168, 100, 2), + dst_address: VRRP_MULTICAST_ADDRESS, + options: None, + padding: None, + }, + ) +}); + +static ETHERNETHDR: LazyLock<(Vec, EthernetHdr)> = LazyLock::new(|| { + ( + vec![ + 0x01, 0x00, 0x5e, 0x00, 0x00, 0x12, 0x00, 0x00, 0x5e, 0x00, 0x01, + 0x33, 0x08, 0x00, + ], + EthernetHdr { + dst_mac: [0x01, 0x00, 0x5e, 0x00, 0x00, 0x12], + src_mac: [0x00, 0x00, 0x5e, 0x00, 0x01, 0x33], + ethertype: libc::ETH_P_IP as _, + }, + ) +}); + +#[test] +fn test_encode_vrrphdr() { + let (ref bytes, ref vrrphdr) = *VRRPHDR; + + let generated_bytes = vrrphdr.encode(); + let generated_data = generated_bytes.as_ref(); + let expected_data: &[u8] = bytes.as_ref(); + assert_eq!(generated_data, expected_data); +} + +#[test] +fn test_decode_vrrphdr() { + let (ref bytes, ref vrrphdr) = *VRRPHDR; + let data = bytes.as_ref(); + let generated_hdr = VrrpHdr::decode(data); + assert!(generated_hdr.is_ok()); + + let mut generated_hdr = generated_hdr.unwrap(); + generated_hdr.generate_checksum(); + assert_eq!(vrrphdr, &generated_hdr); +} + +#[test] +fn test_encode_ipv4hdr() { + let (ref bytes, ref iphdr) = *IPV4HDR; + + let generated_bytes = iphdr.encode(); + let generated_data = generated_bytes.as_ref(); + let expected_data: &[u8] = bytes.as_ref(); + assert_eq!(generated_data, expected_data); +} + +#[test] +fn test_decode_ipv4hdr() { + let (ref bytes, ref ipv4hdr) = *IPV4HDR; + let data = bytes.as_ref(); + let generated_hdr = Ipv4Hdr::decode(data); + assert!(generated_hdr.is_ok()); + + let generated_hdr = generated_hdr.unwrap(); + assert_eq!(ipv4hdr, &generated_hdr); +} + +#[test] +fn test_encode_ethernethdr() { + let (ref bytes, ref ethernethdr) = *ETHERNETHDR; + + let generated_bytes = ethernethdr.encode(); + let generated_data = generated_bytes.as_ref(); + let expected_data: &[u8] = bytes.as_ref(); + assert_eq!(generated_data, expected_data); +} + +#[test] +fn test_decode_ethernethdr() { + let (ref bytes, ref ethernethdr) = *ETHERNETHDR; + let data = bytes.as_ref(); + let generated_hdr = EthernetHdr::decode(data); + assert!(generated_hdr.is_ok()); + + let generated_hdr = generated_hdr.unwrap(); + assert_eq!(ethernethdr, &generated_hdr); +} diff --git a/holo-yang/modules/augmentations/holo-vrrp.yang b/holo-yang/modules/augmentations/holo-vrrp.yang new file mode 100644 index 00000000..7eedcb71 --- /dev/null +++ b/holo-yang/modules/augmentations/holo-vrrp.yang @@ -0,0 +1,26 @@ +module holo-vrrp { + yang-version 1.1; + namespace "http://holo-routing.org/yang/holo-vrrp"; + prefix holo-vrrp; + + import ietf-routing { + prefix rt; + } + + organization + "Holo Routing Stack"; + + description + "This module defines augment statements for the ietf-vrrp + module."; + + /* + * Identities. + */ + + identity vrrp { + base rt:routing-protocol; + description + "VRRP protocol."; + } +} diff --git a/holo-yang/modules/deviations/holo-ietf-vrrp-deviations.yang b/holo-yang/modules/deviations/holo-ietf-vrrp-deviations.yang new file mode 100644 index 00000000..5a0ce4da --- /dev/null +++ b/holo-yang/modules/deviations/holo-ietf-vrrp-deviations.yang @@ -0,0 +1,611 @@ +module holo-ietf-vrrp-deviations { + yang-version 1.1; + namespace "http://holo-routing.org/yang/holo-ietf-vrrp-deviations"; + prefix holo-ietf-vrrp-deviations; + + import ietf-interfaces { + prefix if; + } + + import ietf-ip { + prefix ip; + } + + import ietf-vrrp { + prefix vrrp; + } + + organization + "Holo Routing Stack"; + + description + "This module defines deviation statements for the ietf-vrrp + module."; + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:vrid" { + deviate not-supported; + } + */ + + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:version" { + deviate replace { + mandatory false; + } + deviate add { + default "vrrp:vrrp-v2"; + } + } + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:version" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:log-state-change" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:preempt" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:preempt/vrrp:enabled" { + deviate not-supported; + } + */ + + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:preempt/vrrp:hold-time" { + deviate not-supported; + } + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:priority" { + deviate not-supported; + } + */ + + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:accept-mode" { + deviate not-supported; + } + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:advertise-interval-choice" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:advertise-interval-choice/vrrp:v2" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:advertise-interval-choice/vrrp:v2/vrrp:advertise-interval-sec" { + deviate not-supported; + } + */ + + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:advertise-interval-choice/vrrp:v3" { + deviate not-supported; + } + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:advertise-interval-choice/vrrp:v3/vrrp:advertise-interval-centi-sec" { + deviate not-supported; + } + */ + + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:track" { + deviate not-supported; + } + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:track/vrrp:interfaces" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:track/vrrp:interfaces/vrrp:interface" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:track/vrrp:interfaces/vrrp:interface/vrrp:interface" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:track/vrrp:interfaces/vrrp:interface/vrrp:priority-decrement" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:track/vrrp:networks" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:track/vrrp:networks/vrrp:network" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:track/vrrp:networks/vrrp:network/vrrp:prefix" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:track/vrrp:networks/vrrp:network/vrrp:priority-decrement" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:virtual-ipv4-addresses" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:virtual-ipv4-addresses/vrrp:virtual-ipv4-address" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:virtual-ipv4-addresses/vrrp:virtual-ipv4-address/vrrp:ipv4-address" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:state" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:is-owner" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:last-adv-source" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:up-datetime" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:master-down-interval" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:skew-time" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:last-event" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:new-master-reason" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:statistics" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:statistics/vrrp:discontinuity-datetime" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:statistics/vrrp:master-transitions" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:statistics/vrrp:advertisement-rcvd" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:statistics/vrrp:advertisement-sent" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:statistics/vrrp:priority-zero-pkts-rcvd" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:statistics/vrrp:priority-zero-pkts-sent" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:statistics/vrrp:invalid-type-pkts-rcvd" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:statistics/vrrp:packet-length-errors" { + deviate not-supported; + } + */ + + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp" { + deviate not-supported; + } + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:vrid" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:version" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:log-state-change" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:preempt" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:preempt/vrrp:enabled" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:preempt/vrrp:hold-time" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:priority" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:accept-mode" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:advertise-interval-centi-sec" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:track" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:track/vrrp:interfaces" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:track/vrrp:interfaces/vrrp:interface" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:track/vrrp:interfaces/vrrp:interface/vrrp:interface" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:track/vrrp:interfaces/vrrp:interface/vrrp:priority-decrement" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:track/vrrp:networks" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:track/vrrp:networks/vrrp:network" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:track/vrrp:networks/vrrp:network/vrrp:prefix" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:track/vrrp:networks/vrrp:network/vrrp:priority-decrement" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:virtual-ipv6-addresses" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:virtual-ipv6-addresses/vrrp:virtual-ipv6-address" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:virtual-ipv6-addresses/vrrp:virtual-ipv6-address/vrrp:ipv6-address" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:state" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:is-owner" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:last-adv-source" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:up-datetime" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:master-down-interval" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:skew-time" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:last-event" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:new-master-reason" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:statistics" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:statistics/vrrp:discontinuity-datetime" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:statistics/vrrp:master-transitions" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:statistics/vrrp:advertisement-rcvd" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:statistics/vrrp:advertisement-sent" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:statistics/vrrp:priority-zero-pkts-rcvd" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:statistics/vrrp:priority-zero-pkts-sent" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:statistics/vrrp:invalid-type-pkts-rcvd" { + deviate not-supported; + } + */ + + /* + deviation "/if:interfaces/if:interface/ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:statistics/vrrp:packet-length-errors" { + deviate not-supported; + } + */ + + deviation "/vrrp:vrrp" { + deviate not-supported; + } + + /* + deviation "/vrrp:vrrp/vrrp:virtual-routers" { + deviate not-supported; + } + */ + + /* + deviation "/vrrp:vrrp/vrrp:interfaces" { + deviate not-supported; + } + */ + + /* + deviation "/vrrp:vrrp/vrrp:statistics" { + deviate not-supported; + } + */ + + /* + deviation "/vrrp:vrrp/vrrp:statistics/vrrp:discontinuity-datetime" { + deviate not-supported; + } + */ + + /* + deviation "/vrrp:vrrp/vrrp:statistics/vrrp:checksum-errors" { + deviate not-supported; + } + */ + + /* + deviation "/vrrp:vrrp/vrrp:statistics/vrrp:version-errors" { + deviate not-supported; + } + */ + + /* + deviation "/vrrp:vrrp/vrrp:statistics/vrrp:vrid-errors" { + deviate not-supported; + } + */ + + /* + deviation "/vrrp:vrrp/vrrp:statistics/vrrp:ip-ttl-errors" { + deviate not-supported; + } + */ + + /* + deviation "/vrrp:vrrp-new-master-event" { + deviate not-supported; + } + */ + + /* + deviation "/vrrp:vrrp-new-master-event/vrrp:master-ip-address" { + deviate not-supported; + } + */ + + /* + deviation "/vrrp:vrrp-new-master-event/vrrp:new-master-reason" { + deviate not-supported; + } + */ + + deviation "/vrrp:vrrp-protocol-error-event" { + deviate not-supported; + } + + /* + deviation "/vrrp:vrrp-protocol-error-event/vrrp:protocol-error-reason" { + deviate not-supported; + } + */ + + deviation "/vrrp:vrrp-virtual-router-error-event" { + deviate not-supported; + } +} diff --git a/holo-yang/modules/ietf/ietf-vrrp@2018-03-13.yang b/holo-yang/modules/ietf/ietf-vrrp@2018-03-13.yang new file mode 100644 index 00000000..462158c1 --- /dev/null +++ b/holo-yang/modules/ietf/ietf-vrrp@2018-03-13.yang @@ -0,0 +1,1064 @@ +module ietf-vrrp { + yang-version 1.1; + namespace "urn:ietf:params:xml:ns:yang:ietf-vrrp"; + prefix "vrrp"; + + import ietf-inet-types { + prefix "inet"; + } + + import ietf-yang-types { + prefix "yang"; + } + + import ietf-interfaces { + prefix "if"; + } + + import ietf-ip { + prefix "ip"; + } + + organization + "IETF Routing Area Working Group (RTGWG)"; + contact + "WG Web: + WG List: + + Editor: Xufeng Liu + + + Editor: Athanasios Kyparlis + + Editor: Ravi Parikh + + + Editor: Acee Lindem + + + Editor: Mingui Zhang + "; + + description + "This YANG module defines a model for managing Virtual Router + Redundancy Protocol (VRRP) versions 2 and 3. + + Copyright (c) 2018 IETF Trust and the persons identified as + authors of the code. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject to + the license terms contained in, the Simplified BSD License set + forth in Section 4.c of the IETF Trust's Legal Provisions + Relating to IETF Documents + (https://trustee.ietf.org/license-info). + + This version of this YANG module is part of RFC 8347; see the + RFC itself for full legal notices."; + + revision 2018-03-13 { + description + "Initial revision."; + reference + "RFC 8347: A YANG Data Model for the Virtual Router Redundancy + Protocol (VRRP) + RFC 2787: Definitions of Managed Objects for the Virtual + Router Redundancy Protocol + RFC 3768: Virtual Router Redundancy Protocol (VRRP) + RFC 5798: Virtual Router Redundancy Protocol (VRRP) + Version 3 for IPv4 and IPv6 + RFC 6527: Definitions of Managed Objects for the Virtual + Router Redundancy Protocol Version 3 (VRRPv3)"; + } + + /* + * Features + */ + + feature validate-interval-errors { + description + "This feature indicates that the system validates that the + advertisement interval from advertisement packets received + is the same as the interval configured for the local + VRRP router."; + } + + feature validate-address-list-errors { + description + "This feature indicates that the system validates that + the address list from received packets matches the + locally configured list for the VRRP router."; + } + + /* + * Typedefs + */ + + typedef new-master-reason-type { + type enumeration { + enum not-master { + description + "The virtual router has never transitioned to master + state."; + } + enum priority { + description + "Priority was higher."; + } + enum preempted { + description + "The master was preempted."; + } + enum no-response { + description + "Previous master did not respond."; + } + } + description + "Indicates why the virtual router has transitioned to + master state."; + } // new-master-reason-type + + /* + * Identities + */ + + /* vrrp-event-type identity and its derivatives. */ + identity vrrp-event-type { + description + "Indicates the type of a VRRP protocol event."; + } + identity vrrp-event-none { + base vrrp-event-type; + description + "Indicates a non-meaningful event."; + } + identity vrrp-event-startup { + base vrrp-event-type; + description + "Indicates that a VRRP router has initiated the protocol."; + } + identity vrrp-event-shutdown { + base vrrp-event-type; + description + "Indicates that a VRRP router has closed down the protocol."; + } + identity vrrp-event-higher-priority-backup { + base vrrp-event-type; + description + "Indicates that a backup router has a higher priority than + the current master."; + } + identity vrrp-event-master-timeout { + base vrrp-event-type; + description + "Indicates that the current master has not sent an + advertisement within the limit of master-down-interval."; + } + identity vrrp-event-interface-up { + base vrrp-event-type; + description + "Indicates that the VRRP-enabled interface has become + 'operational up'."; + } + identity vrrp-event-interface-down { + base vrrp-event-type; + description + "Indicates that the VRRP-enabled interface has become + 'operational down'."; + } + identity vrrp-event-no-primary-ip-address { + base vrrp-event-type; + description + "Indicates that the primary IP address on the VRRP-enabled + interface has become unavailable."; + } + identity vrrp-event-primary-ip-address { + base vrrp-event-type; + description + "Indicates that the primary IP address on the VRRP-enabled + interface has become available."; + } + identity vrrp-event-no-virtual-ip-addresses { + base vrrp-event-type; + description + "Indicates that there are no virtual IP addresses on the + virtual router."; + } + identity vrrp-event-virtual-ip-addresses { + base vrrp-event-type; + description + "Indicates that there are virtual IP addresses on the + virtual router."; + } + identity vrrp-event-preempt-hold-timeout { + base vrrp-event-type; + description + "Indicates that the configured preemption hold time has + passed."; + } + identity vrrp-event-lower-priority-master { + base vrrp-event-type; + description + "Indicates that there is a lower-priority VRRP master."; + } + identity vrrp-event-owner-preempt { + base vrrp-event-type; + description + "Indicates that the owner has preempted another router to + become the master."; + } + + /* vrrp-error-global identity and its derivatives. */ + identity vrrp-error-global { + description + "Indicates the type of a VRRP error that occurred + for a packet before it reaches a VRRP router."; + } + identity checksum-error { + base vrrp-error-global; + description + "A packet has been received with an invalid VRRP checksum + value."; + } + identity ip-ttl-error { + base vrrp-error-global; + description + "A packet has been received with IP TTL (Time-To-Live) + not equal to 255."; + } + identity version-error { + base vrrp-error-global; + description + "A packet has been received with an unknown or unsupported + version number."; + } + identity vrid-error { + base vrrp-error-global; + description + "A packet has been received with a Virtual Router Identifier + (VRID) that is not valid for any virtual router on this + router."; + } + + /* vrrp-error-virtual-router identity and its derivatives. */ + identity vrrp-error-virtual-router { + description + "Indicates the type of a VRRP error that occurred + after a packet reaches a VRRP router."; + } + identity address-list-error { + base vrrp-error-virtual-router; + description + "A packet has been received with an address list that + does not match the locally configured address list for + the virtual router."; + } + identity interval-error { + base vrrp-error-virtual-router; + description + "A packet has been received with an advertisement interval + different than the interval configured for the local + virtual router."; + } + identity packet-length-error { + base vrrp-error-virtual-router; + description + "A packet has been received with a packet length less + than the length of the VRRP header."; + } + + /* vrrp-state-type identity and its derivatives. */ + identity vrrp-state-type { + description + "Indicates the state of a virtual router."; + } + identity initialize { + base vrrp-state-type; + description + "Indicates that the virtual router is waiting + for a startup event."; + } + identity backup { + base vrrp-state-type; + description + "Indicates that the virtual router is monitoring the + availability of the master router."; + } + identity master { + base vrrp-state-type; + description + "Indicates that the virtual router is forwarding + packets for IP addresses that are associated with + this virtual router."; + } + + /* vrrp-version identity and its derivatives. */ + identity vrrp-version { + description + "The version of VRRP."; + } + identity vrrp-v2 { + base vrrp-version; + description + "Indicates version 2 of VRRP."; + } + identity vrrp-v3 { + base vrrp-version; + description + "Indicates version 3 of VRRP."; + } + + /* + * Groupings + */ + + grouping vrrp-common-attributes { + description + "Group of VRRP attributes common to versions 2 and 3."; + + leaf vrid { + type uint8 { + range "1..255"; + } + description + "Virtual Router ID (i.e., VRID)."; + } + + leaf version { + type identityref { + base vrrp:vrrp-version; + } + mandatory true; + description + "Version 2 or 3 of VRRP."; + } + + leaf log-state-change { + type boolean; + default "false"; + description + "Generates VRRP state change messages each time the + VRRP instance changes state (from 'up' to 'down' + or 'down' to 'up')."; + } + + container preempt { + description + "Enables a higher-priority VRRP backup router to preempt a + lower-priority VRRP master."; + leaf enabled { + type boolean; + default "true"; + description + "'true' if preemption is enabled."; + } + leaf hold-time { + type uint16; + units seconds; + default 0; + description + "Hold time, in seconds, for which a higher-priority VRRP + backup router must wait before preempting a lower-priority + VRRP master."; + } + } + + leaf priority { + type uint8 { + range "1..254"; + } + default 100; + description + "Configures the VRRP election priority for the backup + virtual router."; + } + + leaf accept-mode { + when "derived-from-or-self(current()/../version, 'vrrp-v3')" { + description + "Applicable only to version 3."; + } + type boolean; + default "false"; + description + "Controls whether a virtual router in master state will + accept packets addressed to the address owner's IPvX address + as its own if it is not the IPvX address owner. The default + is 'false'. Deployments that rely on, for example, pinging + the address owner's IPvX address may wish to configure + accept-mode to 'true'. + + Note: IPv6 Neighbor Solicitations and Neighbor + Advertisements MUST NOT be dropped when accept-mode + is 'false'."; + } + } // vrrp-common-attributes + + grouping vrrp-ipv4-attributes { + description + "Group of VRRP attributes for IPv4."; + + uses vrrp-common-attributes; + + choice advertise-interval-choice { + description + "The options for the advertisement interval at which VRRPv2 + or VRRPv3 advertisements are sent from the specified + interface."; + + case v2 { + when "derived-from-or-self(version, 'vrrp-v2')" { + description + "Applicable only to version 2."; + } + leaf advertise-interval-sec { + type uint8 { + range "1..254"; + } + units seconds; + default 1; + description + "Configures the interval that VRRPv2 advertisements + are sent from the specified interface."; + } + } + case v3 { + when "derived-from-or-self(version, 'vrrp-v3')" { + description + "Applicable only to version 3."; + } + leaf advertise-interval-centi-sec { + type uint16 { + range "1..4095"; + } + units centiseconds; + default 100; + description + "Configures the interval that VRRPv3 advertisements + are sent from the specified interface."; + } + } + } // advertise-interval-choice + + container track { + description + "Enables the specified VRRP instance to track interfaces + or networks."; + container interfaces { + description + "Enables the specified VRRPv2 or VRRPv3 instance to track + interfaces. Interface tracking prevents traffic loss by + detecting the availability of interfaces. The operational + states of other interfaces are associated with the + priority of a VRRP router. When a tracked interface + becomes unavailable (or 'operational down'), the priority + of the VRRP router decrements. When an unavailable + interface becomes available again, the priority of the + VRRP router is incremented by the same amount."; + + list interface { + key "interface"; + description + "Interface to track."; + leaf interface { + type if:interface-ref; + must "/if:interfaces/if:interface[if:name=current()]/" + + "ip:ipv4" { + description + "Interface is IPv4."; + } + description + "Interface to track."; + } + leaf priority-decrement { + type uint8 { + range "1..254"; + } + default 10; + description + "Specifies how much to decrement the priority of the + VRRP instance if the interface goes down."; + } + } // interface + } // interfaces + + container networks { + description + "Enables the VRRPv2 or VRRPv3 router instance to track the + specified networks through their IPv4 network prefixes. + Network tracking prevents traffic loss by detecting + network connectivity failure. The states of + connectivity to some networks are associated with the + priority of a VRRP router. When connectivity to a + tracked network represented by its prefix is lost, the + priority of the VRRP router decrements. When an + unavailable network is again reachable, the priority of + the VRRP router is incremented by the same amount."; + list network { + key "prefix"; + description + "Enables the specified VRRPv2 or VRRPv3 instance to + track an IPv4 network by specifying the prefix of the + IPv4 network."; + + leaf prefix { + type inet:ipv4-prefix; + description + "The IPv4 prefix of the network to track."; + } + + leaf priority-decrement { + type uint8 { + range "1..254"; + } + default 10; + description + "Specifies how much to decrement the priority of the + VRRP router if there is a failure in the IPv4 + network."; + } + } // network + } // networks + } // track + + container virtual-ipv4-addresses { + description + "Configures the virtual IPv4 address for the + VRRP interface."; + + list virtual-ipv4-address { + key "ipv4-address"; + max-elements 16; + description + "Virtual IPv4 addresses for a single VRRP instance. For a + VRRP owner router, the virtual address must match one + of the IPv4 addresses configured on the interface + corresponding to the virtual router."; + + leaf ipv4-address { + type inet:ipv4-address; + description + "An IPv4 address associated with a virtual router."; + reference + "RFC 5798: Virtual Router Redundancy Protocol (VRRP) + Version 3 for IPv4 and IPv6. Section 1.2"; + } + } // virtual-ipv4-address + } // virtual-ipv4-addresses + } // vrrp-ipv4-attributes + + grouping vrrp-ipv6-attributes { + description + "Group of VRRP attributes for IPv6."; + + uses vrrp-common-attributes; + + leaf advertise-interval-centi-sec { + type uint16 { + range "1..4095"; + } + units centiseconds; + default 100; + description + "Configures the interval that VRRPv3 advertisements + are sent from the specified interface."; + } + + container track { + description + "Enables the specified VRRP instance to track interfaces + or networks."; + container interfaces { + description + "Enables the specified VRRPv2 or VRRPv3 instance to track + interfaces. Interface tracking prevents traffic loss by + detecting the availability of interfaces. The operational + states of other interfaces are associated with the + priority of a VRRP router. When a tracked interface + becomes unavailable (or 'operational down'), the priority + of the VRRP router decrements. When an unavailable + interface becomes available again, the priority of the + VRRP router is incremented by the same amount."; + list interface { + key "interface"; + description + "Interface to track."; + + leaf interface { + type if:interface-ref; + must "/if:interfaces/if:interface[if:name=current()]/" + + "ip:ipv6" { + description + "Interface is IPv6."; + } + description + "Interface to track."; + } + + leaf priority-decrement { + type uint8 { + range "1..254"; + } + default 10; + description + "Specifies how much to decrement the priority of the + VRRP instance if the interface goes down."; + } + } // interface + } // interfaces + + container networks { + description + "Enables the VRRPv2 or VRRPv3 router instance to track the + specified networks through their IPv6 network prefixes. + Network tracking prevents traffic loss by detecting + network connectivity failure. The states of + connectivity to some networks are associated with the + priority of a VRRP router. When connectivity to a + tracked network represented by its prefix is lost, the + priority of the VRRP router decrements. When an + unavailable network is again reachable, the priority of + the VRRP router is incremented by the same amount."; + list network { + key "prefix"; + description + "Enables the specified VRRPv2 or VRRPv3 instance to + track an IPv6 network by specifying the prefix of the + IPv6 network."; + + leaf prefix { + type inet:ipv6-prefix; + description + "The IPv6 prefix of the network to track."; + } + + leaf priority-decrement { + type uint8 { + range "1..254"; + } + default 10; + description + "Specifies how much to decrement the priority of the + VRRP router if there is a failure in the IPv6 + network."; + } + } // network + } // networks + } // track + + container virtual-ipv6-addresses { + description + "Configures the virtual IPv6 address for the + VRRP interface."; + list virtual-ipv6-address { + key "ipv6-address"; + max-elements 2; + description + "Two IPv6 addresses are allowed. The first address must + be a link-local address. The second address can be a + link-local or global address."; + + leaf ipv6-address { + type inet:ipv6-address; + description + "An IPv6 address associated with a virtual router."; + reference + "RFC 5798: Virtual Router Redundancy Protocol (VRRP) + Version 3 for IPv4 and IPv6. Section 1.3"; + } + } // virtual-ipv6-address + } // virtual-ipv6-addresses + } // vrrp-ipv6-attributes + + grouping vrrp-state-attributes { + description + "Group of VRRP state attributes."; + + leaf state { + type identityref { + base vrrp:vrrp-state-type; + } + config false; + description + "Operational state."; + } + + leaf is-owner { + type boolean; + config false; + description + "Set to 'true' if this virtual router is the owner."; + } + + leaf last-adv-source { + type inet:ip-address; + config false; + description + "Last advertised IPv4/IPv6 source address."; + } + + leaf up-datetime { + type yang:date-and-time; + config false; + description + "The date and time when this virtual router + transitioned out of 'init' state."; + } + + leaf master-down-interval { + type uint32; + units centiseconds; + config false; + description + "Time interval for the backup virtual router to declare + 'master down'."; + } + + leaf skew-time { + type uint32; + units microseconds; + config false; + description + "Calculated based on the priority and advertisement + interval configuration command parameters. See RFC 3768."; + } + + leaf last-event { + type identityref { + base vrrp:vrrp-event-type; + } + config false; + description + "Last reported event."; + } + + leaf new-master-reason { + type new-master-reason-type; + config false; + description + "Indicates why the virtual router has transitioned to + master state."; + } + + container statistics { + config false; + description + "VRRP statistics."; + + leaf discontinuity-datetime { + type yang:date-and-time; + description + "The time on the most recent occasion at which any one or + more of the VRRP statistics counters suffered a + discontinuity. If no such discontinuities have occurred + since the last re-initialization of the local management + subsystem, then this node contains the time that the + local management subsystem re-initialized itself."; + } + + leaf master-transitions { + type yang:counter32; + description + "The total number of times that this virtual router's + state has transitioned to 'master'."; + } + + leaf advertisement-rcvd { + type yang:counter64; + description + "The total number of VRRP advertisements received by + this virtual router."; + } + + leaf advertisement-sent { + type yang:counter64; + description + "The total number of VRRP advertisements sent by + this virtual router."; + } + + leaf interval-errors { + if-feature validate-interval-errors; + type yang:counter64; + description + "The total number of VRRP advertisement packets received + with an advertisement interval different than the + interval configured for the local virtual router."; + } + + leaf priority-zero-pkts-rcvd { + type yang:counter64; + description + "The total number of VRRP packets received by the + virtual router with a priority of 0."; + } + + leaf priority-zero-pkts-sent { + type yang:counter64; + description + "The total number of VRRP packets sent by the + virtual router with a priority of 0."; + } + + leaf invalid-type-pkts-rcvd { + type yang:counter64; + description + "The number of VRRP packets received by the virtual + router with an invalid value in the 'type' field."; + } + leaf address-list-errors { + if-feature validate-address-list-errors; + type yang:counter64; + description + "The total number of packets received with an + address list that does not match the locally + configured address list for the virtual router."; + } + + leaf packet-length-errors { + type yang:counter64; + description + "The total number of packets received with a packet + length less than the length of the VRRP header."; + } + } // statistics + } // vrrp-state-attributes + + grouping vrrp-global-state-attributes { + description + "Group of VRRP global state attributes."; + + leaf virtual-routers { + type uint32; + description + "Number of configured virtual routers."; + } + + leaf interfaces { + type uint32; + description + "Number of interfaces with VRRP configured."; + } + + container statistics { + description + "VRRP global statistics."; + + leaf discontinuity-datetime { + type yang:date-and-time; + description + "The time on the most recent occasion at which any + one or more of checksum-errors, version-errors, + vrid-errors, or ip-ttl-errors suffered a + discontinuity. + + If no such discontinuities have occurred since the last + re-initialization of the local management subsystem, + then this node contains the time that the local management + subsystem re-initialized itself."; + } + + leaf checksum-errors { + type yang:counter64; + description + "The total number of VRRP packets received with an invalid + VRRP checksum value."; + reference + "RFC 5798: Virtual Router Redundancy Protocol (VRRP) + Version 3 for IPv4 and IPv6. Section 5.2.8"; + } + + leaf version-errors { + type yang:counter64; + description + "The total number of VRRP packets received with an unknown + or unsupported version number."; + reference + "RFC 5798: Virtual Router Redundancy Protocol (VRRP) + Version 3 for IPv4 and IPv6. Section 5.2.1"; + } + + leaf vrid-errors { + type yang:counter64; + description + "The total number of VRRP packets received with a VRID that + is not valid for any virtual router on this router."; + reference + "RFC 5798: Virtual Router Redundancy Protocol (VRRP) + Version 3 for IPv4 and IPv6. Section 5.2.3"; + } + + leaf ip-ttl-errors { + type yang:counter64; + description + "The total number of VRRP packets received by the + virtual router with IP TTL (IPv4) or Hop Limit (IPv6) + not equal to 255."; + reference + "RFC 5798: Virtual Router Redundancy Protocol (VRRP) + Version 3 for IPv4 and IPv6. + Sections 5.1.1.3 and 5.1.2.3"; + } + } // statistics + } // vrrp-global-state-attributes + + /* + * Configuration data and operational state data nodes + */ + + augment "/if:interfaces/if:interface/ip:ipv4" { + description + "Augments IPv4 interface."; + + container vrrp { + description + "Configures VRRP version 2 or 3 for IPv4."; + + list vrrp-instance { + key "vrid"; + description + "Defines a virtual router, identified by a VRID, within the + IPv4 address space."; + + uses vrrp-ipv4-attributes; + uses vrrp-state-attributes; + } + } + } // augments ipv4 + + augment "/if:interfaces/if:interface/ip:ipv6" { + description + "Augments IPv6 interface."; + + container vrrp { + description + "Configures VRRP version 3 for IPv6."; + + list vrrp-instance { + must "derived-from-or-self(version, 'vrrp-v3')" { + description + "IPv6 is only supported by version 3."; + } + key "vrid"; + description + "Defines a virtual router, identified by a VRID, within the + IPv6 address space."; + + uses vrrp-ipv6-attributes; + uses vrrp-state-attributes; + } + } + } // augments ipv6 + + container vrrp { + config false; + description + "VRRP data at the global level."; + + uses vrrp-global-state-attributes; + } + + /* + * Notifications + */ + + notification vrrp-new-master-event { + description + "Notification event for the election of a new VRRP master."; + leaf master-ip-address { + type inet:ip-address; + mandatory true; + description + "IPv4 or IPv6 address of the new master."; + } + leaf new-master-reason { + type new-master-reason-type; + mandatory true; + description + "Indicates why the virtual router has transitioned to + master state."; + } + } + + notification vrrp-protocol-error-event { + description + "Notification event for a VRRP protocol error."; + leaf protocol-error-reason { + type identityref { + base vrrp:vrrp-error-global; + } + mandatory true; + description + "Indicates the reason for the protocol error."; + } + } + + notification vrrp-virtual-router-error-event { + description + "Notification event for an error that happened on a + virtual router."; + leaf interface { + type if:interface-ref; + mandatory true; + description + "Indicates the interface on which the event has occurred."; + } + + choice ip-version { + mandatory true; + description + "The error may have happened on either an IPv4 virtual + router or an IPv6 virtual router. The information + related to a specific IP version is provided by one of + the following cases."; + case ipv4 { + description + "IPv4."; + container ipv4 { + description + "Error information for IPv4."; + leaf vrid { + type leafref { + path "/if:interfaces/if:interface" + + "[if:name = current()/../../vrrp:interface]/" + + "ip:ipv4/vrrp:vrrp/vrrp:vrrp-instance/vrrp:vrid"; + } + mandatory true; + description + "Indicates the virtual router on which the event has + occurred."; + } + } + } + case ipv6 { + description + "IPv6."; + container ipv6 { + description + "Error information for IPv6."; + leaf vrid { + type leafref { + path "/if:interfaces/if:interface" + + "[if:name = current()/../../vrrp:interface]/" + + "ip:ipv6/vrrp:vrrp/vrrp:vrrp-instance/vrrp:vrid"; + } + mandatory true; + description + "Indicates the virtual router on which the event has + occurred."; + } + } + } + } + + leaf virtual-router-error-reason { + type identityref { + base vrrp:vrrp-error-virtual-router; + } + mandatory true; + description + "Indicates the reason for the virtual router error."; + } + } +} diff --git a/holo-yang/src/lib.rs b/holo-yang/src/lib.rs index a773e27d..ade5dd66 100644 --- a/holo-yang/src/lib.rs +++ b/holo-yang/src/lib.rs @@ -125,6 +125,8 @@ pub static YANG_EMBEDDED_MODULES: Lazy = Lazy::new(|| { include_str!("../modules/ietf/ietf-tcp@2022-09-11.yang"), EmbeddedModuleKey::new("ietf-tcp-common", Some("2023-04-17"), None, None) => include_str!("../modules/ietf/ietf-tcp-common@2023-04-17.yang"), + EmbeddedModuleKey::new("ietf-vrrp", Some("2018-03-13"), None, None) => + include_str!("../modules/ietf/ietf-vrrp@2018-03-13.yang"), // IETF Holo augmentations EmbeddedModuleKey::new("holo-bgp", None, None, None) => include_str!("../modules/augmentations/holo-bgp.yang"), @@ -136,6 +138,8 @@ pub static YANG_EMBEDDED_MODULES: Lazy = Lazy::new(|| { include_str!("../modules/augmentations/holo-ospf-dev.yang"), EmbeddedModuleKey::new("holo-routing", None, None, None) => include_str!("../modules/augmentations/holo-routing.yang"), + EmbeddedModuleKey::new("holo-vrrp", None, None, None) => + include_str!("../modules/augmentations/holo-vrrp.yang"), // IETF Holo deviations EmbeddedModuleKey::new("holo-ietf-bgp-deviations", None, None, None) => include_str!("../modules/deviations/holo-ietf-bgp-deviations.yang"), @@ -175,6 +179,8 @@ pub static YANG_EMBEDDED_MODULES: Lazy = Lazy::new(|| { include_str!("../modules/deviations/holo-ietf-routing-policy-deviations.yang"), EmbeddedModuleKey::new("holo-ietf-segment-routing-mpls-deviations", None, None, None) => include_str!("../modules/deviations/holo-ietf-segment-routing-mpls-deviations.yang"), + EmbeddedModuleKey::new("holo-ietf-vrrp-deviations", None, None, None) => + include_str!("../modules/deviations/holo-ietf-vrrp-deviations.yang"), } }); @@ -221,12 +227,14 @@ pub static YANG_IMPLEMENTED_MODULES: Lazy> = "ietf-rip", "ietf-system", "ietf-tcp", + "ietf-vrrp", // IETF Holo augmentations "holo-bgp", "holo-isis", "holo-ospf", "holo-ospf-dev", "holo-routing", + "holo-vrrp", ] }); @@ -277,6 +285,9 @@ pub static YANG_FEATURES: Lazy>> = "ietf-segment-routing-common" => vec![ "sid-last-hop-behavior", ], + "ietf-vrrp" => vec![ + "validate-interval-errors", + ], } });