use std::net::IpAddr;
use super::{
attr::AttrIter,
builder::MessageBuilder,
connection::Connection,
error::{Error, Result},
interface_ref::InterfaceRef,
message::{NLM_F_ACK, NLM_F_DUMP, NLM_F_REQUEST, NLMSG_HDRLEN, NlMsgType},
protocol::Route,
types::nexthop::{NexthopGrp, NhMsg, nha, nha_res_group, nhf, nhg_type},
};
const NLM_F_CREATE: u16 = 0x400;
const NLM_F_REPLACE: u16 = 0x100;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum NexthopGroupType {
#[default]
Multipath,
Resilient,
}
impl From<u16> for NexthopGroupType {
fn from(val: u16) -> Self {
match val {
nhg_type::RES => Self::Resilient,
_ => Self::Multipath,
}
}
}
impl From<NexthopGroupType> for u16 {
fn from(val: NexthopGroupType) -> Self {
match val {
NexthopGroupType::Multipath => nhg_type::MPATH,
NexthopGroupType::Resilient => nhg_type::RES,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ResilientParams {
pub buckets: u16,
pub idle_timer: u32,
pub unbalanced_timer: u32,
}
#[derive(Debug, Clone)]
pub struct NexthopGroupMember {
pub id: u32,
pub weight: u8,
}
#[derive(Debug, Clone)]
pub struct Nexthop {
pub id: u32,
pub gateway: Option<IpAddr>,
pub ifindex: Option<u32>,
pub family: u8,
pub flags: u32,
pub protocol: u8,
pub scope: u8,
pub blackhole: bool,
pub fdb: bool,
pub group: Option<Vec<NexthopGroupMember>>,
pub group_type: Option<NexthopGroupType>,
pub resilient: Option<ResilientParams>,
}
impl Nexthop {
pub fn parse(data: &[u8]) -> Result<Self> {
let nhmsg = NhMsg::from_bytes(data)?;
let attrs_data = &data[NhMsg::SIZE..];
let mut id = 0u32;
let mut gateway: Option<IpAddr> = None;
let mut ifindex: Option<u32> = None;
let mut blackhole = false;
let mut fdb = false;
let mut group: Option<Vec<NexthopGroupMember>> = None;
let mut group_type: Option<NexthopGroupType> = None;
let mut resilient: Option<ResilientParams> = None;
for (attr_type, payload) in AttrIter::new(attrs_data) {
match attr_type {
nha::ID if payload.len() >= 4 => {
id = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]);
}
nha::GATEWAY => {
gateway = match payload.len() {
4 => Some(IpAddr::from(<[u8; 4]>::try_from(payload).unwrap())),
16 => Some(IpAddr::from(<[u8; 16]>::try_from(payload).unwrap())),
_ => None,
};
}
nha::OIF if payload.len() >= 4 => {
ifindex = Some(u32::from_ne_bytes([
payload[0], payload[1], payload[2], payload[3],
]));
}
nha::BLACKHOLE => {
blackhole = true;
}
nha::FDB => {
fdb = true;
}
nha::GROUP => {
let mut members = Vec::new();
let mut offset = 0;
while offset + NexthopGrp::SIZE <= payload.len() {
if let Some(grp) = NexthopGrp::from_bytes(&payload[offset..]) {
members.push(NexthopGroupMember {
id: grp.id,
weight: grp.weight,
});
}
offset += NexthopGrp::SIZE;
}
if !members.is_empty() {
group = Some(members);
}
}
nha::GROUP_TYPE if payload.len() >= 2 => {
let gt = u16::from_ne_bytes([payload[0], payload[1]]);
group_type = Some(NexthopGroupType::from(gt));
}
nha::RES_GROUP => {
let mut params = ResilientParams::default();
for (res_type, res_payload) in AttrIter::new(payload) {
match res_type {
nha_res_group::BUCKETS if res_payload.len() >= 2 => {
params.buckets =
u16::from_ne_bytes([res_payload[0], res_payload[1]]);
}
nha_res_group::IDLE_TIMER if res_payload.len() >= 4 => {
params.idle_timer = u32::from_ne_bytes([
res_payload[0],
res_payload[1],
res_payload[2],
res_payload[3],
]);
}
nha_res_group::UNBALANCED_TIMER if res_payload.len() >= 4 => {
params.unbalanced_timer = u32::from_ne_bytes([
res_payload[0],
res_payload[1],
res_payload[2],
res_payload[3],
]);
}
_ => {}
}
}
resilient = Some(params);
}
_ => {}
}
}
Ok(Self {
id,
gateway,
ifindex,
family: nhmsg.nh_family,
flags: nhmsg.nh_flags,
protocol: nhmsg.nh_protocol,
scope: nhmsg.nh_scope,
blackhole,
fdb,
group,
group_type,
resilient,
})
}
pub fn is_group(&self) -> bool {
self.group.is_some()
}
pub fn is_onlink(&self) -> bool {
self.flags & nhf::ONLINK != 0
}
pub fn is_dead(&self) -> bool {
self.flags & nhf::DEAD != 0
}
pub fn is_linkdown(&self) -> bool {
self.flags & nhf::LINKDOWN != 0
}
}
#[derive(Debug, Clone)]
#[must_use = "builders do nothing unless used"]
pub struct NexthopBuilder {
id: u32,
gateway: Option<IpAddr>,
dev: Option<InterfaceRef>,
blackhole: bool,
onlink: bool,
fdb: bool,
protocol: Option<u8>,
}
impl NexthopBuilder {
pub fn new(id: u32) -> Self {
Self {
id,
gateway: None,
dev: None,
blackhole: false,
onlink: false,
fdb: false,
protocol: None,
}
}
pub fn gateway(mut self, gw: IpAddr) -> Self {
self.gateway = Some(gw);
self
}
pub fn dev(mut self, dev: impl Into<String>) -> Self {
self.dev = Some(InterfaceRef::Name(dev.into()));
self
}
pub fn ifindex(mut self, ifindex: u32) -> Self {
self.dev = Some(InterfaceRef::Index(ifindex));
self
}
pub fn device_ref(&self) -> Option<&InterfaceRef> {
self.dev.as_ref()
}
pub fn blackhole(mut self) -> Self {
self.blackhole = true;
self
}
pub fn onlink(mut self) -> Self {
self.onlink = true;
self
}
pub fn fdb(mut self) -> Self {
self.fdb = true;
self
}
pub fn protocol(mut self, protocol: u8) -> Self {
self.protocol = Some(protocol);
self
}
pub(crate) fn write_to(&self, builder: &mut MessageBuilder, ifindex: Option<u32>) {
let family = match &self.gateway {
Some(IpAddr::V4(_)) => libc::AF_INET as u8,
Some(IpAddr::V6(_)) => libc::AF_INET6 as u8,
None => libc::AF_UNSPEC as u8,
};
let mut nh_flags = 0u32;
if self.onlink {
nh_flags |= nhf::ONLINK;
}
let nhmsg = NhMsg::new()
.with_family(family)
.with_protocol(self.protocol.unwrap_or(4)) .with_flags(nh_flags);
builder.append(&nhmsg);
builder.append_attr_u32(nha::ID, self.id);
if let Some(ref gw) = self.gateway {
match gw {
IpAddr::V4(v4) => {
builder.append_attr(nha::GATEWAY, &v4.octets());
}
IpAddr::V6(v6) => {
builder.append_attr(nha::GATEWAY, &v6.octets());
}
}
}
if let Some(idx) = ifindex {
builder.append_attr_u32(nha::OIF, idx);
}
if self.blackhole {
builder.append_attr(nha::BLACKHOLE, &[]);
}
if self.fdb {
builder.append_attr(nha::FDB, &[]);
}
}
}
#[derive(Debug, Clone)]
#[must_use = "builders do nothing unless used"]
pub struct NexthopGroupBuilder {
id: u32,
group_type: NexthopGroupType,
members: Vec<(u32, u8)>, buckets: Option<u16>,
idle_timer: Option<u32>,
unbalanced_timer: Option<u32>,
protocol: Option<u8>,
}
impl NexthopGroupBuilder {
pub fn new(id: u32) -> Self {
Self {
id,
group_type: NexthopGroupType::Multipath,
members: Vec::new(),
buckets: None,
idle_timer: None,
unbalanced_timer: None,
protocol: None,
}
}
pub fn multipath(mut self) -> Self {
self.group_type = NexthopGroupType::Multipath;
self
}
pub fn resilient(mut self) -> Self {
self.group_type = NexthopGroupType::Resilient;
self
}
pub fn member(mut self, nexthop_id: u32, weight: u8) -> Self {
self.members.push((nexthop_id, weight));
self
}
pub fn buckets(mut self, buckets: u16) -> Self {
self.buckets = Some(buckets);
self
}
pub fn idle_timer(mut self, seconds: u32) -> Self {
self.idle_timer = Some(seconds);
self
}
pub fn unbalanced_timer(mut self, seconds: u32) -> Self {
self.unbalanced_timer = Some(seconds);
self
}
pub fn protocol(mut self, protocol: u8) -> Self {
self.protocol = Some(protocol);
self
}
fn build(&self, msg_type: u16, flags: u16) -> Result<MessageBuilder> {
if self.members.is_empty() {
return Err(Error::InvalidMessage(
"nexthop group must have at least one member".into(),
));
}
let nhmsg = NhMsg::new()
.with_family(libc::AF_UNSPEC as u8)
.with_protocol(self.protocol.unwrap_or(4));
let mut builder = MessageBuilder::new(msg_type, flags);
builder.append(&nhmsg);
builder.append_attr_u32(nha::ID, self.id);
let gt: u16 = self.group_type.into();
builder.append_attr(nha::GROUP_TYPE, >.to_ne_bytes());
let mut grp_data = Vec::with_capacity(self.members.len() * NexthopGrp::SIZE);
for (nh_id, weight) in &self.members {
let grp = NexthopGrp::new(*nh_id, *weight);
grp_data.extend_from_slice(grp.as_bytes());
}
builder.append_attr(nha::GROUP, &grp_data);
if self.group_type == NexthopGroupType::Resilient
&& (self.buckets.is_some()
|| self.idle_timer.is_some()
|| self.unbalanced_timer.is_some())
{
let res_token = builder.nest_start(nha::RES_GROUP);
if let Some(buckets) = self.buckets {
builder.append_attr(nha_res_group::BUCKETS, &buckets.to_ne_bytes());
}
if let Some(idle) = self.idle_timer {
builder.append_attr(nha_res_group::IDLE_TIMER, &idle.to_ne_bytes());
}
if let Some(unbal) = self.unbalanced_timer {
builder.append_attr(nha_res_group::UNBALANCED_TIMER, &unbal.to_ne_bytes());
}
builder.nest_end(res_token);
}
Ok(builder)
}
}
impl Connection<Route> {
async fn resolve_nexthop_interface(&self, builder: &NexthopBuilder) -> Result<Option<u32>> {
match builder.device_ref() {
Some(iface) => Ok(Some(self.resolve_interface(iface).await?)),
None => Ok(None),
}
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "get_nexthops"))]
pub async fn get_nexthops(&self) -> Result<Vec<Nexthop>> {
let nhmsg = NhMsg::new().with_family(libc::AF_UNSPEC as u8);
let mut builder =
MessageBuilder::new(NlMsgType::RTM_GETNEXTHOP, NLM_F_REQUEST | NLM_F_DUMP);
builder.append(&nhmsg);
let responses = self.send_dump(builder).await?;
let mut nexthops = Vec::new();
for data in responses {
if data.len() > NLMSG_HDRLEN
&& let Ok(nh) = Nexthop::parse(&data[NLMSG_HDRLEN..])
{
nexthops.push(nh);
}
}
Ok(nexthops)
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "get_nexthop"))]
pub async fn get_nexthop(&self, id: u32) -> Result<Option<Nexthop>> {
let nhmsg = NhMsg::new().with_family(libc::AF_UNSPEC as u8);
let mut builder = MessageBuilder::new(NlMsgType::RTM_GETNEXTHOP, NLM_F_REQUEST);
builder.append(&nhmsg);
builder.append_attr_u32(nha::ID, id);
match self.send_request(builder).await {
Ok(data) => {
if data.len() > NLMSG_HDRLEN {
Ok(Some(Nexthop::parse(&data[NLMSG_HDRLEN..])?))
} else {
Ok(None)
}
}
Err(e) if e.is_not_found() => Ok(None),
Err(e) => Err(e),
}
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "add_nexthop"))]
pub async fn add_nexthop(&self, nh_builder: NexthopBuilder) -> Result<()> {
let ifindex = self.resolve_nexthop_interface(&nh_builder).await?;
let mut msg = MessageBuilder::new(
NlMsgType::RTM_NEWNEXTHOP,
NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE,
);
nh_builder.write_to(&mut msg, ifindex);
self.send_ack(msg)
.await
.map_err(|e| e.with_context("add_nexthop"))
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "replace_nexthop"))]
pub async fn replace_nexthop(&self, nh_builder: NexthopBuilder) -> Result<()> {
let ifindex = self.resolve_nexthop_interface(&nh_builder).await?;
let mut msg = MessageBuilder::new(
NlMsgType::RTM_NEWNEXTHOP,
NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_REPLACE,
);
nh_builder.write_to(&mut msg, ifindex);
self.send_ack(msg)
.await
.map_err(|e| e.with_context("replace_nexthop"))
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "del_nexthop"))]
pub async fn del_nexthop(&self, id: u32) -> Result<()> {
let nhmsg = NhMsg::new().with_family(libc::AF_UNSPEC as u8);
let mut builder = MessageBuilder::new(NlMsgType::RTM_DELNEXTHOP, NLM_F_REQUEST | NLM_F_ACK);
builder.append(&nhmsg);
builder.append_attr_u32(nha::ID, id);
self.send_ack(builder)
.await
.map_err(|e| e.with_context("del_nexthop"))
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "add_nexthop_group"))]
pub async fn add_nexthop_group(&self, builder: NexthopGroupBuilder) -> Result<()> {
let msg = builder.build(
NlMsgType::RTM_NEWNEXTHOP,
NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE,
)?;
self.send_ack(msg)
.await
.map_err(|e| e.with_context("add_nexthop_group"))
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "replace_nexthop_group"))]
pub async fn replace_nexthop_group(&self, builder: NexthopGroupBuilder) -> Result<()> {
let msg = builder.build(
NlMsgType::RTM_NEWNEXTHOP,
NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_REPLACE,
)?;
self.send_ack(msg)
.await
.map_err(|e| e.with_context("replace_nexthop_group"))
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "del_nexthop_group"))]
pub async fn del_nexthop_group(&self, id: u32) -> Result<()> {
self.del_nexthop(id).await
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "get_nexthop_groups"))]
pub async fn get_nexthop_groups(&self) -> Result<Vec<Nexthop>> {
let nhmsg = NhMsg::new().with_family(libc::AF_UNSPEC as u8);
let mut builder =
MessageBuilder::new(NlMsgType::RTM_GETNEXTHOP, NLM_F_REQUEST | NLM_F_DUMP);
builder.append(&nhmsg);
builder.append_attr(nha::GROUPS, &[]);
let responses = self.send_dump(builder).await?;
let mut groups = Vec::new();
for data in responses {
if data.len() > NLMSG_HDRLEN
&& let Ok(nh) = Nexthop::parse(&data[NLMSG_HDRLEN..])
&& nh.is_group()
{
groups.push(nh);
}
}
Ok(groups)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nexthop_builder() {
let nh = NexthopBuilder::new(42)
.gateway("192.168.1.1".parse().unwrap())
.ifindex(5)
.onlink();
assert_eq!(nh.id, 42);
assert!(nh.gateway.is_some());
assert_eq!(nh.dev, Some(InterfaceRef::Index(5)));
assert!(nh.onlink);
}
#[test]
fn test_nexthop_builder_blackhole() {
let nh = NexthopBuilder::new(1).blackhole();
assert!(nh.blackhole);
assert!(nh.gateway.is_none());
}
#[test]
fn test_nexthop_group_builder() {
let grp = NexthopGroupBuilder::new(100)
.member(1, 1)
.member(2, 2)
.resilient()
.buckets(128)
.idle_timer(120);
assert_eq!(grp.id, 100);
assert_eq!(grp.members.len(), 2);
assert_eq!(grp.group_type, NexthopGroupType::Resilient);
assert_eq!(grp.buckets, Some(128));
assert_eq!(grp.idle_timer, Some(120));
}
#[test]
fn test_group_type_conversion() {
assert_eq!(u16::from(NexthopGroupType::Multipath), nhg_type::MPATH);
assert_eq!(u16::from(NexthopGroupType::Resilient), nhg_type::RES);
assert_eq!(
NexthopGroupType::from(nhg_type::MPATH),
NexthopGroupType::Multipath
);
assert_eq!(
NexthopGroupType::from(nhg_type::RES),
NexthopGroupType::Resilient
);
}
}