From 46e3db045b9042e7044c2259ba24e74bdce5f05b Mon Sep 17 00:00:00 2001
From: Renato Westphal <renato@opensourcerouting.org>
Date: Mon, 17 Jun 2024 22:52:43 -0300
Subject: [PATCH] VRRP WIP

Signed-off-by: Renato Westphal <renato@opensourcerouting.org>
---
 Cargo.toml                                    |    1 +
 README.md                                     |    5 +
 holo-daemon/Cargo.toml                        |    3 +
 holo-daemon/src/northbound/yang.rs            |    5 +
 holo-interface/Cargo.toml                     |    6 +
 holo-interface/src/interface.rs               |    4 +
 .../src/northbound/configuration.rs           |   43 +-
 holo-interface/src/northbound/mod.rs          |   16 +
 holo-interface/src/northbound/state.rs        |   45 +-
 holo-tools/yang-coverage.sh                   |    3 +-
 holo-utils/src/protocol.rs                    |    5 +
 holo-vrrp/Cargo.toml                          |   43 +
 holo-vrrp/LICENSE                             |   19 +
 holo-vrrp/src/debug.rs                        |   65 +
 holo-vrrp/src/error.rs                        |  136 +++
 holo-vrrp/src/events.rs                       |   23 +
 holo-vrrp/src/instance.rs                     |  128 ++
 holo-vrrp/src/interface.rs                    |  229 ++++
 holo-vrrp/src/lib.rs                          |   22 +
 holo-vrrp/src/network.rs                      |   90 ++
 holo-vrrp/src/northbound/configuration.rs     |  190 +++
 holo-vrrp/src/northbound/mod.rs               |   35 +
 holo-vrrp/src/northbound/notification.rs      |   29 +
 holo-vrrp/src/northbound/state.rs             |   92 ++
 holo-vrrp/src/northbound/yang.rs              |   73 ++
 holo-vrrp/src/packet.rs                       |   75 ++
 holo-vrrp/src/southbound.rs                   |   50 +
 holo-vrrp/src/tasks.rs                        |  159 +++
 holo-vrrp/tests/conformance/mod.rs            |    7 +
 holo-vrrp/tests/mod.rs                        |    8 +
 holo-vrrp/tests/packet/mod.rs                 |    7 +
 .../modules/augmentations/holo-vrrp.yang      |   26 +
 .../deviations/ietf-vrrp-holo-deviations.yang |  611 ++++++++++
 .../modules/ietf/ietf-vrrp@2018-03-13.yang    | 1064 +++++++++++++++++
 holo-yang/src/lib.rs                          |   11 +
 35 files changed, 3319 insertions(+), 9 deletions(-)
 create mode 100644 holo-vrrp/Cargo.toml
 create mode 100644 holo-vrrp/LICENSE
 create mode 100644 holo-vrrp/src/debug.rs
 create mode 100644 holo-vrrp/src/error.rs
 create mode 100644 holo-vrrp/src/events.rs
 create mode 100644 holo-vrrp/src/instance.rs
 create mode 100644 holo-vrrp/src/interface.rs
 create mode 100644 holo-vrrp/src/lib.rs
 create mode 100644 holo-vrrp/src/network.rs
 create mode 100644 holo-vrrp/src/northbound/configuration.rs
 create mode 100644 holo-vrrp/src/northbound/mod.rs
 create mode 100644 holo-vrrp/src/northbound/notification.rs
 create mode 100644 holo-vrrp/src/northbound/state.rs
 create mode 100644 holo-vrrp/src/northbound/yang.rs
 create mode 100644 holo-vrrp/src/packet.rs
 create mode 100644 holo-vrrp/src/southbound.rs
 create mode 100644 holo-vrrp/src/tasks.rs
 create mode 100644 holo-vrrp/tests/conformance/mod.rs
 create mode 100644 holo-vrrp/tests/mod.rs
 create mode 100644 holo-vrrp/tests/packet/mod.rs
 create mode 100644 holo-yang/modules/augmentations/holo-vrrp.yang
 create mode 100644 holo-yang/modules/deviations/ietf-vrrp-holo-deviations.yang
 create mode 100644 holo-yang/modules/ietf/ietf-vrrp@2018-03-13.yang

diff --git a/Cargo.toml b/Cargo.toml
index e500401f..35ebdb4d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,6 +14,7 @@ members = [
   "holo-routing",
   "holo-tools",
   "holo-utils",
+  "holo-vrrp",
   "holo-yang",
 ]
 default-members = ["holo-daemon"]
diff --git a/README.md b/README.md
index bbc14874..3c54be23 100644
--- a/README.md
+++ b/README.md
@@ -208,6 +208,10 @@ Holo supports the following IETF RFCs and Internet drafts:
 * 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 |
@@ -234,6 +238,7 @@ Holo supports the following IETF RFCs and Internet drafts:
 | ietf-routing@2018-03-13 | 100.00% | 85.71% | - | - | [92.31%](http://westphal.com.br/holo/ietf-routing.html) |
 | 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-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 3990ec98..5e54c346 100644
--- a/holo-daemon/Cargo.toml
+++ b/holo-daemon/Cargo.toml
@@ -42,6 +42,7 @@ holo-protocol = { path = "../holo-protocol" }
 holo-rip = { path = "../holo-rip", optional = true }
 holo-routing = { path = "../holo-routing", optional = true  }
 holo-utils = { path = "../holo-utils" }
+holo-vrrp = { path = "../holo-vrrp", optional = true  }
 holo-yang = { path = "../holo-yang" }
 
 [build-dependencies]
@@ -67,6 +68,7 @@ default = [
   "ldp",
   "ospf",
   "rip",
+  "vrrp",
 ]
 
 # Base components
@@ -81,6 +83,7 @@ bgp = ["holo-bgp", "holo-routing/bgp"]
 ldp = ["holo-ldp", "holo-routing/ldp"]
 ospf = ["holo-ospf", "holo-routing/ospf"]
 rip = ["holo-rip", "holo-routing/rip"]
+vrrp = ["holo-vrrp", "holo-interface/vrrp"]
 
 # Other features
 io_uring = ["tokio-uring"]
diff --git a/holo-daemon/src/northbound/yang.rs b/holo-daemon/src/northbound/yang.rs
index d16a691f..42428cba 100644
--- a/holo-daemon/src/northbound/yang.rs
+++ b/holo-daemon/src/northbound/yang.rs
@@ -68,6 +68,11 @@ pub(crate) fn create_context() {
         modules_add::<Instance<Ripv2>>(&mut modules);
         modules_add::<Instance<Ripng>>(&mut modules);
     }
+    #[cfg(feature = "vrrp")]
+    {
+        use holo_vrrp::interface::Interface;
+        modules_add::<Interface>(&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 46509c10..cd98905f 100644
--- a/holo-interface/Cargo.toml
+++ b/holo-interface/Cargo.toml
@@ -17,6 +17,7 @@ ipnetwork.workspace = true
 netlink-packet-route.workspace = true
 netlink-packet-core.workspace = true
 netlink-sys.workspace = true
+regex.workspace = true
 rtnetlink.workspace = true
 tokio.workspace = true
 tracing.workspace = true
@@ -26,5 +27,10 @@ holo-northbound = { path = "../holo-northbound" }
 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/interface.rs b/holo-interface/src/interface.rs
index cdb7070c..f0924907 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};
@@ -38,6 +39,7 @@ pub struct Interface {
     pub flags: InterfaceFlags,
     pub addresses: BTreeMap<IpNetwork, InterfaceAddress>,
     pub owner: Owner,
+    pub vrrp: Option<NbDaemonSender>,
 }
 
 #[derive(Debug)]
@@ -123,6 +125,7 @@ impl Interfaces {
             flags: InterfaceFlags::default(),
             addresses: Default::default(),
             owner: Owner::CONFIG,
+            vrrp: None,
         };
 
         let iface_idx = self.arena.insert(iface);
@@ -214,6 +217,7 @@ impl Interfaces {
                     flags,
                     addresses: Default::default(),
                     owner: Owner::SYSTEM,
+                    vrrp: None,
                 };
 
                 // Notify protocol instances about the interface update.
diff --git a/holo-interface/src/northbound/configuration.rs b/holo-interface/src/northbound/configuration.rs
index f1ad3388..f0dc9fe6 100644
--- a/holo-interface/src/northbound/configuration.rs
+++ b/holo-interface/src/northbound/configuration.rs
@@ -4,21 +4,23 @@
 // 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_utils::yang::DataNodeRefExt;
 use ipnetwork::IpNetwork;
 
 use crate::interface::Owner;
+use crate::northbound::REGEX_VRRP;
 use crate::{netlink, Master};
 
 static VALIDATION_CALLBACKS: Lazy<ValidationCallbacks> =
@@ -298,6 +300,41 @@ impl Provider for Master {
         Some(&CALLBACKS)
     }
 
+    fn nested_callbacks() -> Option<Vec<CallbackKey>> {
+        let keys: Vec<Vec<CallbackKey>> = 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<String, ConfigChanges> = 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::<Vec<_>>()
+    }
+
     async fn process_event(&mut self, event: Event) {
         match event {
             Event::InterfaceDelete(ifname) => {
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<String> = Lazy::new(|| {
+    format!(
+        r"{}\[name='(.+?)'\]/ietf-ip:ipv4/ietf-vrrp:vrrp/*",
+        interfaces::interface::PATH
+    )
+});
+pub static REGEX_VRRP: Lazy<Regex> =
+    Lazy::new(|| Regex::new(&REGEX_VRRP_STR).unwrap());
diff --git a/holo-interface/src/northbound/state.rs b/holo-interface/src/northbound/state.rs
index 1b9086cd..91e594fb 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<Callbacks<Master>> = 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<Master> {
-    CallbacksBuilder::default().build()
+    CallbacksBuilder::<Master>::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<Master> {
 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<Master>> {
         Some(&CALLBACKS)
     }
+
+    fn nested_callbacks() -> Option<Vec<CallbackKey>> {
+        let keys: Vec<Vec<CallbackKey>> = vec![
+            #[cfg(feature = "vrrp")]
+            holo_vrrp::northbound::state::CALLBACKS.keys(),
+        ];
+
+        Some(keys.concat())
+    }
 }
 
 // ===== impl ListEntry =====
 
-impl ListEntryKind for ListEntry {}
+impl<'a> ListEntryKind for ListEntry<'a> {
+    fn child_task(&self) -> Option<NbDaemonSender> {
+        match self {
+            ListEntry::Interface(iface) => iface.vrrp.clone(),
+            _ => None,
+        }
+    }
+}
diff --git a/holo-tools/yang-coverage.sh b/holo-tools/yang-coverage.sh
index 446bd24b..8cd987f2 100755
--- a/holo-tools/yang-coverage.sh
+++ b/holo-tools/yang-coverage.sh
@@ -22,4 +22,5 @@ cargo run --bin yang_coverage --\
   -m ietf-ospf\
   -m ietf-ospf-sr-mpls\
   -m ietf-ospfv3-extended-lsa\
-  -m ietf-rip
+  -m ietf-rip\
+  -m ietf-vrrp
diff --git a/holo-utils/src/protocol.rs b/holo-utils/src/protocol.rs
index 15d48073..62b53679 100644
--- a/holo-utils/src/protocol.rs
+++ b/holo-utils/src/protocol.rs
@@ -25,6 +25,7 @@ pub enum Protocol {
     RIPV2,
     RIPNG,
     STATIC,
+    VRRP,
 }
 
 // ===== impl Protocol =====
@@ -41,6 +42,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"),
         }
     }
 }
@@ -59,6 +61,7 @@ impl FromStr for Protocol {
             "ripv2" => Ok(Protocol::RIPV2),
             "ripng" => Ok(Protocol::RIPNG),
             "static" => Ok(Protocol::STATIC),
+            "vrrp" => Ok(Protocol::VRRP),
             _ => Err(()),
         }
     }
@@ -76,6 +79,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(),
         }
     }
 }
@@ -92,6 +96,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-vrrp/Cargo.toml b/holo-vrrp/Cargo.toml
new file mode 100644
index 00000000..cd9cacfc
--- /dev/null
+++ b/holo-vrrp/Cargo.toml
@@ -0,0 +1,43 @@
+[package]
+name = "holo-vrrp"
+version.workspace = true
+authors.workspace = true # TODO
+license.workspace = true
+edition.workspace = true
+
+[dependencies]
+async-trait.workspace = true
+bitflags.workspace = true
+bytes.workspace = true
+chrono.workspace = true
+derive-new.workspace = true
+enum-as-inner.workspace = true
+ipnetwork.workspace = true
+itertools.workspace = true
+libc.workspace = true
+num-derive.workspace = true
+num-traits.workspace = true
+rand.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+serde_with.workspace = true
+tokio.workspace = true
+tracing.workspace = true
+yang2.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"] }
+
+[lints]
+workspace = true
+
+[features]
+default = []
+testing = []
diff --git a/holo-vrrp/LICENSE b/holo-vrrp/LICENSE
new file mode 100644
index 00000000..4481fc10
--- /dev/null
+++ b/holo-vrrp/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2023 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/debug.rs b/holo-vrrp/src/debug.rs
new file mode 100644
index 00000000..3cfb8bce
--- /dev/null
+++ b/holo-vrrp/src/debug.rs
@@ -0,0 +1,65 @@
+//
+// Copyright (c) The Holo Core Contributors
+//
+// SPDX-License-Identifier: MIT
+//
+
+use std::net::IpAddr;
+
+use tracing::{debug, debug_span};
+
+use crate::packet::Packet;
+
+// VRRP debug messages.
+#[derive(Debug)]
+pub enum Debug<'a> {
+    InstanceCreate,
+    InstanceDelete,
+    // Network
+    PacketRx(&'a IpAddr, &'a Packet),
+    PacketTx(&'a IpAddr, &'a Packet),
+}
+
+// ===== impl Debug =====
+
+impl<'a> Debug<'a> {
+    // Log debug message using the tracing API.
+    pub(crate) fn log(&self) {
+        match self {
+            Debug::InstanceCreate | Debug::InstanceDelete => {
+                // Parent span(s): vrrp-instance
+                debug!("{}", self);
+            }
+            Debug::PacketRx(src, packet) => {
+                // Parent span(s): vrrp-instance
+                debug_span!("network").in_scope(|| {
+                    debug_span!("input").in_scope(|| {
+                        let data = serde_json::to_string(&packet).unwrap();
+                        debug!(%src, %data, "{}", self);
+                    })
+                })
+            }
+            Debug::PacketTx(addr, packet) => {
+                // Parent span(s): vrrp-instance:network:output
+                let data = serde_json::to_string(&packet).unwrap();
+                debug!(%addr, %data, "{}", self);
+            }
+        }
+    }
+}
+
+impl<'a> std::fmt::Display for Debug<'a> {
+    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::PacketRx(..) | Debug::PacketTx(..) => {
+                write!(f, "packet")
+            }
+        }
+    }
+}
diff --git a/holo-vrrp/src/error.rs b/holo-vrrp/src/error.rs
new file mode 100644
index 00000000..ed08b6b6
--- /dev/null
+++ b/holo-vrrp/src/error.rs
@@ -0,0 +1,136 @@
+//
+// Copyright (c) The Holo Core Contributors
+//
+// SPDX-License-Identifier: MIT
+//
+
+use std::net::IpAddr;
+
+use tracing::warn;
+
+// BGP errors.
+#[derive(Debug)]
+pub enum Error {
+    // I/O errors
+    IoError(IoError),
+    // TODO other errors
+}
+
+// BGP 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),
+}
+
+// ===== impl Error =====
+
+impl Error {
+    pub(crate) fn log(&self) {
+        match self {
+            Error::IoError(error) => {
+                error.log();
+            }
+        }
+    }
+}
+
+impl std::fmt::Display for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Error::IoError(error) => error.fmt(f),
+        }
+    }
+}
+
+impl std::error::Error for Error {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        match self {
+            Error::IoError(error) => Some(error),
+            _ => None,
+        }
+    }
+}
+
+impl From<IoError> for Error {
+    fn from(error: IoError) -> Error {
+        Error::IoError(error)
+    }
+}
+
+// ===== 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,
+        }
+    }
+}
+
+// ===== global functions =====
+
+fn with_source<E: std::error::Error>(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..3fa599f6
--- /dev/null
+++ b/holo-vrrp/src/events.rs
@@ -0,0 +1,23 @@
+//
+// Copyright (c) The Holo Core Contributors
+//
+// SPDX-License-Identifier: MIT
+//
+
+use std::net::IpAddr;
+
+use crate::error::Error;
+use crate::interface::Interface;
+use crate::packet::{DecodeResult, Packet};
+
+// ===== Network packet receipt =====
+
+pub(crate) fn process_packet(
+    _interface: &mut Interface,
+    _src: IpAddr,
+    _packet: DecodeResult<Packet>,
+) -> Result<(), Error> {
+    // TODO
+
+    Ok(())
+}
diff --git a/holo-vrrp/src/instance.rs b/holo-vrrp/src/instance.rs
new file mode 100644
index 00000000..24f6c52f
--- /dev/null
+++ b/holo-vrrp/src/instance.rs
@@ -0,0 +1,128 @@
+//
+// Copyright (c) The Holo Core Contributors
+//
+// SPDX-License-Identifier: MIT
+//
+
+use std::net::Ipv4Addr;
+
+use chrono::{DateTime, Utc};
+
+use crate::northbound::configuration::InstanceCfg;
+
+#[derive(Debug)]
+pub struct Instance {
+    // Instance configuration data.
+    pub config: InstanceCfg,
+    // Instance state data.
+    pub state: InstanceState,
+}
+
+#[derive(Debug)]
+pub struct InstanceState {
+    pub state: State,
+    pub last_adv_src: Option<Ipv4Addr>,
+    pub up_time: Option<DateTime<Utc>>,
+    pub last_event: Event,
+    pub new_master_reason: MasterReason,
+    // TODO: interval/timer tasks
+    pub statistics: Statistics,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum State {
+    Initialize,
+    Backup,
+    Master,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum Event {
+    None,
+    Startup,
+    Shutdown,
+    HigherPriorityBackup,
+    MasterTimeout,
+    InterfaceUp,
+    InterfaceDown,
+    NoPrimaryIpAddress,
+    PrimaryIpAddress,
+    NoVirtualIpAddresses,
+    VirtualIpAddresses,
+    PreemptHoldTimeout,
+    LowerPriorityMaster,
+    OwnerPreempt,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum MasterReason {
+    NotMaster,
+    Priority,
+    Preempted,
+    NoResponse,
+}
+
+#[derive(Debug)]
+pub struct Statistics {
+    pub discontinuity_time: DateTime<Utc>,
+    pub master_transitions: u32,
+    pub adv_rcvd: u64,
+    pub adv_sent: u64,
+    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,
+    pub checksum_errors: u64,
+    pub version_errors: u64,
+    pub vrid_errors: u64,
+    pub ip_ttl_errors: u64,
+}
+
+// ===== impl Instance =====
+
+impl Instance {
+    pub(crate) fn new() -> Self {
+        Instance {
+            config: Default::default(),
+            state: InstanceState::new(),
+        }
+    }
+}
+
+// ===== impl InstanceState =====
+
+impl InstanceState {
+    pub(crate) fn new() -> Self {
+        InstanceState {
+            state: State::Initialize,
+            last_adv_src: None,
+            up_time: None,
+            last_event: Event::None,
+            new_master_reason: MasterReason::NotMaster,
+            statistics: Default::default(),
+        }
+    }
+}
+
+// ===== impl Statistics =====
+
+impl Default for Statistics {
+    fn default() -> Self {
+        Statistics {
+            discontinuity_time: Utc::now(),
+            master_transitions: 0,
+            adv_rcvd: 0,
+            adv_sent: 0,
+            interval_errors: 0,
+            priority_zero_pkts_rcvd: 0,
+            priority_zero_pkts_sent: 0,
+            invalid_type_pkts_rcvd: 0,
+            pkt_length_errors: 0,
+            checksum_errors: 0,
+            version_errors: 0,
+            vrid_errors: 0,
+            ip_ttl_errors: 0,
+        }
+    }
+}
diff --git a/holo-vrrp/src/interface.rs b/holo-vrrp/src/interface.rs
new file mode 100644
index 00000000..65d9fc0a
--- /dev/null
+++ b/holo-vrrp/src/interface.rs
@@ -0,0 +1,229 @@
+//
+// Copyright (c) The Holo Core Contributors
+//
+// SPDX-License-Identifier: MIT
+//
+
+use std::collections::{BTreeMap, BTreeSet};
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use holo_protocol::{
+    InstanceChannelsTx, InstanceShared, MessageReceiver, ProtocolInstance,
+};
+use holo_utils::ibus::IbusMsg;
+use holo_utils::protocol::Protocol;
+use holo_utils::socket::{AsyncFd, Socket};
+use holo_utils::southbound::InterfaceFlags;
+use holo_utils::task::Task;
+use holo_utils::{Receiver, Sender, UnboundedSender};
+use ipnetwork::Ipv4Network;
+use tokio::sync::mpsc;
+
+use crate::error::{Error, IoError};
+use crate::instance::Instance;
+use crate::tasks::messages::input::NetRxPacketMsg;
+use crate::tasks::messages::output::NetTxPacketMsg;
+use crate::tasks::messages::{ProtocolInputMsg, ProtocolOutputMsg};
+use crate::{events, network, southbound, tasks};
+
+#[derive(Debug)]
+pub struct Interface {
+    // Interface name.
+    pub name: String,
+    // Interface system data.
+    pub system: InterfaceSys,
+    // Raw sockets and Tx/Rx tasks.
+    pub net: InterfaceNet,
+    // Interface VRRP instances.
+    pub instances: BTreeMap<u8, Instance>,
+    // Tx channels.
+    pub tx: InstanceChannelsTx<Interface>,
+    // Shared data.
+    pub shared: InstanceShared,
+}
+
+#[derive(Debug, Default)]
+pub struct InterfaceSys {
+    // Interface flags.
+    pub flags: InterfaceFlags,
+    // Interface index.
+    pub ifindex: Option<u32>,
+    // Interface IPv4 addresses.
+    pub addresses: BTreeSet<Ipv4Network>,
+}
+
+#[derive(Debug)]
+pub struct InterfaceNet {
+    // Raw sockets.
+    pub socket_vrrp: Arc<AsyncFd<Socket>>,
+    pub socket_arp: Arc<AsyncFd<Socket>>,
+    // Network Tx/Rx tasks.
+    _net_tx_task: Task<()>,
+    _net_rx_task: Task<()>,
+    // Network Tx output channel.
+    pub net_tx_packetp: UnboundedSender<NetTxPacketMsg>,
+}
+
+#[derive(Clone, Debug)]
+pub struct ProtocolInputChannelsTx {
+    // Packet Rx event.
+    pub net_packet_rx: Sender<NetRxPacketMsg>,
+}
+
+#[derive(Debug)]
+pub struct ProtocolInputChannelsRx {
+    // Packet Rx event.
+    pub net_packet_rx: Receiver<NetRxPacketMsg>,
+}
+
+// ===== impl Interface =====
+
+#[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 {
+        // TODO: proper error handling
+        let net = InterfaceNet::new(&name, &tx)
+            .expect("Failed to initialize VRRP network tasks");
+        Interface {
+            name,
+            system: Default::default(),
+            net,
+            instances: Default::default(),
+            tx,
+            shared,
+        }
+    }
+
+    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::NetRxPacket(msg) => {
+                events::process_packet(self, msg.src, msg.packet)
+            }
+        } {
+            error.log();
+        }
+    }
+
+    fn protocol_input_channels(
+    ) -> (ProtocolInputChannelsTx, ProtocolInputChannelsRx) {
+        let (net_packet_rxp, net_packet_rxc) = mpsc::channel(4);
+
+        let tx = ProtocolInputChannelsTx {
+            net_packet_rx: net_packet_rxp,
+        };
+        let rx = ProtocolInputChannelsRx {
+            net_packet_rx: net_packet_rxc,
+        };
+
+        (tx, rx)
+    }
+
+    #[cfg(feature = "testing")]
+    fn test_dir() -> String {
+        format!("{}/tests/conformance", env!("CARGO_MANIFEST_DIR"),)
+    }
+}
+
+// ===== impl InterfaceNet =====
+
+impl InterfaceNet {
+    fn new(
+        ifname: &str,
+        instance_channels_tx: &InstanceChannelsTx<Interface>,
+    ) -> Result<Self, IoError> {
+        // Create raw sockets.
+        let socket_vrrp = network::socket_vrrp(ifname)
+            .map_err(IoError::SocketError)
+            .and_then(|socket| {
+                AsyncFd::new(socket).map_err(IoError::SocketError)
+            })
+            .map(Arc::new)?;
+        let socket_arp = network::socket_arp(ifname)
+            .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 mut net_tx_task = tasks::net_tx(
+            socket_vrrp.clone(),
+            socket_arp.clone(),
+            net_tx_packetc,
+            #[cfg(feature = "testing")]
+            &instance_channels_tx.protocol_output,
+        );
+        let net_rx_task = tasks::net_rx(
+            socket_vrrp.clone(),
+            &instance_channels_tx.protocol_input.net_packet_rx,
+        );
+
+        Ok(InterfaceNet {
+            socket_vrrp,
+            socket_arp,
+            _net_tx_task: net_tx_task,
+            _net_rx_task: net_rx_task,
+            net_tx_packetp,
+        })
+    }
+}
+
+// ===== impl ProtocolInputChannelsRx =====
+
+#[async_trait]
+impl MessageReceiver<ProtocolInputMsg> for ProtocolInputChannelsRx {
+    async fn recv(&mut self) -> Option<ProtocolInputMsg> {
+        tokio::select! {
+            biased;
+            msg = self.net_packet_rx.recv() => {
+                msg.map(ProtocolInputMsg::NetRxPacket)
+            }
+        }
+    }
+}
+
+// ===== helper functions =====
+
+async fn process_ibus_msg(
+    interface: &mut Interface,
+    msg: IbusMsg,
+) -> Result<(), Error> {
+    match msg {
+        // Interface update notification.
+        IbusMsg::InterfaceUpd(msg) => {
+            southbound::process_iface_update(interface, msg);
+        }
+        // Interface address addition notification.
+        IbusMsg::InterfaceAddressAdd(msg) => {
+            southbound::process_addr_add(interface, msg);
+        }
+        // Interface address delete notification.
+        IbusMsg::InterfaceAddressDel(msg) => {
+            southbound::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..d4acf624
--- /dev/null
+++ b/holo-vrrp/src/lib.rs
@@ -0,0 +1,22 @@
+//
+// Copyright (c) The Holo Core Contributors
+//
+// SPDX-License-Identifier: MIT
+//
+
+#![cfg_attr(
+    feature = "testing",
+    allow(dead_code, unused_variables, unused_imports)
+)]
+#![feature(let_chains)]
+
+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..33636dcf
--- /dev/null
+++ b/holo-vrrp/src/network.rs
@@ -0,0 +1,90 @@
+//
+// Copyright (c) The Holo Core Contributors
+//
+// SPDX-License-Identifier: MIT
+//
+
+use std::net::IpAddr;
+use std::sync::Arc;
+
+use holo_utils::socket::{AsyncFd, Socket};
+use holo_utils::{Sender, UnboundedReceiver};
+use tokio::sync::mpsc::error::SendError;
+
+use crate::error::IoError;
+use crate::packet::Packet;
+use crate::tasks::messages::input::NetRxPacketMsg;
+use crate::tasks::messages::output::NetTxPacketMsg;
+
+pub(crate) fn socket_vrrp(_ifname: &str) -> Result<Socket, std::io::Error> {
+    #[cfg(not(feature = "testing"))]
+    {
+        todo!()
+    }
+    #[cfg(feature = "testing")]
+    {
+        Ok(Socket {})
+    }
+}
+
+pub(crate) fn socket_arp(_ifname: &str) -> Result<Socket, std::io::Error> {
+    #[cfg(not(feature = "testing"))]
+    {
+        todo!()
+    }
+    #[cfg(feature = "testing")]
+    {
+        Ok(Socket {})
+    }
+}
+
+#[cfg(not(feature = "testing"))]
+async fn send_packet_vrrp(
+    _socket: &AsyncFd<Socket>,
+    _src: IpAddr,
+    _dst: IpAddr,
+    _packet: Packet,
+) -> Result<(), IoError> {
+    todo!()
+}
+
+#[cfg(not(feature = "testing"))]
+async fn send_packet_arp(
+    _socket: &AsyncFd<Socket>,
+    // TODO: add other params
+) -> Result<(), IoError> {
+    todo!()
+}
+
+#[cfg(not(feature = "testing"))]
+pub(crate) async fn write_loop(
+    socket_vrrp: Arc<AsyncFd<Socket>>,
+    socket_arp: Arc<AsyncFd<Socket>>,
+    mut net_tx_packetc: UnboundedReceiver<NetTxPacketMsg>,
+) {
+    while let Some(msg) = net_tx_packetc.recv().await {
+        match msg {
+            NetTxPacketMsg::Vrrp { packet, src, dst } => {
+                if let Err(error) =
+                    send_packet_vrrp(&socket_vrrp, src, dst, packet).await
+                {
+                    error.log();
+                }
+            }
+            NetTxPacketMsg::Arp {} => {
+                if let Err(error) = send_packet_arp(&socket_arp).await {
+                    error.log();
+                }
+            }
+        }
+    }
+}
+
+#[cfg(not(feature = "testing"))]
+pub(crate) async fn read_loop(
+    _socket_vrrp: Arc<AsyncFd<Socket>>,
+    _net_packet_rxp: Sender<NetRxPacketMsg>,
+) -> Result<(), SendError<NetRxPacketMsg>> {
+    // TODO: receive VRRP packets
+    todo!()
+}
diff --git a/holo-vrrp/src/northbound/configuration.rs b/holo-vrrp/src/northbound/configuration.rs
new file mode 100644
index 00000000..f70a020f
--- /dev/null
+++ b/holo-vrrp/src/northbound/configuration.rs
@@ -0,0 +1,190 @@
+//
+// Copyright (c) The Holo Core Contributors
+//
+// SPDX-License-Identifier: MIT
+//
+
+#![allow(clippy::derivable_impls)]
+
+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::Instance;
+use crate::interface::Interface;
+
+#[derive(Debug, Default, EnumAsInner)]
+pub enum ListEntry {
+    #[default]
+    None,
+    Vrid(u8),
+}
+
+#[derive(Debug)]
+pub enum Resource {}
+
+#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
+pub enum Event {
+    InstanceCreate { vrid: u8 },
+    InstanceDelete { vrid: u8 },
+}
+
+pub static VALIDATION_CALLBACKS: Lazy<ValidationCallbacks> =
+    Lazy::new(load_validation_callbacks);
+pub static CALLBACKS: Lazy<Callbacks<Interface>> = 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<Ipv4Network>,
+}
+
+// ===== callbacks =====
+
+fn load_callbacks() -> Callbacks<Interface> {
+    CallbacksBuilder::<Interface>::default()
+        .path(interfaces::interface::ipv4::vrrp::vrrp_instance::PATH)
+        .create_apply(|interface, args| {
+            let vrid = args.dnode.get_u8_relative("./vrid").unwrap();
+            let instance = Instance::new();
+            interface.instances.insert(vrid, instance);
+
+            let event_queue = args.event_queue;
+            event_queue.insert(Event::InstanceCreate { 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;
+        })
+        .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();
+            instance.config.virtual_addresses.insert(addr);
+        })
+        .delete_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();
+            instance.config.virtual_addresses.remove(&addr);
+        })
+        .lookup(|_instance, _list_entry, _dnode| {
+            ListEntry::None
+        })
+        .build()
+}
+
+fn load_validation_callbacks() -> ValidationCallbacks {
+    ValidationCallbacksBuilder::default().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<Interface>> {
+        Some(&CALLBACKS)
+    }
+
+    async fn process_event(&mut self, event: Event) {
+        match event {
+            Event::InstanceCreate { vrid } => {
+                // TODO
+            }
+            Event::InstanceDelete { vrid } => {
+                // TODO
+            }
+        }
+    }
+}
+
+// ===== 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..df822d23
--- /dev/null
+++ b/holo-vrrp/src/northbound/mod.rs
@@ -0,0 +1,35 @@
+//
+// Copyright (c) The Holo Core Contributors
+//
+// SPDX-License-Identifier: MIT
+//
+
+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 {
+        // TODO
+        String::new()
+    }
+
+    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..e1245243
--- /dev/null
+++ b/holo-vrrp/src/northbound/notification.rs
@@ -0,0 +1,29 @@
+//
+// Copyright (c) The Holo Core Contributors
+//
+// SPDX-License-Identifier: MIT
+//
+
+use std::borrow::Cow;
+use std::net::Ipv4Addr;
+
+use holo_northbound::{notification, yang, NbProviderSender};
+use holo_yang::ToYang;
+
+use crate::instance::MasterReason;
+
+// ===== global functions =====
+
+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..eee30281
--- /dev/null
+++ b/holo-vrrp/src/northbound/state.rs
@@ -0,0 +1,92 @@
+//
+// Copyright (c) The Holo Core Contributors
+//
+// 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_utils::option::OptionExt;
+use holo_yang::ToYang;
+
+use crate::instance::Instance;
+use crate::interface::Interface;
+
+pub static CALLBACKS: Lazy<Callbacks<Interface>> = Lazy::new(load_callbacks);
+
+#[derive(Debug, Default, EnumAsInner)]
+pub enum ListEntry<'a> {
+    #[default]
+    None,
+    Instance(u8, &'a Instance),
+}
+
+// ===== callbacks =====
+
+fn load_callbacks() -> Callbacks<Interface> {
+    CallbacksBuilder::<Interface>::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().ignore_in_testing(),
+                // TODO
+                master_down_interval: None,
+                // 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(&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).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<Interface>> {
+        Some(&CALLBACKS)
+    }
+}
+
+// ===== impl ListEntry =====
+
+impl<'a> ListEntryKind for ListEntry<'a> {}
diff --git a/holo-vrrp/src/northbound/yang.rs b/holo-vrrp/src/northbound/yang.rs
new file mode 100644
index 00000000..5e38cd4d
--- /dev/null
+++ b/holo-vrrp/src/northbound/yang.rs
@@ -0,0 +1,73 @@
+//
+// Copyright (c) The Holo Core Contributors
+//
+// SPDX-License-Identifier: MIT
+//
+
+use std::borrow::Cow;
+
+use holo_yang::ToYang;
+
+use crate::instance::{Event, MasterReason, State};
+
+// ===== ToYang implementations =====
+
+impl ToYang for State {
+    fn to_yang(&self) -> Cow<'static, str> {
+        match self {
+            State::Initialize => "ietf-vrrp:initialize".into(),
+            State::Backup => "ietf-vrrp::backup".into(),
+            State::Master => "ietf-vrrp::master".into(),
+        }
+    }
+}
+
+impl ToYang for Event {
+    fn to_yang(&self) -> Cow<'static, str> {
+        match self {
+            Event::None => "ietf-vrrp:vrrp-event-none".into(),
+            Event::Startup => "ietf-vrrp:vrrp-event-startup".into(),
+            Event::Shutdown => "ietf-vrrp:vrrp-event-shutdown".into(),
+            Event::HigherPriorityBackup => {
+                "ietf-vrrp:vrrp-event-higher-priority-backup".into()
+            }
+            Event::MasterTimeout => {
+                "ietf-vrrp:vrrp-event-master-timeout".into()
+            }
+            Event::InterfaceUp => "ietf-vrrp:vrrp-event-interface-up".into(),
+            Event::InterfaceDown => {
+                "ietf-vrrp:vrrp-event-interface-down".into()
+            }
+            Event::NoPrimaryIpAddress => {
+                "ietf-vrrp:vrrp-event-no-primary-ip-address".into()
+            }
+            Event::PrimaryIpAddress => {
+                "ietf-vrrp:vrrp-event-primary-ip-address".into()
+            }
+            Event::NoVirtualIpAddresses => {
+                "ietf-vrrp:vrrp-event-no-virtual-ip-addresses".into()
+            }
+            Event::VirtualIpAddresses => {
+                "ietf-vrrp:vrrp-event-virtual-ip-addresses".into()
+            }
+            Event::PreemptHoldTimeout => {
+                "ietf-vrrp:vrrp-event-preempt-hold-timeout".into()
+            }
+            Event::LowerPriorityMaster => {
+                "ietf-vrrp:vrrp-event-lower-priority-master".into()
+            }
+            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..e3163ea3
--- /dev/null
+++ b/holo-vrrp/src/packet.rs
@@ -0,0 +1,75 @@
+//
+// Copyright (c) The Holo Core Contributors
+//
+// SPDX-License-Identifier: MIT
+//
+
+//use bitflags::bitflags;
+use bytes::{Buf, BufMut, Bytes, BytesMut};
+//use holo_utils::bytes::TLS_BUF;
+//use num_derive::FromPrimitive;
+//use num_traits::FromPrimitive;
+use serde::{Deserialize, Serialize};
+
+// Type aliases.
+pub type DecodeResult<T> = Result<T, DecodeError>;
+
+//
+// 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 Packet {
+    // TODO
+}
+
+// VRRP decode errors.
+#[derive(Debug, Eq, PartialEq)]
+#[derive(Deserialize, Serialize)]
+pub enum DecodeError {
+    // TODO
+}
+
+// ===== impl Packet =====
+
+impl Packet {
+    // Encodes VRRP packet into a bytes buffer.
+    pub fn encode(&self) -> BytesMut {
+        todo!()
+    }
+
+    // Decodes VRRP packet from a bytes buffer.
+    pub fn decode(_data: &[u8]) -> Result<Self, DecodeError> {
+        todo!()
+    }
+}
+
+// ===== impl DecodeError =====
+
+impl std::fmt::Display for DecodeError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        todo!()
+    }
+}
+
+impl std::error::Error for DecodeError {}
diff --git a/holo-vrrp/src/southbound.rs b/holo-vrrp/src/southbound.rs
new file mode 100644
index 00000000..5735afad
--- /dev/null
+++ b/holo-vrrp/src/southbound.rs
@@ -0,0 +1,50 @@
+//
+// Copyright (c) The Holo Core Contributors
+//
+// SPDX-License-Identifier: MIT
+//
+
+use holo_utils::southbound::{AddressMsg, InterfaceUpdateMsg};
+use ipnetwork::IpNetwork;
+
+use crate::interface::Interface;
+
+// ===== global functions =====
+
+pub(crate) fn process_iface_update(
+    iface: &mut Interface,
+    msg: InterfaceUpdateMsg,
+) {
+    if msg.ifname != iface.name {
+        return;
+    }
+
+    iface.system.flags = msg.flags;
+    iface.system.ifindex = Some(msg.ifindex);
+
+    // TODO: trigger protocol event?
+}
+
+pub(crate) fn process_addr_add(iface: &mut Interface, msg: AddressMsg) {
+    if msg.ifname != iface.name {
+        return;
+    }
+
+    if let IpNetwork::V4(addr) = msg.addr {
+        iface.system.addresses.insert(addr);
+
+        // TODO: trigger protocol event?
+    }
+}
+
+pub(crate) fn process_addr_del(iface: &mut Interface, msg: AddressMsg) {
+    if msg.ifname != iface.name {
+        return;
+    }
+
+    if let IpNetwork::V4(addr) = msg.addr {
+        iface.system.addresses.remove(&addr);
+
+        // TODO: trigger protocol event?
+    }
+}
diff --git a/holo-vrrp/src/tasks.rs b/holo-vrrp/src/tasks.rs
new file mode 100644
index 00000000..078584db
--- /dev/null
+++ b/holo-vrrp/src/tasks.rs
@@ -0,0 +1,159 @@
+//
+// Copyright (c) The Holo Core Contributors
+//
+// SPDX-License-Identifier: MIT
+//
+
+use std::sync::Arc;
+
+use holo_utils::socket::{AsyncFd, Socket};
+//use std::time::Duration;
+use holo_utils::task::Task;
+use holo_utils::{Sender, UnboundedReceiver};
+use tracing::{debug_span, Instrument};
+
+use crate::network;
+
+//
+// VRRP tasks diagram:
+//                                     +--------------+
+//                                     |  northbound  |
+//                                     +--------------+
+//                                           | ^
+//                                           | |
+//                        northbound_rx (1x) V | (1x) northbound_tx
+//                                     +--------------+
+//                                     |              |
+//                      net_rx (Nx) -> |   instance   | -> (Nx) net_tx
+//                                     |              |
+//                                     +--------------+
+//                              ibus_tx (1x) | ^ (1x) ibus_rx
+//                                           | |
+//                                           V |
+//                                     +--------------+
+//                                     |     ibus     |
+//                                     +--------------+
+//
+
+// BGP inter-task message types.
+pub mod messages {
+    use std::net::IpAddr;
+
+    use serde::{Deserialize, Serialize};
+
+    use crate::packet::{DecodeError, Packet};
+
+    // Type aliases.
+    pub type ProtocolInputMsg = input::ProtocolMsg;
+    pub type ProtocolOutputMsg = output::ProtocolMsg;
+
+    // Input messages (child task -> main task).
+    pub mod input {
+        use super::*;
+
+        #[derive(Debug, Deserialize, Serialize)]
+        pub enum ProtocolMsg {
+            NetRxPacket(NetRxPacketMsg),
+            // TODO
+        }
+
+        #[derive(Debug, Deserialize, Serialize)]
+        pub struct NetRxPacketMsg {
+            pub src: IpAddr,
+            pub packet: Result<Packet, DecodeError>,
+        }
+    }
+
+    // Output messages (main task -> child task).
+    pub mod output {
+        use super::*;
+
+        #[derive(Debug, Serialize)]
+        pub enum ProtocolMsg {
+            NetTxPacket(NetTxPacketMsg),
+        }
+
+        #[derive(Clone, Debug, Serialize)]
+        pub enum NetTxPacketMsg {
+            Vrrp {
+                packet: Packet,
+                src: IpAddr,
+                dst: IpAddr,
+            },
+            Arp {
+                // TODO
+            },
+        }
+    }
+}
+
+// ===== VRRP tasks =====
+
+// Network Rx task.
+pub(crate) fn net_rx(
+    socket_vrrp: Arc<AsyncFd<Socket>>,
+    net_packet_rxp: &Sender<messages::input::NetRxPacketMsg>,
+) -> 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::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<AsyncFd<Socket>>,
+    socket_arp: Arc<AsyncFd<Socket>>,
+    mut net_packet_txc: UnboundedReceiver<messages::output::NetTxPacketMsg>,
+    #[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;
+            }
+        })
+    }
+}
diff --git a/holo-vrrp/tests/conformance/mod.rs b/holo-vrrp/tests/conformance/mod.rs
new file mode 100644
index 00000000..8151c7f1
--- /dev/null
+++ b/holo-vrrp/tests/conformance/mod.rs
@@ -0,0 +1,7 @@
+//
+// Copyright (c) The Holo Core Contributors
+//
+// SPDX-License-Identifier: MIT
+//
+
+// TODO
diff --git a/holo-vrrp/tests/mod.rs b/holo-vrrp/tests/mod.rs
new file mode 100644
index 00000000..f505cbe8
--- /dev/null
+++ b/holo-vrrp/tests/mod.rs
@@ -0,0 +1,8 @@
+//
+// Copyright (c) The Holo Core Contributors
+//
+// SPDX-License-Identifier: MIT
+//
+
+mod conformance;
+mod packet;
diff --git a/holo-vrrp/tests/packet/mod.rs b/holo-vrrp/tests/packet/mod.rs
new file mode 100644
index 00000000..8151c7f1
--- /dev/null
+++ b/holo-vrrp/tests/packet/mod.rs
@@ -0,0 +1,7 @@
+//
+// Copyright (c) The Holo Core Contributors
+//
+// SPDX-License-Identifier: MIT
+//
+
+// TODO
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/ietf-vrrp-holo-deviations.yang b/holo-yang/modules/deviations/ietf-vrrp-holo-deviations.yang
new file mode 100644
index 00000000..92253c52
--- /dev/null
+++ b/holo-yang/modules/deviations/ietf-vrrp-holo-deviations.yang
@@ -0,0 +1,611 @@
+module ietf-vrrp-holo-deviations {
+  yang-version 1.1;
+  namespace "http://holo-routing.org/yang/ietf-vrrp-holo-deviations";
+  prefix ietf-vrrp-holo-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:   <https://datatracker.ietf.org/wg/rtgwg/>
+     WG List:  <mailto:rtgwg@ietf.org>
+
+     Editor:   Xufeng Liu
+               <mailto:xufeng.liu.ietf@gmail.com>
+
+     Editor:   Athanasios Kyparlis
+               <mailto:Athanasios_Kyparlis@jabil.com>
+     Editor:   Ravi Parikh
+               <mailto:parikhr@vmware.com>
+
+     Editor:   Acee Lindem
+               <mailto:acee@cisco.com>
+
+     Editor:   Mingui Zhang
+               <mailto:zhangmingui@huawei.com>";
+
+  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 abcb3aaf..36875eae 100644
--- a/holo-yang/src/lib.rs
+++ b/holo-yang/src/lib.rs
@@ -119,6 +119,8 @@ pub static YANG_EMBEDDED_MODULES: Lazy<EmbeddedModules> = 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"),
@@ -126,6 +128,8 @@ pub static YANG_EMBEDDED_MODULES: Lazy<EmbeddedModules> = Lazy::new(|| {
             include_str!("../modules/augmentations/holo-ospf.yang"),
         EmbeddedModuleKey::new("holo-ospf-dev", None, None, None) =>
             include_str!("../modules/augmentations/holo-ospf-dev.yang"),
+        EmbeddedModuleKey::new("holo-vrrp", None, None, None) =>
+            include_str!("../modules/augmentations/holo-vrrp.yang"),
         // IETF Holo deviations
         EmbeddedModuleKey::new("ietf-bgp-holo-deviations", None, None, None) =>
             include_str!("../modules/deviations/ietf-bgp-holo-deviations.yang"),
@@ -159,6 +163,8 @@ pub static YANG_EMBEDDED_MODULES: Lazy<EmbeddedModules> = Lazy::new(|| {
             include_str!("../modules/deviations/ietf-routing-policy-holo-deviations.yang"),
         EmbeddedModuleKey::new("ietf-segment-routing-mpls-holo-deviations", None, None, None) =>
             include_str!("../modules/deviations/ietf-segment-routing-mpls-holo-deviations.yang"),
+        EmbeddedModuleKey::new("ietf-vrrp-holo-deviations", None, None, None) =>
+            include_str!("../modules/deviations/ietf-vrrp-holo-deviations.yang"),
     }
 });
 
@@ -202,10 +208,12 @@ pub static YANG_IMPLEMENTED_MODULES: Lazy<Vec<&'static str>> =
             "ietf-ospfv3-extended-lsa",
             "ietf-rip",
             "ietf-tcp",
+            "ietf-vrrp",
             // IETF Holo augmentations
             "holo-bgp",
             "holo-ospf",
             "holo-ospf-dev",
+            "holo-vrrp",
         ]
     });
 
@@ -248,6 +256,9 @@ pub static YANG_FEATURES: Lazy<HashMap<&'static str, Vec<&'static str>>> =
             "ietf-segment-routing-common" => vec![
                 "sid-last-hop-behavior",
             ],
+            "ietf-vrrp" => vec![
+                "validate-interval-errors",
+            ],
         }
     });