From 545f2db13a9363d4d1dab85aa3ce02c5200578e7 Mon Sep 17 00:00:00 2001 From: nanamicat Date: Sat, 10 Jan 2026 21:41:43 +0800 Subject: [PATCH 1/2] add nexthop support --- src/lib.rs | 1 + src/message.rs | 48 ++++++++ src/nexthop/attribute.rs | 230 +++++++++++++++++++++++++++++++++++++++ src/nexthop/buffer.rs | 34 ++++++ src/nexthop/header.rs | 65 +++++++++++ src/nexthop/message.rs | 57 ++++++++++ src/nexthop/mod.rs | 13 +++ src/route/attribute.rs | 13 ++- 8 files changed, 458 insertions(+), 3 deletions(-) create mode 100644 src/nexthop/attribute.rs create mode 100644 src/nexthop/buffer.rs create mode 100644 src/nexthop/header.rs create mode 100644 src/nexthop/message.rs create mode 100644 src/nexthop/mod.rs diff --git a/src/lib.rs b/src/lib.rs index 037c57ce..cb790946 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,7 @@ mod address_family; pub mod link; pub mod neighbour; pub mod neighbour_table; +pub mod nexthop; pub mod nsid; pub mod prefix; pub mod route; diff --git a/src/message.rs b/src/message.rs index 9a1e88cd..78fb4f1e 100644 --- a/src/message.rs +++ b/src/message.rs @@ -10,6 +10,7 @@ use crate::{ link::{LinkMessage, LinkMessageBuffer}, neighbour::{NeighbourMessage, NeighbourMessageBuffer}, neighbour_table::{NeighbourTableMessage, NeighbourTableMessageBuffer}, + nexthop::{NexthopMessage, NexthopMessageBuffer}, nsid::{NsidMessage, NsidMessageBuffer}, prefix::{PrefixMessage, PrefixMessageBuffer}, route::{RouteHeader, RouteMessage, RouteMessageBuffer}, @@ -74,6 +75,9 @@ const RTM_DELCHAIN: u16 = 101; const RTM_GETCHAIN: u16 = 102; const RTM_NEWLINKPROP: u16 = 108; const RTM_DELLINKPROP: u16 = 109; +const RTM_NEWNEXTHOP: u16 = 104; +const RTM_DELNEXTHOP: u16 = 105; +const RTM_GETNEXTHOP: u16 = 106; buffer!(RouteNetlinkMessageBuffer); @@ -318,6 +322,22 @@ impl<'a, T: AsRef<[u8]> + ?Sized> } } + // Nexthop Messages + RTM_NEWNEXTHOP | RTM_GETNEXTHOP | RTM_DELNEXTHOP => { + let err = "invalid nexthop message"; + let msg = NexthopMessage::parse( + &NexthopMessageBuffer::new_checked(&buf.inner()) + .context(err)?, + ) + .context(err)?; + match message_type { + RTM_NEWNEXTHOP => RouteNetlinkMessage::NewNexthop(msg), + RTM_DELNEXTHOP => RouteNetlinkMessage::DelNexthop(msg), + RTM_GETNEXTHOP => RouteNetlinkMessage::GetNexthop(msg), + _ => unreachable!(), + } + } + _ => { return Err( format!("Unknown message type: {message_type}").into() @@ -371,6 +391,9 @@ pub enum RouteNetlinkMessage { NewRule(RuleMessage), DelRule(RuleMessage), GetRule(RuleMessage), + NewNexthop(NexthopMessage), + DelNexthop(NexthopMessage), + GetNexthop(NexthopMessage), } impl RouteNetlinkMessage { @@ -522,6 +545,18 @@ impl RouteNetlinkMessage { matches!(self, RouteNetlinkMessage::DelRule(_)) } + pub fn is_new_nexthop(&self) -> bool { + matches!(self, RouteNetlinkMessage::NewNexthop(_)) + } + + pub fn is_del_nexthop(&self) -> bool { + matches!(self, RouteNetlinkMessage::DelNexthop(_)) + } + + pub fn is_get_nexthop(&self) -> bool { + matches!(self, RouteNetlinkMessage::GetNexthop(_)) + } + pub fn message_type(&self) -> u16 { use self::RouteNetlinkMessage::*; @@ -566,6 +601,9 @@ impl RouteNetlinkMessage { GetRule(_) => RTM_GETRULE, NewRule(_) => RTM_NEWRULE, DelRule(_) => RTM_DELRULE, + NewNexthop(_) => RTM_NEWNEXTHOP, + DelNexthop(_) => RTM_DELNEXTHOP, + GetNexthop(_) => RTM_GETNEXTHOP, } } } @@ -629,6 +667,11 @@ impl Emitable for RouteNetlinkMessage { | GetRule(ref msg) => msg.buffer_len(), + | NewNexthop(ref msg) + | DelNexthop(ref msg) + | GetNexthop(ref msg) + => msg.buffer_len(), + | NewTrafficAction(ref msg) | DelTrafficAction(ref msg) | GetTrafficAction(ref msg) @@ -694,6 +737,11 @@ impl Emitable for RouteNetlinkMessage { | GetRule(ref msg) => msg.emit(buffer), + | NewNexthop(ref msg) + | DelNexthop(ref msg) + | GetNexthop(ref msg) + => msg.emit(buffer), + | NewTrafficAction(ref msg) | DelTrafficAction(ref msg) | GetTrafficAction(ref msg) diff --git a/src/nexthop/attribute.rs b/src/nexthop/attribute.rs new file mode 100644 index 00000000..63d43351 --- /dev/null +++ b/src/nexthop/attribute.rs @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: MIT + +use netlink_packet_core::{ + DecodeError, Emitable, Nla, Parseable, ParseableParametrized, +}; + +#[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] +pub enum NexthopAttribute { + Id(u32), + Group(Vec), + GroupType(u16), + Blackhole, + Oif(u32), + Gateway(Vec), // Can be IPv4 or IPv6 + EncapType(u16), + Encap(Vec), // TODO: Parse encap attributes properly + Fdb(Vec), // TODO: Parse FDB + ResGroup(Vec), // TODO: Parse ResGroup + Other(u16, Vec), +} + +impl Nla for NexthopAttribute { + fn value_len(&self) -> usize { + use self::NexthopAttribute::*; + match self { + Id(_) => 4, + Group(entries) => entries.len() * 8, // Each entry is 8 bytes + GroupType(_) => 2, + Blackhole => 0, + Oif(_) => 4, + Gateway(bytes) => bytes.len(), + EncapType(_) => 2, + Encap(bytes) => bytes.len(), + Fdb(bytes) => bytes.len(), + ResGroup(bytes) => bytes.len(), + Other(_, bytes) => bytes.len(), + } + } + + #[rustfmt::skip] + fn emit_value(&self, buffer: &mut [u8]) { + use self::NexthopAttribute::*; + match self { + | Id(value) + | Oif(value) + => buffer[0..4].copy_from_slice(&value.to_ne_bytes()), + + | GroupType(value) + | EncapType(value) + => buffer[0..2].copy_from_slice(&value.to_ne_bytes()), + + Group(entries) => { + for (i, entry) in entries.iter().enumerate() { + entry.emit(&mut buffer[i * 8..]); + } + } + Blackhole => {}, + Gateway(bytes) + | Encap(bytes) + | Fdb(bytes) + | ResGroup(bytes) + | Other(_, bytes) + => buffer.copy_from_slice(bytes), + } + } + + fn kind(&self) -> u16 { + use self::NexthopAttribute::*; + match self { + Id(_) => NHA_ID, + Group(_) => NHA_GROUP, + GroupType(_) => NHA_GROUP_TYPE, + Blackhole => NHA_BLACKHOLE, + Oif(_) => NHA_OIF, + Gateway(_) => NHA_GATEWAY, + EncapType(_) => NHA_ENCAP_TYPE, + Encap(_) => NHA_ENCAP, + Fdb(_) => NHA_FDB, + ResGroup(_) => NHA_RES_GROUP, + Other(kind, _) => *kind, + } + } +} + +pub struct NexthopAttributeType; + +impl NexthopAttributeType { + pub const ID: u16 = NHA_ID; + pub const GROUP: u16 = NHA_GROUP; + pub const GROUP_TYPE: u16 = NHA_GROUP_TYPE; + pub const BLACKHOLE: u16 = NHA_BLACKHOLE; + pub const OIF: u16 = NHA_OIF; + pub const GATEWAY: u16 = NHA_GATEWAY; + pub const ENCAP_TYPE: u16 = NHA_ENCAP_TYPE; + pub const ENCAP: u16 = NHA_ENCAP; + pub const FDB: u16 = NHA_FDB; + pub const RES_GROUP: u16 = NHA_RES_GROUP; +} + +impl<'a, T: AsRef<[u8]> + ?Sized> ParseableParametrized<(&'a T, u16), ()> + for NexthopAttribute +{ + fn parse_with_param( + input: &(&'a T, u16), + _params: (), + ) -> Result { + let (payload, kind) = input; + let payload = payload.as_ref(); + + Ok(match *kind { + NHA_ID => { + if payload.len() != 4 { + return Err(DecodeError::from("Invalid NHA_ID length")); + } + NexthopAttribute::Id(u32::from_ne_bytes( + payload.try_into().map_err(|_| { + DecodeError::from("Invalid NHA_ID length") + })?, + )) + } + NHA_GROUP => { + if payload.len() % 8 != 0 { + return Err(DecodeError::from("Invalid NHA_GROUP length")); + } + let mut entries = Vec::new(); + for chunk in payload.chunks(8) { + if let Ok(entry) = NexthopGroupEntry::parse(&chunk) { + entries.push(entry); + } else { + return Err(DecodeError::from( + "Failed to parse group entry", + )); + } + } + NexthopAttribute::Group(entries) + } + NHA_GROUP_TYPE => { + if payload.len() != 2 { + return Err(DecodeError::from( + "Invalid NHA_GROUP_TYPE length", + )); + } + NexthopAttribute::GroupType(u16::from_ne_bytes( + payload.try_into().map_err(|_| { + DecodeError::from("Invalid NHA_GROUP_TYPE length") + })?, + )) + } + NHA_BLACKHOLE => NexthopAttribute::Blackhole, + NHA_OIF => { + if payload.len() != 4 { + return Err(DecodeError::from("Invalid NHA_OIF length")); + } + NexthopAttribute::Oif(u32::from_ne_bytes( + payload.try_into().map_err(|_| { + DecodeError::from("Invalid NHA_OIF length") + })?, + )) + } + NHA_GATEWAY => NexthopAttribute::Gateway(payload.to_vec()), + NHA_ENCAP_TYPE => { + if payload.len() != 2 { + return Err(DecodeError::from( + "Invalid NHA_ENCAP_TYPE length", + )); + } + NexthopAttribute::EncapType(u16::from_ne_bytes( + payload.try_into().map_err(|_| { + DecodeError::from("Invalid NHA_ENCAP_TYPE length") + })?, + )) + } + NHA_ENCAP => NexthopAttribute::Encap(payload.to_vec()), + NHA_FDB => NexthopAttribute::Fdb(payload.to_vec()), + NHA_RES_GROUP => NexthopAttribute::ResGroup(payload.to_vec()), + _ => NexthopAttribute::Other(*kind, payload.to_vec()), + }) + } +} + +// Constants +const NHA_ID: u16 = 1; +const NHA_GROUP: u16 = 2; +const NHA_GROUP_TYPE: u16 = 3; +const NHA_BLACKHOLE: u16 = 4; +const NHA_OIF: u16 = 5; +const NHA_GATEWAY: u16 = 6; +const NHA_ENCAP_TYPE: u16 = 7; +const NHA_ENCAP: u16 = 8; +// const NHA_GROUPS: u16 = 9; // Not implementing NHA_GROUPS as it seems deprecated or complex +// const NHA_MASTER: u16 = 10; +const NHA_FDB: u16 = 11; +const NHA_RES_GROUP: u16 = 12; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct NexthopGroupEntry { + pub id: u32, + pub weight: u8, + pub resvd1: u8, + pub resvd2: u16, +} + +impl Emitable for NexthopGroupEntry { + fn buffer_len(&self) -> usize { + 8 + } + + fn emit(&self, buffer: &mut [u8]) { + buffer[0..4].copy_from_slice(&self.id.to_ne_bytes()); + buffer[4] = self.weight; + buffer[5] = self.resvd1; + buffer[6..8].copy_from_slice(&self.resvd2.to_ne_bytes()); + } +} + +impl<'a, T: AsRef<[u8]> + ?Sized> Parseable for NexthopGroupEntry { + fn parse(buf: &T) -> Result { + let buf = buf.as_ref(); + if buf.len() < 8 { + return Err(DecodeError::from("Invalid NexthopGroupEntry length")); + } + Ok(NexthopGroupEntry { + id: u32::from_ne_bytes(buf[0..4].try_into().unwrap()), + weight: buf[4], + resvd1: buf[5], + resvd2: u16::from_ne_bytes(buf[6..8].try_into().unwrap()), + }) + } +} diff --git a/src/nexthop/buffer.rs b/src/nexthop/buffer.rs new file mode 100644 index 00000000..7e384dc0 --- /dev/null +++ b/src/nexthop/buffer.rs @@ -0,0 +1,34 @@ +use netlink_packet_core::{NlaBuffer, NlasIterator}; + +use super::NexthopFlags; + +buffer!(NexthopMessageBuffer(8) { + family: (u8, 0), + scope: (u8, 1), + protocol: (u8, 2), + resvd: (u8, 3), + flags_raw: (u32, 4..8), + payload: (slice, 8..), +}); + +impl> NexthopMessageBuffer { + pub fn flags(&self) -> NexthopFlags { + NexthopFlags::from_bits_truncate(self.flags_raw()) + } +} + +impl + AsMut<[u8]>> NexthopMessageBuffer { + pub fn set_flags(&mut self, flags: NexthopFlags) { + self.set_flags_raw(flags.bits()); + } +} + +impl<'a, T: AsRef<[u8]> + ?Sized> NexthopMessageBuffer<&'a T> { + pub fn attributes( + &self, + ) -> impl Iterator< + Item = Result, netlink_packet_core::DecodeError>, + > { + NlasIterator::new(self.payload()) + } +} diff --git a/src/nexthop/header.rs b/src/nexthop/header.rs new file mode 100644 index 00000000..f80bb573 --- /dev/null +++ b/src/nexthop/header.rs @@ -0,0 +1,65 @@ +use netlink_packet_core::{DecodeError, Emitable, Parseable}; + +use super::NexthopMessageBuffer; +use crate::AddressFamily; + +#[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] +pub struct NexthopHeader { + pub family: AddressFamily, + pub scope: u8, + pub protocol: u8, + pub flags: NexthopFlags, +} + +impl Default for NexthopHeader { + fn default() -> Self { + Self { + family: AddressFamily::Unspec, + scope: 0, + protocol: 0, + flags: NexthopFlags::empty(), + } + } +} + +impl<'a, T: AsRef<[u8]> + 'a> Parseable> + for NexthopHeader +{ + fn parse(buf: &NexthopMessageBuffer<&'a T>) -> Result { + Ok(Self { + family: buf.family().into(), + scope: buf.scope(), + protocol: buf.protocol(), + flags: buf.flags(), + }) + } +} + +impl Emitable for NexthopHeader { + fn buffer_len(&self) -> usize { + 8 + } + + fn emit(&self, buffer: &mut [u8]) { + let mut buffer = NexthopMessageBuffer::new(buffer); + buffer.set_family(self.family.into()); + buffer.set_scope(self.scope); + buffer.set_protocol(self.protocol); + buffer.set_flags(self.flags); + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, Eq, PartialEq)] + #[non_exhaustive] + pub struct NexthopFlags: u32 { + const F_DEAD = 1; + const F_PERVASIVE = 2; + const F_ONLINK = 4; + const F_OFFLOAD = 8; + const F_LINKDOWN = 16; + const F_UNRESOLVED = 32; + const F_TRAP = 64; + } +} diff --git a/src/nexthop/message.rs b/src/nexthop/message.rs new file mode 100644 index 00000000..5037ef3f --- /dev/null +++ b/src/nexthop/message.rs @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT + +use netlink_packet_core::{ + DecodeError, Emitable, ErrorContext, Parseable, ParseableParametrized, +}; + +use super::{NexthopAttribute, NexthopHeader, NexthopMessageBuffer}; + +#[derive(Debug, PartialEq, Eq, Clone, Default)] +#[non_exhaustive] +pub struct NexthopMessage { + pub header: NexthopHeader, + pub nlas: Vec, +} + +impl Emitable for NexthopMessage { + fn buffer_len(&self) -> usize { + self.header.buffer_len() + self.nlas.as_slice().buffer_len() + } + + fn emit(&self, buffer: &mut [u8]) { + self.header.emit(buffer); + self.nlas + .as_slice() + .emit(&mut buffer[self.header.buffer_len()..]); + } +} + +impl<'a, T: AsRef<[u8]> + 'a> Parseable> + for NexthopMessage +{ + fn parse(buf: &NexthopMessageBuffer<&'a T>) -> Result { + let header = NexthopHeader::parse(buf) + .context("failed to parse nexthop message header")?; + Ok(NexthopMessage { + header, + nlas: Vec::::parse(buf) + .context("failed to parse nexthop message NLAs")?, + }) + } +} + +impl<'a, T: AsRef<[u8]> + 'a> Parseable> + for Vec +{ + fn parse(buf: &NexthopMessageBuffer<&'a T>) -> Result { + let mut nlas = vec![]; + for nla_buf in buf.attributes() { + let nla = nla_buf?; + nlas.push(NexthopAttribute::parse_with_param( + &(nla.value(), nla.kind()), + (), + )?); + } + Ok(nlas) + } +} diff --git a/src/nexthop/mod.rs b/src/nexthop/mod.rs new file mode 100644 index 00000000..adca612a --- /dev/null +++ b/src/nexthop/mod.rs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +mod attribute; +mod buffer; +mod header; +mod message; + +pub use self::{ + attribute::{NexthopAttribute, NexthopAttributeType, NexthopGroupEntry}, + buffer::NexthopMessageBuffer, + header::{NexthopFlags, NexthopHeader}, + message::NexthopMessage, +}; diff --git a/src/route/attribute.rs b/src/route/attribute.rs index b949fc9d..b0ca4cff 100644 --- a/src/route/attribute.rs +++ b/src/route/attribute.rs @@ -44,7 +44,7 @@ const RTA_TTL_PROPAGATE: u16 = 26; // const RTA_IP_PROTO:u16 = 27; // const RTA_SPORT:u16 = 28; // const RTA_DPORT:u16 = 29; -// const RTA_NH_ID:u16 = 30; +const RTA_NH_ID: u16 = 30; /// Netlink attributes for `RTM_NEWROUTE`, `RTM_DELROUTE`, /// `RTM_GETROUTE` netlink messages. @@ -81,6 +81,7 @@ pub enum RouteAttribute { Realm(RouteRealm), Table(u32), Mark(u32), + NextHopId(u32), Other(DefaultNla), } @@ -110,7 +111,8 @@ impl Nla for RouteAttribute { | Self::Oif(_) | Self::Priority(_) | Self::Table(_) - | Self::Mark(_) => 4, + | Self::Mark(_) + | Self::NextHopId(_) => 4, Self::MulticastExpires(_) => 8, Self::Other(attr) => attr.value_len(), } @@ -146,7 +148,8 @@ impl Nla for RouteAttribute { | Self::Oif(value) | Self::Priority(value) | Self::Table(value) - | Self::Mark(value) => emit_u32(buffer, *value).unwrap(), + | Self::Mark(value) + | Self::NextHopId(value) => emit_u32(buffer, *value).unwrap(), Self::Realm(v) => v.emit(buffer), Self::MulticastExpires(value) => emit_u64(buffer, *value).unwrap(), Self::Other(attr) => attr.emit_value(buffer), @@ -178,6 +181,7 @@ impl Nla for RouteAttribute { Self::MulticastExpires(_) => RTA_EXPIRES, Self::Uid(_) => RTA_UID, Self::TtlPropagate(_) => RTA_TTL_PROPAGATE, + Self::NextHopId(_) => RTA_NH_ID, Self::Other(ref attr) => attr.kind(), } } @@ -282,6 +286,9 @@ impl<'a, T: AsRef<[u8]> + ?Sized> RTA_MARK => Self::Mark( parse_u32(payload).context("invalid RTA_MARK value")?, ), + RTA_NH_ID => Self::NextHopId( + parse_u32(payload).context("invalid RTA_NH_ID value")?, + ), RTA_CACHEINFO => Self::CacheInfo( RouteCacheInfo::parse( From dfc76d732b9924c5efcacb8e29ec4d4b85becd7b Mon Sep 17 00:00:00 2001 From: nanamicat Date: Sat, 10 Jan 2026 22:19:30 +0800 Subject: [PATCH 2/2] fix --- src/nexthop/header.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/nexthop/header.rs b/src/nexthop/header.rs index f80bb573..5a856ccc 100644 --- a/src/nexthop/header.rs +++ b/src/nexthop/header.rs @@ -47,6 +47,7 @@ impl Emitable for NexthopHeader { buffer.set_scope(self.scope); buffer.set_protocol(self.protocol); buffer.set_flags(self.flags); + buffer.set_resvd(0); } }