[RFC PATCH 4/6] rust: net: add minimal rtnl registration and netlink tap support
From: Wenzhao Liao
Date: Thu Apr 02 2026 - 12:53:48 EST
Add the minimal pieces needed to register an rtnl link driver and to
attach or detach a netlink tap from a net_device.
The abstractions model kernel-owned registration lifetime and keep the
driver layer free of unsafe blocks.
Signed-off-by: Wenzhao Liao <wenzhaoliao@xxxxxxxxxx>
---
rust/kernel/net.rs | 2 +
rust/kernel/net/netlink_tap.rs | 89 +++++++++++++
rust/kernel/net/rtnl.rs | 221 +++++++++++++++++++++++++++++++++
3 files changed, 312 insertions(+)
create mode 100644 rust/kernel/net/netlink_tap.rs
create mode 100644 rust/kernel/net/rtnl.rs
diff --git a/rust/kernel/net.rs b/rust/kernel/net.rs
index a61bc76f4499..35459816c518 100644
--- a/rust/kernel/net.rs
+++ b/rust/kernel/net.rs
@@ -5,5 +5,7 @@
#[cfg(CONFIG_RUST_PHYLIB_ABSTRACTIONS)]
pub mod phy;
pub mod netdevice;
+pub mod netlink_tap;
+pub mod rtnl;
pub mod skbuff;
pub mod stats;
diff --git a/rust/kernel/net/netlink_tap.rs b/rust/kernel/net/netlink_tap.rs
new file mode 100644
index 000000000000..b26461937c6c
--- /dev/null
+++ b/rust/kernel/net/netlink_tap.rs
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Netlink tap lifecycle helpers.
+//!
+//! C header: [`include/linux/netlink.h`](srctree/include/linux/netlink.h)
+
+use crate::{
+ bindings,
+ error::to_result,
+ net::netdevice,
+ prelude::*,
+ types::Opaque,
+ ThisModule,
+};
+
+/// Owns a `struct netlink_tap` plus its registration state.
+#[pin_data]
+#[derive(Zeroable)]
+pub struct Tap {
+ #[pin]
+ inner: Opaque<bindings::netlink_tap>,
+ registered: bool,
+}
+
+impl Tap {
+ /// Creates an unregistered tap.
+ pub const fn new() -> Self {
+ Self {
+ inner: Opaque::zeroed(),
+ registered: false,
+ }
+ }
+
+ /// Returns whether the tap is currently registered.
+ pub fn is_registered(&self) -> bool {
+ self.registered
+ }
+
+ /// Registers the tap for the provided device.
+ pub fn add(
+ self: Pin<&mut Self>,
+ dev: &netdevice::Device,
+ module: &'static ThisModule,
+ ) -> Result {
+ // SAFETY: The caller pinned `self`, so accessing the interior through the stable address is
+ // valid for the duration of this method.
+ let this = unsafe { self.get_unchecked_mut() };
+
+ if this.registered {
+ return Err(EBUSY);
+ }
+
+ let tap = this.inner.get();
+
+ // SAFETY: `tap` points to valid storage for `struct netlink_tap`.
+ unsafe {
+ (*tap).dev = dev.as_ptr();
+ (*tap).module = module.as_ptr();
+ }
+
+ // SAFETY: `tap` points to a valid `struct netlink_tap`.
+ to_result(unsafe { bindings::netlink_add_tap(tap) })?;
+ this.registered = true;
+ Ok(())
+ }
+
+ /// Unregisters the tap if it is currently active.
+ pub fn remove(self: Pin<&mut Self>) -> Result {
+ // SAFETY: The caller pinned `self`, so accessing the interior through the stable address is
+ // valid for the duration of this method.
+ let this = unsafe { self.get_unchecked_mut() };
+
+ if !this.registered {
+ return Ok(());
+ }
+
+ let tap = this.inner.get();
+
+ // SAFETY: `self.inner` contains a valid `struct netlink_tap` previously passed to
+ // `netlink_add_tap`.
+ to_result(unsafe { bindings::netlink_remove_tap(tap) })?;
+ this.registered = false;
+
+ // SAFETY: The tap has been removed and `netlink_remove_tap` waited for in-flight users via
+ // `synchronize_net`, so restoring the zeroed unregistered state is valid.
+ unsafe { tap.write(core::mem::zeroed()) };
+ Ok(())
+ }
+}
diff --git a/rust/kernel/net/rtnl.rs b/rust/kernel/net/rtnl.rs
new file mode 100644
index 000000000000..f3bddd29874f
--- /dev/null
+++ b/rust/kernel/net/rtnl.rs
@@ -0,0 +1,221 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! RTNL link-type registration support.
+//!
+//! C headers:
+//! - [`include/net/rtnetlink.h`](srctree/include/net/rtnetlink.h)
+//! - [`include/uapi/linux/if_link.h`](srctree/include/uapi/linux/if_link.h)
+
+use crate::{
+ bindings,
+ error::{from_result, to_result},
+ net::netdevice,
+ prelude::*,
+ types::Opaque,
+};
+
+use core::{
+ marker::PhantomData,
+ mem::size_of,
+};
+
+/// Typed link-level netlink attribute selector.
+#[derive(Clone, Copy)]
+pub struct LinkAttr(usize);
+
+impl LinkAttr {
+ /// Equivalent to `IFLA_ADDRESS`.
+ pub const ADDRESS: Self = Self(bindings::IFLA_ADDRESS as usize);
+
+ const fn as_index(self) -> usize {
+ self.0
+ }
+}
+
+/// Typed info-level netlink attribute selector.
+#[derive(Clone, Copy)]
+pub struct InfoAttr(usize);
+
+impl InfoAttr {
+ /// Equivalent to `IFLA_INFO_KIND`.
+ pub const KIND: Self = Self(bindings::IFLA_INFO_KIND as usize);
+
+ /// Equivalent to `IFLA_INFO_DATA`.
+ pub const DATA: Self = Self(bindings::IFLA_INFO_DATA as usize);
+
+ const fn as_index(self) -> usize {
+ self.0
+ }
+}
+
+const LINK_ATTR_TABLE_LEN: usize = bindings::__IFLA_MAX as usize;
+const INFO_ATTR_TABLE_LEN: usize = bindings::__IFLA_INFO_MAX as usize;
+
+/// Validation context for `rtnl_link_ops::validate`.
+pub struct ValidateContext<'a> {
+ link_attrs: NlAttrTable,
+ data_attrs: NlAttrTable,
+ extack: Option<&'a mut ExtAck>,
+}
+
+impl<'a> ValidateContext<'a> {
+ fn new(
+ link_attrs: NlAttrTable,
+ data_attrs: NlAttrTable,
+ extack: Option<&'a mut ExtAck>,
+ ) -> Self {
+ Self {
+ link_attrs,
+ data_attrs,
+ extack,
+ }
+ }
+
+ /// Returns whether a link-level netlink attribute is present.
+ pub fn has_link_attr(&self, attr: LinkAttr) -> bool {
+ self.link_attrs.has_attr_index(attr.as_index())
+ }
+
+ /// Returns whether an info-level netlink attribute is present.
+ pub fn has_info_attr(&self, attr: InfoAttr) -> bool {
+ self.data_attrs.has_attr_index(attr.as_index())
+ }
+
+ /// Returns the optional extack wrapper.
+ pub fn extack(&mut self) -> Option<&mut ExtAck> {
+ self.extack.as_deref_mut()
+ }
+}
+
+/// Safe view over the `struct nlattr *tb[]` / `data[]` arrays passed to validate callbacks.
+#[derive(Clone, Copy)]
+pub struct NlAttrTable {
+ raw: *mut *mut bindings::nlattr,
+ len: usize,
+}
+
+impl NlAttrTable {
+ fn new(raw: *mut *mut bindings::nlattr, len: usize) -> Self {
+ Self { raw, len }
+ }
+
+ /// Returns `true` if the attribute slot contains a non-null pointer.
+ fn has_attr_index(&self, attr: usize) -> bool {
+ if self.raw.is_null() || attr >= self.len {
+ return false;
+ }
+
+ // SAFETY: The RTNL core provides these arrays for validate callbacks. Indexing is kept in
+ // the abstraction layer so driver code does not perform raw pointer arithmetic.
+ unsafe { !(*self.raw.add(attr)).is_null() }
+ }
+
+}
+
+/// Wrapper over `struct netlink_ext_ack`.
+#[repr(transparent)]
+pub struct ExtAck(Opaque<bindings::netlink_ext_ack>);
+
+impl ExtAck {
+ /// Creates a mutable wrapper from a raw extack pointer.
+ ///
+ /// # Safety
+ ///
+ /// The pointer must be valid for the returned lifetime.
+ pub unsafe fn from_raw<'a>(ptr: *mut bindings::netlink_ext_ack) -> &'a mut Self {
+ let ptr = ptr.cast::<Self>();
+ // SAFETY: The caller guarantees validity for the returned lifetime.
+ unsafe { &mut *ptr }
+ }
+}
+
+/// A Rust RTNL link-type driver.
+pub trait Driver: netdevice::Operations {
+ /// The RTNL link kind, e.g. `"nlmon"`.
+ const KIND: &'static CStr;
+
+ /// Performs link-type setup.
+ fn setup(dev: &mut netdevice::Device);
+
+ /// Optional netlink validation.
+ fn validate(_ctx: &mut ValidateContext<'_>) -> Result {
+ Ok(())
+ }
+}
+
+/// Owns an RTNL link-type registration.
+#[pin_data(PinnedDrop)]
+pub struct Registration<T: Driver> {
+ #[pin]
+ ops: Opaque<bindings::rtnl_link_ops>,
+ _driver: PhantomData<T>,
+}
+
+// SAFETY: Shared references do not expose interior mutation beyond drop semantics.
+unsafe impl<T: Driver> Sync for Registration<T> {}
+
+// SAFETY: Registration and unregistration are handled by RTNL core code and can be performed from
+// the module init/exit path.
+unsafe impl<T: Driver> Send for Registration<T> {}
+
+impl<T: Driver> Registration<T> {
+ extern "C" fn setup_callback(dev: *mut bindings::net_device) {
+ // SAFETY: The RTNL core only calls setup with a valid `net_device`.
+ let dev = unsafe { netdevice::Device::from_raw(dev) };
+ dev.set_netdevice_ops::<T>();
+ T::setup(dev);
+ }
+
+ extern "C" fn validate_callback(
+ tb: *mut *mut bindings::nlattr,
+ data: *mut *mut bindings::nlattr,
+ extack: *mut bindings::netlink_ext_ack,
+ ) -> c_int {
+ from_result(|| {
+ let extack = if extack.is_null() {
+ None
+ } else {
+ // SAFETY: The RTNL core passes a valid extack pointer when non-null.
+ Some(unsafe { ExtAck::from_raw(extack) })
+ };
+ let mut ctx = ValidateContext::new(
+ NlAttrTable::new(tb, LINK_ATTR_TABLE_LEN),
+ NlAttrTable::new(data, INFO_ATTR_TABLE_LEN),
+ extack,
+ );
+ T::validate(&mut ctx)?;
+ Ok(0)
+ })
+ }
+
+ /// Creates a new RTNL registration object.
+ pub fn new() -> impl PinInit<Self, Error> {
+ build_assert!(!core::mem::needs_drop::<T::Private>());
+ try_pin_init!(Self {
+ ops <- Opaque::try_ffi_init(|ptr: *mut bindings::rtnl_link_ops| {
+ // SAFETY: All-zero is a valid initial state for the opaque RTNL ops structure.
+ unsafe { ptr.write(core::mem::zeroed()) };
+
+ // SAFETY: `ptr` is valid for writes for the duration of this initializer.
+ unsafe {
+ (*ptr).kind = T::KIND.as_char_ptr();
+ (*ptr).priv_size = size_of::<T::Private>();
+ (*ptr).setup = Some(Self::setup_callback);
+ (*ptr).validate = Some(Self::validate_callback);
+ }
+
+ // SAFETY: `ptr` now points to a fully initialized `rtnl_link_ops`.
+ to_result(unsafe { bindings::rtnl_link_register(ptr) })
+ }),
+ _driver: PhantomData,
+ })
+ }
+}
+
+#[pinned_drop]
+impl<T: Driver> PinnedDrop for Registration<T> {
+ fn drop(self: Pin<&mut Self>) {
+ // SAFETY: The existence of `self` guarantees a successful earlier registration.
+ unsafe { bindings::rtnl_link_unregister(self.ops.get()) };
+ }
+}
--
2.34.1