diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1001b6c..09935e6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -122,6 +122,12 @@ jobs: echo echo "Testing ${D}" echo + + if make BOARD=native info-modules |grep -q netdev_tap; then + # Seems we can't have tap interfaces on GitHub actions, aborting. + echo "Board requires tap interface, skipping." + exit 0 + fi make all test BOARD=native fi else diff --git a/build.rs b/build.rs index f50c6a4..90e1ca2 100644 --- a/build.rs +++ b/build.rs @@ -74,6 +74,7 @@ fn main() { "gcoap", "gnrc", "gnrc_icmpv6", + "gnrc_ipv6_nib", "gnrc_pktbuf", "gnrc_udp", "ipv6", diff --git a/src/gnrc/mod.rs b/src/gnrc/mod.rs index 0255226..41c5296 100644 --- a/src/gnrc/mod.rs +++ b/src/gnrc/mod.rs @@ -5,6 +5,8 @@ pub mod ipv6; pub mod netapi; pub mod netreg; +#[cfg(riot_module_gnrc_ipv6_nib)] +pub mod nib; #[deprecated(note = "Internally, use gnrc_pktbuf directly")] pub(crate) use crate::gnrc_pktbuf as pktbuf; diff --git a/src/gnrc/nib.rs b/src/gnrc/nib.rs new file mode 100644 index 0000000..ac56f26 --- /dev/null +++ b/src/gnrc/nib.rs @@ -0,0 +1,183 @@ +/// A single entry in the neighbor cache. +/// +/// These can be obtained by iterating +pub struct NcEntry(riot_sys::gnrc_ipv6_nib_nc_t); + +/// Neighbor Unreachability Detection state +/// +/// See +/// https://doc.riot-os.org/group__net__gnrc__ipv6__nib__nc.html +/// for more detailed semantics +// FIXME can we pull doc from riot_sys? +#[derive(Debug)] +pub enum NudState { + Unmanaged, + Unreachable, + Incomplete, + Stale, + Delay, + Probe, + Reachable, +} + +impl NudState { + fn from_c(input: riot_sys::libc::c_uint) -> Option { + Some(match input { + riot_sys::GNRC_IPV6_NIB_NC_INFO_NUD_STATE_UNMANAGED => NudState::Unmanaged, + riot_sys::GNRC_IPV6_NIB_NC_INFO_NUD_STATE_UNREACHABLE => NudState::Unreachable, + riot_sys::GNRC_IPV6_NIB_NC_INFO_NUD_STATE_INCOMPLETE => NudState::Incomplete, + riot_sys::GNRC_IPV6_NIB_NC_INFO_NUD_STATE_STALE => NudState::Stale, + riot_sys::GNRC_IPV6_NIB_NC_INFO_NUD_STATE_DELAY => NudState::Delay, + riot_sys::GNRC_IPV6_NIB_NC_INFO_NUD_STATE_PROBE => NudState::Probe, + riot_sys::GNRC_IPV6_NIB_NC_INFO_NUD_STATE_REACHABLE => NudState::Reachable, + _ => return None, + }) + } + + /// Returns a plain text label of the state. + /// + /// This is equivalent to debug (except for capitalization), but more versatile in its use due + /// to its type. + pub fn label(&self) -> &'static str { + match self { + NudState::Unmanaged => "managed", + NudState::Unreachable => "unreachable", + NudState::Incomplete => "incomplete", + NudState::Stale => "stale", + NudState::Delay => "delay", + NudState::Probe => "probe", + NudState::Reachable => "reachable", + } + } +} + +/// 6LoWPAN address registration (6Lo-AR) state +/// +/// See +/// https://doc.riot-os.org/group__net__gnrc__ipv6__nib__nc.html +/// for more detailed semantics +// FIXME can we pull doc from riot_sys? +#[derive(Debug)] +pub enum ArState { + Gc, + Tentative, + Registered, + Manual, +} + +impl ArState { + fn from_c(input: riot_sys::libc::c_uint) -> Option { + Some(match input { + riot_sys::GNRC_IPV6_NIB_NC_INFO_AR_STATE_GC => ArState::Gc, + riot_sys::GNRC_IPV6_NIB_NC_INFO_AR_STATE_TENTATIVE => ArState::Tentative, + riot_sys::GNRC_IPV6_NIB_NC_INFO_AR_STATE_REGISTERED => ArState::Registered, + riot_sys::GNRC_IPV6_NIB_NC_INFO_AR_STATE_MANUAL => ArState::Manual, + _ => return None, + }) + } + + /// Returns a plain text label of the state. + /// + /// This is equivalent to debug (except for capitalization), but more versatile in its use due + /// to its type. + pub fn label(&self) -> &'static str { + match self { + ArState::Gc => "GC", + ArState::Tentative => "tentative", + ArState::Registered => "registered", + ArState::Manual => "manual", + } + } +} + +impl NcEntry { + /// Iterate over the Neighbor Cache. + #[doc(alias = "gnrc_ipv6_nib_nc_iter")] + pub fn all() -> impl Iterator { + // If we add anything like all_nc_entries_on_interface(): + // // Interfaces are positive numbers; MAX is clearly out of range and allows us to have an easier + // // input type + // let interface = interface.map(|i| { + // riot_sys::libc::c_uint::try_from(usize::from(i)).unwrap_or(riot_sys::libc::c_uint::MAX) + // }); + + any_nc_query(0) + } + + pub fn l2addr(&self) -> &[u8] { + &self.0.l2addr[..self.0.l2addr_len as usize] + } + + pub fn ipv6_addr(&self) -> &crate::gnrc::ipv6::Address { + // unsafe: It's repr(transparent) around it + unsafe { core::mem::transmute(&self.0.ipv6) } + } + + #[doc(alias = "gnrc_ipv6_nib_nc_get_iface")] + pub fn iface(&self) -> Option> { + const { + assert!(riot_sys::KERNEL_PID_UNDEF == 0, "Interface lookup mixes unspecified interface with PIDs and thus relies on the unspecified latter being 0.") + }; + let interface = unsafe { + riot_sys::inline::gnrc_ipv6_nib_nc_get_iface(crate::inline_cast_ref(&self.0)) + }; + // Let's not get into size discussions + let interface = interface as usize; + interface.try_into().ok() + } + + #[doc(alias = "gnrc_ipv6_nib_nc_is_router")] + pub fn is_router(&self) -> bool { + unsafe { riot_sys::inline::gnrc_ipv6_nib_nc_is_router(crate::inline_cast_ref(&self.0)) } + } + + /// Access the entry's Neighbor Unreachability Detection (NUD) state + /// + /// This is None if the interface's NUD state is invalid (including values introduced to RIOT + /// OS but not known to riot-wrappers). + pub fn nud_state(&self) -> Option { + let result = NudState::from_c(unsafe { + riot_sys::inline::gnrc_ipv6_nib_nc_get_nud_state(crate::inline_cast_ref(&self.0)) + }); + result + } + + /// Access the entry's 6LoWPAN address registration (6Lo-AR) state + /// + /// This is None if the interface's neighbor state is invalid (including values + /// introduced to RIOT OS but not known to riot-wrappers). + pub fn ar_state(&self) -> Option { + let result = ArState::from_c(unsafe { + riot_sys::inline::gnrc_ipv6_nib_nc_get_ar_state(crate::inline_cast_ref(&self.0)) + }); + result + } +} + +struct NcIterator { + interface: riot_sys::libc::c_uint, + state: *mut riot_sys::libc::c_void, +} + +impl Iterator for NcIterator { + type Item = NcEntry; + + fn next(&mut self) -> Option { + let mut nc_entry = core::mem::MaybeUninit::::uninit(); + if unsafe { + riot_sys::gnrc_ipv6_nib_nc_iter(self.interface, &mut self.state, nc_entry.as_mut_ptr()) + } { + let nc_entry = NcEntry(unsafe { nc_entry.assume_init() }); + Some(nc_entry) + } else { + None + } + } +} + +fn any_nc_query(interface: riot_sys::libc::c_uint) -> impl Iterator { + NcIterator { + interface, + state: core::ptr::null_mut(), + } +} diff --git a/src/gnrc_util.rs b/src/gnrc_util.rs index f468730..f0f5182 100644 --- a/src/gnrc_util.rs +++ b/src/gnrc_util.rs @@ -11,9 +11,7 @@ use crate::thread::KernelPID; #[cfg(riot_module_gnrc_udp)] use riot_sys::gnrc_nettype_t_GNRC_NETTYPE_UDP as GNRC_NETTYPE_UDP; -use riot_sys::{ - gnrc_netif_hdr_t, gnrc_nettype_t_GNRC_NETTYPE_NETIF as GNRC_NETTYPE_NETIF, udp_hdr_t, -}; +use riot_sys::{gnrc_netif_hdr_t, gnrc_nettype_t_GNRC_NETTYPE_NETIF as GNRC_NETTYPE_NETIF}; /// Trait of data structures that store all the information needed to respond to a Pktsnip in some /// way; the data (typically address and port information) is copied into the trait implementation @@ -110,7 +108,7 @@ impl RoundtripData for UDPRoundtripDataFull { let (src, dst) = incoming .search_type(GNRC_NETTYPE_UDP) .map(|s| { - let hdr: &udp_hdr_t = unsafe { &*(s.data.as_ptr() as *const _) }; + let hdr: &riot_sys::udp_hdr_t = unsafe { &*(s.data.as_ptr() as *const _) }; ( u16::from_be_bytes(unsafe { (*hdr).src_port.u8_ }), u16::from_be_bytes(unsafe { (*hdr).dst_port.u8_ }), diff --git a/tests/network-properties/Cargo.toml b/tests/network-properties/Cargo.toml new file mode 100644 index 0000000..02831c7 --- /dev/null +++ b/tests/network-properties/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "riot-wrappers-test-networkproperties" +version = "0.1.0" +authors = ["Christian Amsüss "] +edition = "2021" +publish = false + +[lib] +crate-type = ["staticlib"] + +[profile.release] +panic = "abort" + +[dependencies] +riot-wrappers = { path = "../..", features = [ "set_panic_handler", "panic_handler_format" ] } diff --git a/tests/network-properties/Makefile b/tests/network-properties/Makefile new file mode 100644 index 0000000..f8ef21f --- /dev/null +++ b/tests/network-properties/Makefile @@ -0,0 +1,21 @@ +# name of your application +APPLICATION = riot-wrappers-test-networkproperties +BOARD ?= native +APPLICATION_RUST_MODULE = riot_wrappers_test_networkproperties +BASELIBS += $(APPLICATION_RUST_MODULE).module +FEATURES_REQUIRED += rust_target + +# This may not be a GNRC netdev in all cases; when it is not, the test will +# break, and that will be the time to split it. So far, also IPv6 support can +# just be assumed. (Really, anyone writing an IoT application without IPv6 +# support may want to look into Cobol). +USEMODULE += netdev_default +USEMODULE += auto_init_gnrc_netif +USEMODULE += gnrc_ipv6_default +# This is an easy way to visibly populate entries into the neighbor cache: ping +# the RIOT instance. +USEMODULE += gnrc_icmpv6_echo + +USEMODULE += ztimer_msec + +include $(RIOTBASE)/Makefile.include diff --git a/tests/network-properties/src/lib.rs b/tests/network-properties/src/lib.rs new file mode 100644 index 0000000..0de78f7 --- /dev/null +++ b/tests/network-properties/src/lib.rs @@ -0,0 +1,41 @@ +#![no_std] + +use riot_wrappers::println; +use riot_wrappers::riot_main; + +riot_main!(main); + +fn main() { + use riot_wrappers::ztimer::*; + + let msec = Clock::msec(); + + loop { + for netif in riot_wrappers::gnrc::Netif::all() { + println!( + "Netif at PID {:?} with link-layer addr {:?}", + netif.pid(), + netif.l2addr() + ); + for addr in &netif.ipv6_addrs().unwrap() { + println!("- Address {:?}", addr); + } + } + + println!("Cache entries:"); + for cache_entry in riot_wrappers::gnrc::nib::NcEntry::all() { + println!( + "- on interface {:?}: {:02x?} <=> {:?}, router? {:?}, NUD {:?}, AR {:?}", + cache_entry.iface(), + cache_entry.l2addr(), + cache_entry.ipv6_addr(), + cache_entry.is_router(), + cache_entry.nud_state(), + cache_entry.ar_state() + ); + } + println!(""); + + msec.sleep(Ticks(300)); + } +} diff --git a/tests/network-properties/tests/01-run.py b/tests/network-properties/tests/01-run.py new file mode 100755 index 0000000..028aab6 --- /dev/null +++ b/tests/network-properties/tests/01-run.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 + +import os +import sys +from testrunner import run + +def test(child): + # Cant' make any predictions about network addresses, but showing them + # should not crash. + for _ in range(3): + child.expect("Netif at ") + child.expect("Cache entries") + +if __name__ == "__main__": + sys.exit(run(test))