Skip to content

Add support for LWTUNNEL_ENCAP_IP6 #148

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions src/route/lwtunnel.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
// SPDX-License-Identifier: MIT

use std::{fmt::Debug, net::Ipv6Addr};

use anyhow::Context;
use byteorder::{BigEndian, ByteOrder, NetworkEndian};
use netlink_packet_utils::{
nla::{DefaultNla, Nla, NlaBuffer, NlasIterator},
parsers::{parse_u16_be, parse_u8},
traits::{Emitable, Parseable, ParseableParametrized},
DecodeError,
};

use crate::ip::parse_ipv6_addr;

use super::{RouteMplsIpTunnel, RouteSeg6IpTunnel};

const LWTUNNEL_ENCAP_NONE: u16 = 0;
Expand All @@ -21,6 +27,20 @@ const LWTUNNEL_ENCAP_RPL: u16 = 8;
const LWTUNNEL_ENCAP_IOAM6: u16 = 9;
const LWTUNNEL_ENCAP_XFRM: u16 = 10;

const LWTUNNEL_IP6_UNSPEC: u16 = 0;
const LWTUNNEL_IP6_ID: u16 = 1;
const LWTUNNEL_IP6_DST: u16 = 2;
const LWTUNNEL_IP6_SRC: u16 = 3;
const LWTUNNEL_IP6_HOPLIMIT: u16 = 4;
const LWTUNNEL_IP6_TC: u16 = 5;
const LWTUNNEL_IP6_FLAGS: u16 = 6;
//const LWTUNNEL_IP6_PAD: u16 = 7;
//const LWTUNNEL_IP6_OPTS: u16 = 8;

const IP_TUNNEL_CSUM_BIT: u16 = 1;
const IP_TUNNEL_KEY_BIT: u16 = 4;
const IP_TUNNEL_SEQ_BIT: u16 = 8;

#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
#[non_exhaustive]
pub enum RouteLwEnCapType {
Expand Down Expand Up @@ -106,11 +126,156 @@ impl std::fmt::Display for RouteLwEnCapType {
}
}

#[derive(Debug, PartialEq, Eq, Clone, Default)]
#[non_exhaustive]
pub enum RouteIp6Tunnel {
#[default]
Unspecified,
Id(u64),
Destination(Ipv6Addr),
Source(Ipv6Addr),
Hoplimit(u8),
Tc(u8),
Flags(RouteIp6TunnelFlags),
Other(DefaultNla),
}

bitflags! {
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RouteIp6TunnelFlags : u16 {
const Key = IP_TUNNEL_KEY_BIT;
const Checksum = IP_TUNNEL_CSUM_BIT;
const Sequence = IP_TUNNEL_SEQ_BIT;
const _ = !0;
}
}

impl std::fmt::Display for RouteIp6Tunnel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unspecified => write!(f, "unspecified"),
Self::Id(id) => write!(f, "id {id}"),
Self::Destination(dst) => write!(f, "dst {dst}"),
Self::Source(src) => write!(f, "src, {src}"),
Self::Hoplimit(hoplimit) => write!(f, "hoplimit {hoplimit}"),
Self::Tc(tc) => write!(f, "tc {tc}"),
Self::Flags(flags) => {
if flags.contains(RouteIp6TunnelFlags::Key) {
write!(f, "key ")?;
}
if flags.contains(RouteIp6TunnelFlags::Checksum) {
write!(f, "csum ")?;
}

if flags.contains(RouteIp6TunnelFlags::Sequence) {
write!(f, "seq ")?;
}

Ok(())
}
Self::Other(other) => other.fmt(f),
}
}
}

impl Nla for RouteIp6Tunnel {
fn value_len(&self) -> usize {
match self {
Self::Unspecified => 0,
Self::Id(_) => const { size_of::<u64>() },
Self::Destination(_) => const { size_of::<Ipv6Addr>() },
Self::Source(_) => const { size_of::<Ipv6Addr>() },
Self::Hoplimit(_) => const { size_of::<u8>() },
Self::Tc(_) => const { size_of::<u8>() },
Self::Flags(_) => const { size_of::<u16>() },
Self::Other(_) => const { size_of::<DefaultNla>() },
}
}

fn kind(&self) -> u16 {
match self {
Self::Unspecified => LWTUNNEL_IP6_UNSPEC,
Self::Id(_) => LWTUNNEL_IP6_ID,
Self::Destination(_) => LWTUNNEL_IP6_DST,
Self::Source(_) => LWTUNNEL_IP6_SRC,
Self::Hoplimit(_) => LWTUNNEL_IP6_HOPLIMIT,
Self::Tc(_) => LWTUNNEL_IP6_TC,
Self::Flags(_) => LWTUNNEL_IP6_FLAGS,
Self::Other(other) => other.kind(),
}
}

fn emit_value(&self, buffer: &mut [u8]) {
match self {
Self::Unspecified => {}
Self::Id(id) => NetworkEndian::write_u64(buffer, *id),
Self::Destination(ip) | Self::Source(ip) => {
buffer.copy_from_slice(&ip.octets());
}
Self::Hoplimit(value) | Self::Tc(value) => buffer[0] = *value,
Self::Flags(flags) => BigEndian::write_u16(buffer, flags.bits()),
Self::Other(other) => other.emit_value(buffer),
}
}
}

// should probably be in utils
fn parse_u64_be(payload: &[u8]) -> Result<u64, DecodeError> {
if payload.len() != size_of::<u64>() {
return Err(format!("invalid u64: {payload:?}").into());
}
Ok(BigEndian::read_u64(payload))
}

impl<'a, T> Parseable<NlaBuffer<&'a T>> for RouteIp6Tunnel
where
T: AsRef<[u8]> + ?Sized,
{
fn parse(buf: &NlaBuffer<&'a T>) -> Result<Self, DecodeError> {
let payload = buf.value();
Ok(match buf.kind() {
LWTUNNEL_IP6_UNSPEC => Self::Unspecified,
LWTUNNEL_IP6_ID => Self::Id(
parse_u64_be(payload)
.context("invalid LWTUNNEL_IP6_ID value")?,
),
LWTUNNEL_IP6_DST => Self::Destination(
parse_ipv6_addr(payload)
.context("invalid LWTUNNEL_IP6_DST value")?,
),
LWTUNNEL_IP6_SRC => Self::Source(
parse_ipv6_addr(payload)
.context("invalid LWTUNNEL_IP6_SRC value")?,
),
LWTUNNEL_IP6_HOPLIMIT => Self::Hoplimit(
parse_u8(payload)
.context("invalid LWTUNNEL_IP6_HOPLIMIT value")?,
),
LWTUNNEL_IP6_TC => Self::Tc(
parse_u8(payload).context("invalid LWTUNNEL_IP6_TC value")?,
),
LWTUNNEL_IP6_FLAGS => {
Self::Flags(RouteIp6TunnelFlags::from_bits_retain(
parse_u16_be(payload)
.context("invalid LWTUNNEL_IP6_FLAGS value")?,
))
}
_ => {
return Err(DecodeError::from(
Copy link
Member

@cathay4t cathay4t Jun 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of return error on unknown LWTUNNEL_IP6_XXX, please use RouteIp6Tunnel::Other to store it. We have many existing code doing that, just copy them.

"invalid NLA value (unknown type) value",
))
}
})
}
}

#[derive(Debug, PartialEq, Eq, Clone)]
#[non_exhaustive]
pub enum RouteLwTunnelEncap {
Mpls(RouteMplsIpTunnel),
Seg6(RouteSeg6IpTunnel),
Ip6(RouteIp6Tunnel),
Other(DefaultNla),
}

Expand All @@ -119,6 +284,7 @@ impl Nla for RouteLwTunnelEncap {
match self {
Self::Mpls(v) => v.value_len(),
Self::Seg6(v) => v.value_len(),
Self::Ip6(v) => v.value_len(),
Self::Other(v) => v.value_len(),
}
}
Expand All @@ -127,6 +293,7 @@ impl Nla for RouteLwTunnelEncap {
match self {
Self::Mpls(v) => v.emit_value(buffer),
Self::Seg6(v) => v.emit_value(buffer),
Self::Ip6(v) => v.emit_value(buffer),
Self::Other(v) => v.emit_value(buffer),
}
}
Expand All @@ -135,6 +302,7 @@ impl Nla for RouteLwTunnelEncap {
match self {
Self::Mpls(v) => v.kind(),
Self::Seg6(v) => v.kind(),
Self::Ip6(v) => v.kind(),
Self::Other(v) => v.kind(),
}
}
Expand All @@ -156,6 +324,7 @@ where
RouteLwEnCapType::Seg6 => {
Self::Seg6(RouteSeg6IpTunnel::parse(buf)?)
}
RouteLwEnCapType::Ip6 => Self::Ip6(RouteIp6Tunnel::parse(buf)?),
_ => Self::Other(DefaultNla::parse(buf)?),
})
}
Expand Down
4 changes: 3 additions & 1 deletion src/route/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ pub use self::cache_info::{RouteCacheInfo, RouteCacheInfoBuffer};
pub use self::header::{
RouteHeader, RouteMessageBuffer, RouteProtocol, RouteScope, RouteType,
};
pub use self::lwtunnel::{RouteLwEnCapType, RouteLwTunnelEncap};
pub use self::lwtunnel::{
RouteIp6Tunnel, RouteLwEnCapType, RouteLwTunnelEncap,
};
pub use self::message::RouteMessage;
pub use self::metrics::RouteMetric;
pub use self::mfc_stats::{RouteMfcStats, RouteMfcStatsBuffer};
Expand Down
84 changes: 84 additions & 0 deletions src/route/tests/ip6_tunnel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use std::net::{Ipv4Addr, Ipv6Addr};
use std::str::FromStr;

use netlink_packet_utils::{Emitable, Parseable};

use crate::route::lwtunnel::RouteIp6TunnelFlags;
use crate::route::{
RouteAttribute, RouteFlags, RouteHeader, RouteIp6Tunnel, RouteLwEnCapType,
RouteLwTunnelEncap, RouteMessage, RouteMessageBuffer, RouteProtocol,
RouteScope, RouteType,
};
use crate::AddressFamily;

// Setup:
// ip link add dummy1 type dummy
// ip link set dummy1 up
// ip route add 192.0.2.0/24 encap ip6 \
// dst 2001:db8:1::1 src 2001:db8:1::2 \
// id 100 tc 7 hoplimit 253 csum dev dummy1
// wireshark capture(netlink message header removed) of nlmon against command:
// ip route show dev dummy1
#[test]
fn test_ip6_tunnel() {
let raw = vec![
0x02, 0x18, 0x00, 0x00, 0xfe, 0x03, 0xfd, 0x01, 0x00, 0x00, 0x00, 0x00,
0x08, 0x00, 0x0f, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x08, 0x00, 0x01, 0x00,
0xc0, 0x00, 0x02, 0x00, 0x08, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00,
0x50, 0x00, 0x16, 0x00, 0x0c, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x64, 0x14, 0x00, 0x02, 0x00, 0x20, 0x01, 0x0d, 0xb8,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x14, 0x00, 0x03, 0x00, 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x05, 0x00, 0x05, 0x00,
0x07, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xfd, 0x00, 0x00, 0x00,
0x06, 0x00, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x06, 0x00, 0x15, 0x00,
0x04, 0x00, 0x00, 0x00,
];

let expected = RouteMessage {
header: RouteHeader {
address_family: AddressFamily::Inet,
destination_prefix_length: 24,
source_prefix_length: 0,
tos: 0,
table: 254,
protocol: RouteProtocol::Boot,
scope: RouteScope::Link,
kind: RouteType::Unicast,
flags: RouteFlags::empty(),
},
attributes: vec![
RouteAttribute::Table(254),
RouteAttribute::Destination(
Ipv4Addr::from_str("192.0.2.0").unwrap().into(),
),
RouteAttribute::Oif(8),
RouteAttribute::Encap(vec![
RouteLwTunnelEncap::Ip6(RouteIp6Tunnel::Id(100)),
RouteLwTunnelEncap::Ip6(RouteIp6Tunnel::Destination(
Ipv6Addr::from_str("2001:db8:1::1").unwrap(),
)),
RouteLwTunnelEncap::Ip6(RouteIp6Tunnel::Source(
Ipv6Addr::from_str("2001:db8:1::2").unwrap(),
)),
RouteLwTunnelEncap::Ip6(RouteIp6Tunnel::Tc(7)),
RouteLwTunnelEncap::Ip6(RouteIp6Tunnel::Hoplimit(253)),
RouteLwTunnelEncap::Ip6(RouteIp6Tunnel::Flags(
RouteIp6TunnelFlags::Checksum,
)),
]),
RouteAttribute::EncapType(RouteLwEnCapType::Ip6),
],
};

assert_eq!(
expected,
RouteMessage::parse(&RouteMessageBuffer::new(&raw)).unwrap()
);

let mut buf = vec![0; expected.buffer_len()];

expected.emit(&mut buf);

assert_eq!(buf, raw);
}
2 changes: 2 additions & 0 deletions src/route/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ mod cache_info;
#[cfg(test)]
mod expires;
#[cfg(test)]
mod ip6_tunnel;
#[cfg(test)]
mod loopback;
#[cfg(test)]
mod mpls;
Expand Down