From ef2e3e7ffddc7ef64289b8a98d27c86e1ab02d7a Mon Sep 17 00:00:00 2001 From: Daniel Noland Date: Tue, 24 Mar 2026 19:25:19 -0600 Subject: [PATCH 01/17] chore(net): remove kani annotations Remove all kani proof annotations and cfg(kani) guards from net crate tests. Remove the [lints.rust] unexpected_cfgs kani check-cfg from net/Cargo.toml. Kani is not actively used and these annotations add noise. Co-authored-by: Claude (Opus 4.6) Signed-off-by: Daniel Noland --- net/Cargo.toml | 3 --- net/src/icmp6/mod.rs | 2 -- net/src/ipv4/mod.rs | 4 ---- net/src/ipv6/addr.rs | 1 - net/src/ipv6/mod.rs | 4 ---- net/src/tcp/mod.rs | 5 ----- net/src/udp/mod.rs | 5 ----- net/src/vlan/mod.rs | 6 ------ net/src/vxlan/mod.rs | 6 ------ 9 files changed, 36 deletions(-) diff --git a/net/Cargo.toml b/net/Cargo.toml index 5cf3dc327..26ae27a4b 100644 --- a/net/Cargo.toml +++ b/net/Cargo.toml @@ -37,6 +37,3 @@ tracing = { workspace = true } ahash = { workspace = true, features = ["no-rng"] } bolero = { workspace = true, features = ["alloc", "arbitrary", "std"] } etherparse = { workspace = true, default-features = false, features = ["std"] } - -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(kani)'] } diff --git a/net/src/icmp6/mod.rs b/net/src/icmp6/mod.rs index f646e257b..e6d460372 100644 --- a/net/src/icmp6/mod.rs +++ b/net/src/icmp6/mod.rs @@ -475,7 +475,6 @@ mod test { } #[test] - #[cfg_attr(kani, kani::proof)] fn parse_back() { bolero::check!() .with_type() @@ -483,7 +482,6 @@ mod test { } #[test] - #[cfg_attr(kani, kani::proof)] fn parse_arbitrary_bytes() { bolero::check!() .with_type() diff --git a/net/src/ipv4/mod.rs b/net/src/ipv4/mod.rs index d30b53dcf..54e9ac885 100644 --- a/net/src/ipv4/mod.rs +++ b/net/src/ipv4/mod.rs @@ -525,7 +525,6 @@ mod test { const MAX_LEN_USIZE: usize = 60; #[test] - #[cfg_attr(kani, kani::proof)] fn parse_back() { bolero::check!().with_type().for_each(|header: &Ipv4| { let mut buffer = [0u8; MIN_LEN_USIZE]; @@ -540,14 +539,12 @@ mod test { assert_eq!(header.protocol(), parse_back.protocol()); assert_eq!(header.ecn(), parse_back.ecn()); assert_eq!(header.dscp(), parse_back.dscp()); - #[cfg(not(kani))] // remove when we fix options generation assert_eq!(header, &parse_back); assert_eq!(bytes_written, bytes_read); }); } #[test] - #[cfg_attr(kani, kani::proof)] fn parse_arbitrary_bytes() { bolero::check!() .with_type() @@ -561,7 +558,6 @@ mod test { // reserved bit in ipv4 flags should serialize to zero assert_eq!(slice[6] & 0b0111_1111, buf[6]); assert_eq!(&slice[7..MIN_LEN_USIZE], &buf.as_slice()[7..MIN_LEN_USIZE]); - #[cfg(not(kani))] // remove when we fix options generation assert_eq!( &slice[MIN_LEN_USIZE..consumed.into_non_zero_usize().get()], &buf.as_slice()[MIN_LEN_USIZE..consumed.into_non_zero_usize().get()] diff --git a/net/src/ipv6/addr.rs b/net/src/ipv6/addr.rs index f67b2938d..355cc66c7 100644 --- a/net/src/ipv6/addr.rs +++ b/net/src/ipv6/addr.rs @@ -115,7 +115,6 @@ mod test { use crate::ipv6::addr::UnicastIpv6Addr; #[test] - #[cfg_attr(kani, kani::proof)] fn generated_unicast_ipv6_address_is_unicast() { bolero::check!() .with_type() diff --git a/net/src/ipv6/mod.rs b/net/src/ipv6/mod.rs index 86a6988d4..2ab7c190e 100644 --- a/net/src/ipv6/mod.rs +++ b/net/src/ipv6/mod.rs @@ -651,7 +651,6 @@ mod test { const MIN_LEN: usize = Ipv6::MIN_LEN.get() as usize; #[test] - #[cfg_attr(kani, kani::proof)] fn parse_back() { bolero::check!().with_type().for_each(|header: &Ipv6| { let mut buf = [0u8; MIN_LEN]; @@ -664,7 +663,6 @@ mod test { } #[test] - #[cfg_attr(kani, kani::proof)] fn parse_arbitrary_bytes() { bolero::check!() .with_type() @@ -697,7 +695,6 @@ mod test { } #[test] - #[cfg_attr(kani, kani::proof)] fn parse_arbitrary_bytes_too_short() { bolero::check!() .with_type() @@ -711,7 +708,6 @@ mod test { } #[test] - #[cfg_attr(kani, kani::proof)] fn parse_arbitrary_bytes_above_minimum() { bolero::check!() .with_type() diff --git a/net/src/tcp/mod.rs b/net/src/tcp/mod.rs index 60586ac55..4a657e923 100644 --- a/net/src/tcp/mod.rs +++ b/net/src/tcp/mod.rs @@ -410,7 +410,6 @@ mod test { const MIN_LEN: usize = Tcp::MIN_LENGTH.get() as usize; #[test] - #[cfg_attr(kani, kani::proof)] fn parse_back() { bolero::check!().with_type().for_each(|tcp: &Tcp| { let mut buffer = [0u8; 64]; @@ -439,14 +438,12 @@ mod test { assert_eq!(tcp.urg(), parsed.urg()); assert_eq!(tcp.window_size(), parsed.window_size()); assert_eq!(tcp.urgent_pointer(), parsed.urgent_pointer()); - #[cfg(not(kani))] // remove after fixing options assert_eq!(tcp, &parsed); assert_eq!(consumed, consumed2); }); } #[test] - #[cfg_attr(kani, kani::proof)] fn parse_noise() { bolero::check!() .with_type() @@ -477,13 +474,11 @@ mod test { let (parsed_back, consumed3) = Tcp::parse(&slice2[..consumed2.into_non_zero_usize().get()]).unwrap(); assert_eq!(consumed2, consumed3); - #[cfg(not(kani))] // remove after fixing options assert_eq!(parsed, parsed_back); assert_eq!(&slice[..12], &slice2[..12]); // check for reserved bits getting zeroed by `write` (regardless of inputs) assert_eq!(slice[12] & 0b1111_0001, slice2[12]); assert_eq!(&slice[13..MIN_LEN], &slice2[13..MIN_LEN]); - #[cfg(not(kani))] // remove after fixing options assert_eq!( &slice[MIN_LEN..consumed1.into_non_zero_usize().get()], &slice2[MIN_LEN..consumed1.into_non_zero_usize().get()] diff --git a/net/src/udp/mod.rs b/net/src/udp/mod.rs index 02dc34767..d173feecc 100644 --- a/net/src/udp/mod.rs +++ b/net/src/udp/mod.rs @@ -283,7 +283,6 @@ mod test { const MIN_LENGTH_USIZE: usize = 8; #[test] - #[cfg_attr(kani, kani::proof)] fn parse_back() { bolero::check!().with_type().for_each(|input: &Udp| { let mut buffer = [0u8; MIN_LENGTH_USIZE]; @@ -306,7 +305,6 @@ mod test { } #[test] - #[cfg_attr(kani, kani::proof)] fn parse_arbitrary_bytes() { bolero::check!() .with_type() @@ -335,7 +333,6 @@ mod test { } #[test] - #[cfg_attr(kani, kani::proof)] fn too_short_buffer_parse_fails_gracefully() { bolero::check!() .with_type() @@ -353,7 +350,6 @@ mod test { } #[test] - #[cfg_attr(kani, kani::proof)] fn longer_buffer_parses_ok() { bolero::check!() .with_type() @@ -385,7 +381,6 @@ mod test { // evolve an arbitrary source towards an arbitrary target to make sure mutation methods work #[test] - #[cfg_attr(kani, kani::proof)] fn arbitrary_mutation() { bolero::check!() .with_type() diff --git a/net/src/vlan/mod.rs b/net/src/vlan/mod.rs index 572b8cc92..b992f8451 100644 --- a/net/src/vlan/mod.rs +++ b/net/src/vlan/mod.rs @@ -500,7 +500,6 @@ mod test { } #[test] - #[cfg_attr(kani, kani::proof)] fn pcp_bounds_respected() { bolero::check!() .with_type() @@ -519,7 +518,6 @@ mod test { } #[test] - #[cfg_attr(kani, kani::proof)] fn parse_back() { bolero::check!().with_type().for_each(|vlan: &Vlan| { let mut buf = [0u8; MIN_LENGTH_USIZE]; // vlan headers are always 4 bytes long @@ -538,7 +536,6 @@ mod test { } #[test] - #[cfg_attr(kani, kani::proof)] fn parse_noise() { bolero::check!().with_type().for_each(|buf: &[u8; MIN_LENGTH_USIZE]| { let (vlan, consumed) = match Vlan::parse(buf) { @@ -561,7 +558,6 @@ mod test { } #[test] - #[cfg_attr(kani, kani::proof)] fn parse_noise_too_short() { bolero::check!().with_type().for_each( |buf: &[u8; MIN_LENGTH_USIZE - 1]| match Vlan::parse(buf) { @@ -575,7 +571,6 @@ mod test { } #[test] - #[cfg_attr(kani, kani::proof)] fn arbitrary_mutation() { bolero::check!() .with_type() @@ -595,7 +590,6 @@ mod test { } #[test] - #[cfg_attr(kani, kani::proof)] fn deparse_to_insufficient_buffer_is_graceful() { bolero::check!().with_type().for_each(|vlan: &Vlan| { let mut buf = [0u8; MIN_LENGTH_USIZE - 1]; diff --git a/net/src/vxlan/mod.rs b/net/src/vxlan/mod.rs index 6f84a77aa..02c545d39 100644 --- a/net/src/vxlan/mod.rs +++ b/net/src/vxlan/mod.rs @@ -152,7 +152,6 @@ mod test { const MIN_LENGTH_USIZE: usize = 8; #[test] - #[cfg_attr(kani, kani::proof)] fn parse_back() { bolero::check!().with_type().for_each(|vxlan: &Vxlan| { assert_eq!(vxlan.size(), Vxlan::MIN_LENGTH); @@ -167,7 +166,6 @@ mod test { } #[test] - #[cfg_attr(kani, kani::proof)] fn creation_identity_check() { bolero::check!().with_type().for_each(|vxlan: &Vxlan| { assert_eq!(vxlan, &Vxlan::new(vxlan.vni())); @@ -176,7 +174,6 @@ mod test { } #[test] - #[cfg_attr(kani, kani::proof)] fn parse_noise() { bolero::check!() .with_type() @@ -223,7 +220,6 @@ mod test { } #[test] - #[cfg_attr(kani, kani::proof)] fn write_to_insufficient_buffer_fails_gracefully() { bolero::check!().with_type().for_each(|vni: &Vxlan| { let mut too_small_buffer = [0u8; MIN_LENGTH_USIZE - 1]; @@ -238,7 +234,6 @@ mod test { } #[test] - #[cfg_attr(kani, kani::proof)] fn parse_of_insufficient_buffer_fails_gracefully() { bolero::check!() .with_type() @@ -254,7 +249,6 @@ mod test { } #[test] - #[cfg_attr(kani, kani::proof)] fn mutation_of_header_preserves_contract() { bolero::check!() .with_type() From 9c846ec3c9eebadd03030bebbf74b1faa9b3ccc9 Mon Sep 17 00:00:00 2001 From: Daniel Noland Date: Tue, 24 Mar 2026 19:25:35 -0600 Subject: [PATCH 02/17] build(net): reduce bolero requirements Slim down bolero feature flags from [alloc, arbitrary, std] to [std] in both dependencies and dev-dependencies. Remove duplicate etherparse dev-dependency (already present in [dependencies]). This drops the transitive 'arbitrary' crate dependency. Co-authored-by: Claude (Opus 4.6) Signed-off-by: Daniel Noland --- net/Cargo.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/net/Cargo.toml b/net/Cargo.toml index 26ae27a4b..4b9df60ef 100644 --- a/net/Cargo.toml +++ b/net/Cargo.toml @@ -17,7 +17,7 @@ test_buffer = [] arrayvec = { workspace = true, features = ["serde", "std"] } atomic-instant-full = { workspace = true } bitflags = { workspace = true } -bolero = { workspace = true, features = ["alloc", "arbitrary", "std"], optional = true } +bolero = { workspace = true, features = ["std"], optional = true } bytecheck = { workspace = true } concurrency = { workspace = true } derive_builder = { workspace = true, features = ["alloc"] } @@ -35,5 +35,4 @@ tracing = { workspace = true } [dev-dependencies] ahash = { workspace = true, features = ["no-rng"] } -bolero = { workspace = true, features = ["alloc", "arbitrary", "std"] } -etherparse = { workspace = true, default-features = false, features = ["std"] } +bolero = { workspace = true, features = ["std"] } From e653402d5c8f45557e5b9ff549ad4bc181ed674c Mon Sep 17 00:00:00 2001 From: Daniel Noland Date: Tue, 24 Mar 2026 19:25:52 -0600 Subject: [PATCH 03/17] style(net): fix clippy lints in headers - Add #[must_use] to pure accessor methods - Add #[allow(clippy::cast_possible_truncation)] and #[allow(clippy::too_many_lines)] where needed - Replace .map(...).unwrap_or(0) with .map_or(0, ...) - Collapse nested Some(A) | Some(B) match arms into Some(A | B) - Replace Default::default() with ArrayVec::default() in contract generators - Convert match on bool to if/else in bolero generators - Fix stray semicolons and narrow wildcard imports in contract module - Add Errors doc sections to fallible public methods Co-authored-by: Claude (Opus 4.6) Signed-off-by: Daniel Noland --- net/src/headers/embedded.rs | 67 +++++++++++++++++++-------- net/src/headers/mod.rs | 90 +++++++++++++++++++++++++------------ 2 files changed, 109 insertions(+), 48 deletions(-) diff --git a/net/src/headers/embedded.rs b/net/src/headers/embedded.rs index cac4780e1..821bcb6bb 100644 --- a/net/src/headers/embedded.rs +++ b/net/src/headers/embedded.rs @@ -50,6 +50,7 @@ pub struct EmbeddedHeaders { impl EmbeddedHeaders { #[cfg(any(test, feature = "bolero"))] + #[must_use] pub fn new( net: Option, transport: Option, @@ -58,8 +59,8 @@ impl EmbeddedHeaders { ) -> Self { Self { net, - transport, net_ext, + transport, full_payload_length, } } @@ -95,25 +96,29 @@ impl EmbeddedHeaders { } } + #[must_use] pub fn net_headers_len(&self) -> u16 { - self.net.as_ref().map(|net| net.size().get()).unwrap_or(0) + self.net.as_ref().map_or(0, |net| net.size().get()) } + #[must_use] pub fn transport_headers_len(&self) -> u16 { self.transport .as_ref() - .map(|transport| transport.size().get()) - .unwrap_or(0) + .map_or(0, |transport| transport.size().get()) } + #[must_use] pub fn is_full_payload(&self) -> bool { self.full_payload_length.is_some() } + #[must_use] pub fn payload_length(&self) -> Option { self.full_payload_length } + #[allow(clippy::cast_possible_truncation, clippy::too_many_lines)] pub fn check_full_payload( &mut self, buf: &[u8], @@ -125,18 +130,22 @@ impl EmbeddedHeaders { match &mut self.transport { None - | Some(EmbeddedTransport::Tcp(TruncatedTcp::PartialHeader(_))) - | Some(EmbeddedTransport::Udp(TruncatedUdp::PartialHeader(_))) - | Some(EmbeddedTransport::Icmp4(TruncatedIcmp4::PartialHeader(_))) - | Some(EmbeddedTransport::Icmp6(TruncatedIcmp6::PartialHeader(_))) => { + | Some( + EmbeddedTransport::Tcp(TruncatedTcp::PartialHeader(_)) + | EmbeddedTransport::Udp(TruncatedUdp::PartialHeader(_)) + | EmbeddedTransport::Icmp4(TruncatedIcmp4::PartialHeader(_)) + | EmbeddedTransport::Icmp6(TruncatedIcmp6::PartialHeader(_)), + ) => { // We couldn't parse the full transport header, of course we don't have the full, // valid payload return; } - Some(EmbeddedTransport::Tcp(TruncatedTcp::FullHeader(_))) - | Some(EmbeddedTransport::Udp(TruncatedUdp::FullHeader(_))) - | Some(EmbeddedTransport::Icmp4(TruncatedIcmp4::FullHeader(_))) - | Some(EmbeddedTransport::Icmp6(TruncatedIcmp6::FullHeader(_))) => { + Some( + EmbeddedTransport::Tcp(TruncatedTcp::FullHeader(_)) + | EmbeddedTransport::Udp(TruncatedUdp::FullHeader(_)) + | EmbeddedTransport::Icmp4(TruncatedIcmp4::FullHeader(_)) + | EmbeddedTransport::Icmp6(TruncatedIcmp6::FullHeader(_)), + ) => { // There's a chance payload is full, keep going } } @@ -467,6 +476,7 @@ pub enum EmbeddedTransport { } impl EmbeddedTransport { + #[must_use] pub fn icmp_identifier(&self) -> Option { match self { EmbeddedTransport::Icmp4(icmp) => icmp.identifier(), @@ -475,6 +485,7 @@ impl EmbeddedTransport { } } + #[must_use] pub fn source(&self) -> Option> { match self { EmbeddedTransport::Tcp(tcp) => Some(tcp.source().into()), @@ -483,6 +494,7 @@ impl EmbeddedTransport { } } + #[must_use] pub fn destination(&self) -> Option> { match self { EmbeddedTransport::Tcp(tcp) => Some(tcp.destination().into()), @@ -491,6 +503,12 @@ impl EmbeddedTransport { } } + /// Set the source port of the embedded transport header. + /// + /// # Errors + /// + /// Returns [`EmbeddedHeaderError::NoPorts`] if the transport header does not have ports + /// (i.e., ICMP). pub fn set_source(&mut self, port: NonZero) -> Result<(), EmbeddedHeaderError> { match self { EmbeddedTransport::Tcp(tcp) => { @@ -505,6 +523,12 @@ impl EmbeddedTransport { } } + /// Set the destination port of the embedded transport header. + /// + /// # Errors + /// + /// Returns [`EmbeddedHeaderError::NoPorts`] if the transport header does not have ports + /// (i.e., ICMP). pub fn set_destination(&mut self, port: NonZero) -> Result<(), EmbeddedHeaderError> { match self { EmbeddedTransport::Tcp(tcp) => { @@ -1145,6 +1169,7 @@ mod tests { // Basic parsing, deparsing checks #[test] + #[allow(clippy::cast_possible_truncation)] fn test_parse_ipv4_with_truncated_tcp() { let buf = create_truncated_ipv4_tcp_packet(); @@ -1162,6 +1187,7 @@ mod tests { } #[test] + #[allow(clippy::cast_possible_truncation)] fn test_parse_ipv4_with_full_udp() { let buf = create_full_ipv4_udp_packet(); @@ -1179,6 +1205,7 @@ mod tests { } #[test] + #[allow(clippy::cast_possible_truncation)] fn test_parse_ipv6_with_truncated_tcp() { let buf = create_truncated_ipv6_tcp_packet(); @@ -1196,6 +1223,7 @@ mod tests { } #[test] + #[allow(clippy::cast_possible_truncation)] fn test_parse_ipv6_with_full_udp() { let buf = create_full_ipv6_udp_packet(); @@ -1592,7 +1620,7 @@ mod tests { #[cfg(any(test, feature = "bolero"))] mod contract { - use super::*; + use super::{EmbeddedHeaders, EmbeddedTransport}; use crate::headers::Net; use crate::ipv4; use crate::ipv6; @@ -1606,17 +1634,18 @@ mod contract { type Output = EmbeddedHeaders; fn generate(&self, driver: &mut D) -> Option { - let (ipv4_next_header, ipv6_next_header, transport) = match driver.produce::()? { - true => ( + let (ipv4_next_header, ipv6_next_header, transport) = if driver.produce::()? { + ( ipv4::CommonNextHeader::Tcp, ipv6::CommonNextHeader::Tcp, EmbeddedTransport::Tcp(driver.produce::()?), - ), - false => ( + ) + } else { + ( ipv4::CommonNextHeader::Udp, ipv6::CommonNextHeader::Udp, EmbeddedTransport::Udp(driver.produce::()?), - ), + ) }; let is_ipv4 = driver.produce::()?; @@ -1680,6 +1709,6 @@ mod tests_fuzzing { None => { unreachable!() } - }) + }); } } diff --git a/net/src/headers/mod.rs b/net/src/headers/mod.rs index 668299567..fae70af3e 100644 --- a/net/src/headers/mod.rs +++ b/net/src/headers/mod.rs @@ -2,7 +2,7 @@ // Copyright Open Network Fabric Authors //! Definition of [`Headers`] and related methods and types. -#![allow(missing_docs, clippy::pedantic)] // temporary +#![allow(missing_docs)] // temporary use crate::checksum::Checksum; use crate::eth::ethtype::EthType; @@ -65,6 +65,7 @@ pub enum Net { } impl Net { + #[must_use] pub fn dst_addr(&self) -> IpAddr { match self { Net::Ipv4(ip) => IpAddr::V4(ip.destination()), @@ -72,6 +73,7 @@ impl Net { } } + #[must_use] pub fn src_addr(&self) -> IpAddr { match self { Net::Ipv4(ip) => IpAddr::V4(ip.source().inner()), @@ -79,6 +81,7 @@ impl Net { } } + #[must_use] pub fn next_header(&self) -> NextHeader { match self { Net::Ipv4(ip) => ip.protocol().into(), @@ -86,6 +89,12 @@ impl Net { } } + /// Sets the source address of the network header. + /// + /// # Errors + /// + /// Returns [`NetError::InvalidIpVersion`] if the IP version of `addr` does not match the + /// IP version of the network header. pub fn try_set_source(&mut self, addr: UnicastIpAddr) -> Result<(), NetError> { match (self, addr) { (Net::Ipv4(ip), UnicastIpAddr::V4(addr)) => { @@ -101,6 +110,12 @@ impl Net { Ok(()) } + /// Sets the destination address of the network header. + /// + /// # Errors + /// + /// Returns [`NetError::InvalidIpVersion`] if the IP version of `addr` does not match the + /// IP version of the network header. pub fn try_set_destination(&mut self, addr: IpAddr) -> Result<(), NetError> { match (self, addr) { (Net::Ipv4(ip), IpAddr::V4(addr)) => { @@ -195,7 +210,7 @@ impl Transport { icmp4 .update_checksum(payload.as_ref()) .unwrap_or_else(|()| unreachable!()); // Updating ICMPv4 checksum never fails - }; + } } (Net::Ipv6(ip), Transport::Icmp6(icmp6)) => { if icmp6.is_error_message() && embedded_headers.is_some() { @@ -315,6 +330,12 @@ impl Transport { Ok(()) } + /// Sets the ICMP identifier field. + /// + /// # Errors + /// + /// Returns [`TransportError::UnsupportedIdentifier`] if the transport header does not + /// support identifiers (i.e., TCP or UDP). pub fn try_set_identifier(&mut self, identifier: u16) -> Result<(), TransportError> { match self { Transport::Icmp4(icmp4) => icmp4 @@ -467,7 +488,7 @@ impl DeParse for Headers { fn size(&self) -> NonZero { // TODO(blocking): Deal with ip{v4,v6} extensions - let eth = self.eth.as_ref().map(|x| x.size().get()).unwrap_or(0); + let eth = self.eth.as_ref().map_or(0, |x| x.size().get()); let vlan = self.vlan.iter().map(|v| v.size().get()).sum::(); let net = match self.net { None => { @@ -482,7 +503,7 @@ impl DeParse for Headers { }; let encap = match self.udp_encap { None => 0, - Some(UdpEncap::Vxlan(vxlan)) => vxlan.size().get(), + Some(UdpEncap::Vxlan(vx)) => vx.size().get(), }; let embedded_ip = self .embedded_ip @@ -582,6 +603,7 @@ pub enum PopVlanError { impl Headers { /// Create a new [`Headers`] with the supplied `Eth` header. + #[must_use] pub fn new() -> Headers { Headers::default() } @@ -622,6 +644,12 @@ impl Headers { /// /// This method will ensure that the `eth` field has its [`EthType`] adjusted to /// [`EthType::VLAN`] if there are no [`Vlan`]s on the stack at the time this method was called. + /// + /// # Errors + /// + /// Returns [`PushVlanError::TooManyVlans`] if there are already [`MAX_VLANS`] VLANs on the + /// stack. + /// Returns [`PushVlanError::NoEthernetHeader`] if no Ethernet header is present. pub fn push_vlan(&mut self, vid: Vid) -> Result<(), PushVlanError> { if self.vlan.len() >= MAX_VLANS { return Err(PushVlanError::TooManyVlans); @@ -646,6 +674,10 @@ impl Headers { /// preserve the structure. /// /// If `None` is returned, the [`Headers`] is not modified. + /// + /// # Errors + /// + /// Returns [`PopVlanError::NoEthernetHeader`] if no Ethernet header is present. pub fn pop_vlan(&mut self) -> Result, PopVlanError> { match &mut self.eth { None => Err(PopVlanError::NoEthernetHeader), @@ -1371,6 +1403,7 @@ mod contract { impl ValueGenerator for CommonHeaders { type Output = Headers; + #[allow(clippy::too_many_lines)] fn generate(&self, driver: &mut D) -> Option { let common_eth_type: CommonEthType = driver.produce()?; let eth = GenWithEthType(common_eth_type.into()).generate(driver)?; @@ -1384,9 +1417,9 @@ mod contract { let tcp: Tcp = driver.produce()?; let headers = Headers { eth: Some(eth), - vlan: Default::default(), + vlan: ArrayVec::default(), net: Some(Net::Ipv4(ipv4)), - net_ext: Default::default(), + net_ext: ArrayVec::default(), transport: Some(Transport::Tcp(tcp)), udp_encap: None, embedded_ip: None, @@ -1403,9 +1436,9 @@ mod contract { }; let headers = Headers { eth: Some(eth), - vlan: Default::default(), + vlan: ArrayVec::default(), net: Some(Net::Ipv4(ipv4)), - net_ext: Default::default(), + net_ext: ArrayVec::default(), transport: Some(Transport::Udp(udp)), udp_encap, embedded_ip: None, @@ -1418,7 +1451,7 @@ mod contract { eth: Some(eth), vlan: ArrayVec::default(), net: Some(Net::Ipv4(ipv4)), - net_ext: Default::default(), + net_ext: ArrayVec::default(), transport: Some(Transport::Icmp4(icmp)), udp_encap: None, embedded_ip: None, @@ -1436,9 +1469,9 @@ mod contract { let tcp: Tcp = driver.produce()?; let headers = Headers { eth: Some(eth), - vlan: Default::default(), + vlan: ArrayVec::default(), net: Some(Net::Ipv6(ipv6)), - net_ext: Default::default(), + net_ext: ArrayVec::default(), transport: Some(Transport::Tcp(tcp)), udp_encap: None, embedded_ip: None, @@ -1455,9 +1488,9 @@ mod contract { }; let headers = Headers { eth: Some(eth), - vlan: Default::default(), + vlan: ArrayVec::default(), net: Some(Net::Ipv6(ipv6)), - net_ext: Default::default(), + net_ext: ArrayVec::default(), transport: Some(Transport::Udp(udp)), udp_encap, embedded_ip: None, @@ -1468,9 +1501,9 @@ mod contract { let icmp6: Icmp6 = driver.produce()?; let headers = Headers { eth: Some(eth), - vlan: Default::default(), + vlan: ArrayVec::default(), net: Some(Net::Ipv6(ipv6)), - net_ext: Default::default(), + net_ext: ArrayVec::default(), transport: Some(Transport::Icmp6(icmp6)), udp_encap: None, embedded_ip: None, @@ -1484,21 +1517,22 @@ mod contract { } } -#[cfg(any(test, kani))] -#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] // fine to unwarp in tests +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] // fine to unwrap in tests mod test { use std::net::Ipv4Addr; + use crate::checksum::Checksum; use crate::headers::Headers; use crate::headers::contract::CommonHeaders; use crate::icmp4::Icmp4Checksum; use crate::parse::{DeParse, DeParseError, IntoNonZeroUSize, Parse, ParseError}; - use super::*; + use super::{Net, Transport}; use crate::icmp6::{Icmp6Checksum, Icmp6ChecksumPayload}; use crate::ipv4::{Ipv4Checksum, UnicastIpv4Addr}; - use crate::tcp::{TcpChecksum, TcpChecksumPayload}; - use crate::udp::{UdpChecksum, UdpChecksumPayload}; + use crate::tcp::{TcpChecksum, TcpChecksumPayload, TcpPort}; + use crate::udp::{UdpChecksum, UdpChecksumPayload, UdpPort}; fn parse_back_test(headers: &Headers) { let mut buffer = [0_u8; 1024]; @@ -1521,17 +1555,15 @@ mod test { } #[test] - #[cfg_attr(kani, kani::proof)] fn parse_back() { - bolero::check!().with_type().for_each(parse_back_test) + bolero::check!().with_type().for_each(parse_back_test); } #[test] - #[cfg_attr(kani, kani::proof)] fn parse_back_common() { bolero::check!() .with_generator(CommonHeaders) - .for_each(parse_back_test) + .for_each(parse_back_test); } mod sample { @@ -1696,6 +1728,7 @@ mod test { } } + #[allow(clippy::too_many_lines)] fn test_checksum(mut headers: Headers) { match &headers.transport { None => {} @@ -1794,7 +1827,7 @@ mod test { // Check incremental updates match &mut headers.transport { - None => {} + None | Some(Transport::Icmp6(_)) => {} Some(Transport::Udp(transport)) => { let net = headers.net.clone().unwrap(); let old_value = transport.source().into(); @@ -1845,9 +1878,8 @@ mod test { .expect("expected valid checksum"); } Net::Ipv6(_) => panic!("unexpected ipv6"), - }; + } } - Some(Transport::Icmp6(_)) => {} } } @@ -1927,7 +1959,7 @@ mod test { _ => unreachable!(), } } - _ => unreachable!(), + Net::Ipv6(_) => unreachable!(), }, _ => unreachable!(), } @@ -1980,7 +2012,7 @@ mod test { _ => unreachable!(), } } - _ => unreachable!(), + Net::Ipv6(_) => unreachable!(), }, _ => unreachable!(), } From b894ab1fccdec22a120bf43a53a0252ff5574030 Mon Sep 17 00:00:00 2001 From: Daniel Noland Date: Tue, 24 Mar 2026 20:49:58 -0600 Subject: [PATCH 04/17] chore(config): deny clippy::struct_excessive_bools Remove the crate-wide #![allow(clippy::struct_excessive_bools)] from lib.rs. Add targeted #[allow(clippy::struct_excessive_bools)] to the 6 structs that legitimately need many bool fields: AfL2vpnEvpn, BgpNeighCapabilities, BgpNeighbor, BgpDefaultsAF, BgpOptions, and BmpOptions. Co-authored-by: Claude (Opus 4.6) Signed-off-by: Daniel Noland --- config/src/internal/routing/bgp.rs | 5 +++++ config/src/internal/routing/bmp.rs | 1 + config/src/lib.rs | 1 - 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/config/src/internal/routing/bgp.rs b/config/src/internal/routing/bgp.rs index a27e6c5ed..f4c796d5a 100644 --- a/config/src/internal/routing/bgp.rs +++ b/config/src/internal/routing/bgp.rs @@ -51,6 +51,7 @@ pub struct AfIpv6Ucast { } #[derive(Clone, Debug, Default)] +#[allow(clippy::struct_excessive_bools)] pub struct AfL2vpnEvpn { pub adv_all_vni: bool, pub adv_default_gw: bool, @@ -64,6 +65,7 @@ pub struct AfL2vpnEvpn { } #[derive(Clone, Debug, Default)] +#[allow(clippy::struct_excessive_bools)] pub struct BgpNeighCapabilities { pub dynamic: bool, pub ext_nhop: bool, @@ -119,6 +121,7 @@ impl BgpNeighAF { #[derive(Clone, Debug, Default)] /// A BGP neighbor config +#[allow(clippy::struct_excessive_bools)] pub struct BgpNeighbor { pub ntype: BgpNeighType, pub remote_as: Option, @@ -159,6 +162,7 @@ pub struct BgpNeighbor { } #[derive(Clone, Debug, Default)] +#[allow(clippy::struct_excessive_bools)] pub struct BgpDefaultsAF { flow_spec: bool, labeled_unicast: bool, @@ -178,6 +182,7 @@ pub struct BgpDefaults { #[derive(Clone, Debug)] /// BGP global configuration options +#[allow(clippy::struct_excessive_bools)] pub struct BgpOptions { pub network_import_check: bool, pub ebgp_requires_policy: bool, diff --git a/config/src/internal/routing/bmp.rs b/config/src/internal/routing/bmp.rs index 126f3249d..5b297ef10 100644 --- a/config/src/internal/routing/bmp.rs +++ b/config/src/internal/routing/bmp.rs @@ -26,6 +26,7 @@ pub enum BmpSource { } #[derive(Clone, Debug)] +#[allow(clippy::struct_excessive_bools)] pub struct BmpOptions { /// Name for `bmp targets ` pub target_name: String, diff --git a/config/src/lib.rs b/config/src/lib.rs index 0cd826887..f9c198985 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -18,7 +18,6 @@ #![allow(clippy::redundant_closure_for_method_calls)] #![allow(clippy::doc_markdown)] #![allow(clippy::missing_errors_doc)] -#![allow(clippy::struct_excessive_bools)] pub mod converters; pub mod display; From fe99b519888ef9d001760d074fa2673e0c6e28ab Mon Sep 17 00:00:00 2001 From: Daniel Noland Date: Tue, 24 Mar 2026 20:51:29 -0600 Subject: [PATCH 05/17] docs(config): deny clippy::missing_errors_doc and clippy::doc_markdown Remove the crate-wide #![allow(clippy::doc_markdown)] and #![allow(clippy::missing_errors_doc)] from lib.rs. - Add '# Errors' doc sections to ~20 fallible public methods across config/src/ - Fix doc_markdown lints: backtick-wrap type names in doc comments - Convert // comments to /// doc comments on VpcExpose NAT methods - Fix trailing colon in VpcExpose::validate doc comment Co-authored-by: Claude (Opus 4.6) Signed-off-by: Daniel Noland --- config/src/converters/k8s/status/bgp.rs | 2 +- config/src/external/communities.rs | 4 + config/src/external/gwgroup.rs | 11 +++ config/src/external/mod.rs | 5 ++ config/src/external/overlay/mod.rs | 8 ++ .../src/external/overlay/validation_tests.rs | 2 +- config/src/external/overlay/vpcpeering.rs | 75 +++++++++++++------ config/src/external/underlay/mod.rs | 5 ++ config/src/gwconfig.rs | 6 +- config/src/internal/device/mod.rs | 5 ++ config/src/internal/interfaces/interface.rs | 5 ++ config/src/internal/mod.rs | 5 ++ config/src/internal/routing/prefixlist.rs | 10 +++ config/src/internal/routing/routemap.rs | 5 ++ config/src/internal/routing/vrf.rs | 4 + config/src/lib.rs | 2 - 16 files changed, 124 insertions(+), 30 deletions(-) diff --git a/config/src/converters/k8s/status/bgp.rs b/config/src/converters/k8s/status/bgp.rs index 979ada8c9..e718badea 100644 --- a/config/src/converters/k8s/status/bgp.rs +++ b/config/src/converters/k8s/status/bgp.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Open Network Fabric Authors -//! Converters for internal BGP status -> K8s GatewayAgentStatusStateBgp CRD. +//! Converters for internal BGP status -> K8s `GatewayAgentStatusStateBgp` CRD. use std::collections::BTreeMap; diff --git a/config/src/external/communities.rs b/config/src/external/communities.rs index 0f2ebbecf..139983723 100644 --- a/config/src/external/communities.rs +++ b/config/src/external/communities.rs @@ -16,6 +16,10 @@ impl PriorityCommunityTable { Self::default() } /// Insert a community + /// + /// # Errors + /// + /// Returns [`ConfigError::DuplicateCommunity`] if the community already exists in the table. pub fn insert(&mut self, order: usize, community: &str) -> Result<(), ConfigError> { if self.0.iter().any(|(_, comm)| comm == community) { return Err(ConfigError::DuplicateCommunity(community.to_string())); diff --git a/config/src/external/gwgroup.rs b/config/src/external/gwgroup.rs index 377657024..e4d5dd039 100644 --- a/config/src/external/gwgroup.rs +++ b/config/src/external/gwgroup.rs @@ -77,6 +77,12 @@ impl GwGroup { //N.B. we reverse the operands since want the most preferred first self.members.sort_by(|m1, m2| m2.cmp(m1)); } + /// Add a member to the gateway group. + /// + /// # Errors + /// + /// Returns [`ConfigError::DuplicateMember`] if a member with the same name already exists. + /// Returns [`ConfigError::DuplicateMemberAddress`] if a member with the same IP address already exists. pub fn add_member(&mut self, member: GwGroupMember) -> Result<(), ConfigError> { if self.get_member_by_name(&member.name).is_some() { return Err(ConfigError::DuplicateMember(member.name.clone())); @@ -126,6 +132,11 @@ impl GwGroupTable { pub fn new() -> Self { Self::default() } + /// Add a gateway group to the table. + /// + /// # Errors + /// + /// Returns [`ConfigError::DuplicateGroup`] if a group with the same name already exists. pub fn add_group(&mut self, group: GwGroup) -> Result<(), ConfigError> { if self.0.contains_key(group.name()) { return Err(ConfigError::DuplicateGroup(group.name().to_owned())); diff --git a/config/src/external/mod.rs b/config/src/external/mod.rs index e8c2ecc3e..d8be5c668 100644 --- a/config/src/external/mod.rs +++ b/config/src/external/mod.rs @@ -93,6 +93,11 @@ impl ExternalConfig { } Ok(()) } + /// Validate the external configuration. + /// + /// # Errors + /// + /// Returns a [`ConfigError`] if validation fails. pub fn validate(&mut self) -> ConfigResult { self.device.validate()?; self.underlay.validate()?; diff --git a/config/src/external/overlay/mod.rs b/config/src/external/overlay/mod.rs index 525c78283..01fe756fe 100644 --- a/config/src/external/overlay/mod.rs +++ b/config/src/external/overlay/mod.rs @@ -39,6 +39,10 @@ impl Overlay { } /// Validate all peerings, checking if the VPCs they refer to exist in vpc table + /// + /// # Errors + /// + /// Returns an error if a peering references a VPC that does not exist in the VPC table. pub fn validate_peerings(&self) -> ConfigResult { debug!("Validating VPC peerings..."); for peering in self.peering_table.values() { @@ -60,6 +64,10 @@ impl Overlay { } /// Top most validation function for `Overlay` configuration + /// + /// # Errors + /// + /// Returns an error if the overlay configuration is invalid. pub fn validate(&mut self) -> ConfigResult { debug!("Validating overlay configuration..."); diff --git a/config/src/external/overlay/validation_tests.rs b/config/src/external/overlay/validation_tests.rs index 9bb8b6c7b..75b2ecf59 100644 --- a/config/src/external/overlay/validation_tests.rs +++ b/config/src/external/overlay/validation_tests.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Open Network Fabric Authors -//! Validation tests for VpcExpose / VpcPeering / Overlay +//! Validation tests for `VpcExpose` / `VpcPeering` / Overlay //! //! These tests cover the expected semantics and restrictions for Expose objects in VPC peerings. //! diff --git a/config/src/external/overlay/vpcpeering.rs b/config/src/external/overlay/vpcpeering.rs index 1d7b7d8e7..d4c6e04db 100644 --- a/config/src/external/overlay/vpcpeering.rs +++ b/config/src/external/overlay/vpcpeering.rs @@ -83,11 +83,11 @@ pub struct VpcExpose { pub nat: Option, } impl VpcExpose { - // Make the [`VpcExpose`] use stateless NAT. - // - // # Errors - // - // Returns an error if the [`VpcExpose`] already has a different NAT mode. + /// Make the [`VpcExpose`] use stateless NAT. + /// + /// # Errors + /// + /// Returns an error if the [`VpcExpose`] already has a different NAT mode. pub fn make_stateless_nat(mut self) -> Result { match self.nat.as_mut() { Some(nat) if nat.is_stateless() => Ok(self), @@ -103,12 +103,12 @@ impl VpcExpose { } } - // Make the [`VpcExpose`] use stateful NAT, with the given idle timeout, if provided. - // If the [`VpcExpose`] is already in stateful mode, the idle timeout is overwritten. - // - // # Errors - // - // Returns an error if the [`VpcExpose`] already has a different NAT mode. + /// Make the [`VpcExpose`] use stateful NAT, with the given idle timeout, if provided. + /// If the [`VpcExpose`] is already in stateful mode, the idle timeout is overwritten. + /// + /// # Errors + /// + /// Returns an error if the [`VpcExpose`] already has a different NAT mode. pub fn make_stateful_nat( mut self, idle_timeout: Option, @@ -132,15 +132,15 @@ impl VpcExpose { } } - // Make the [`VpcExpose`] use port forwarding, with the given idle timeout, if provided, and the - // given L4 protocol, if provided. - // - // If the [`VpcExpose`] is already in port forwarding mode, the idle timeout and L4 protocol are - // overwritten. - // - // # Errors - // - // Returns an error if the [`VpcExpose`] already has a different NAT mode. + /// Make the [`VpcExpose`] use port forwarding, with the given idle timeout, if provided, and the + /// given L4 protocol, if provided. + /// + /// If the [`VpcExpose`] is already in port forwarding mode, the idle timeout and L4 protocol are + /// overwritten. + /// + /// # Errors + /// + /// Returns an error if the [`VpcExpose`] already has a different NAT mode. pub fn make_port_forwarding( mut self, idle_timeout: Option, @@ -223,6 +223,11 @@ impl VpcExpose { self.nots.insert(prefix); self } + /// Add a prefix to the NAT `as` range. + /// + /// # Errors + /// + /// Returns an error if the expose has no NAT configuration. pub fn as_range(mut self, prefix: PrefixWithOptionalPorts) -> Result { let nat = self.nat.as_mut().ok_or(ConfigError::MissingParameter( "'as' block requires NAT configuration for the expose", @@ -230,6 +235,11 @@ impl VpcExpose { nat.as_range.insert(prefix); Ok(self) } + /// Add a prefix to the NAT `not as` exclusion set. + /// + /// # Errors + /// + /// Returns an error if the expose has no NAT configuration. pub fn not_as(mut self, prefix: PrefixWithOptionalPorts) -> Result { let nat = self.nat.as_mut().ok_or(ConfigError::MissingParameter( "'not' prefix for 'as' block requires NAT configuration for the expose", @@ -361,7 +371,11 @@ impl VpcExpose { Ok(()) } - /// Validate the [`VpcExpose`]: + /// Validate the [`VpcExpose`]. + /// + /// # Errors + /// + /// Returns an error if the expose configuration is invalid. #[allow(clippy::too_many_lines)] pub fn validate(&self) -> ConfigResult { // Check default exposes and prefixes @@ -602,6 +616,11 @@ impl VpcManifest { pub fn add_exposes(&mut self, exposes: impl IntoIterator) { self.exposes.extend(exposes); } + /// Validate the [`VpcManifest`]. + /// + /// # Errors + /// + /// Returns an error if the manifest configuration is invalid. pub fn validate(&self) -> ConfigResult { if self.name.is_empty() { return Err(ConfigError::MissingIdentifier("Manifest name")); @@ -695,7 +714,7 @@ impl VpcPeering { } } - /// Create a VpcPeering mapped to a group called "default". + /// Create a `VpcPeering` mapped to a group called "default". /// This should only be used for tests #[must_use] pub fn with_default_group(name: &str, left: VpcManifest, right: VpcManifest) -> Self { @@ -708,7 +727,11 @@ impl VpcPeering { } #[cfg(test)] - /// Validate A VpcPeering. Only used in tests. Dataplane validates `Peerings` + /// Validate A `VpcPeering`. Only used in tests. Dataplane validates `Peerings` + /// + /// # Errors + /// + /// Returns an error if the peering configuration is invalid. pub fn validate(&self) -> ConfigResult { self.left.validate()?; self.right.validate()?; @@ -744,7 +767,11 @@ impl VpcPeeringTable { self.0.is_empty() } - /// Add a [`VpcPeering`] to a [`VpcPeeringTable`] + /// Add a [`VpcPeering`] to a [`VpcPeeringTable`]. + /// + /// # Errors + /// + /// Returns an error if the peering name is missing or a duplicate peering exists. pub fn add(&mut self, peering: VpcPeering) -> ConfigResult { if peering.name.is_empty() { return Err(ConfigError::MissingIdentifier("Peering name")); diff --git a/config/src/external/underlay/mod.rs b/config/src/external/underlay/mod.rs index d47954d25..f18c33717 100644 --- a/config/src/external/underlay/mod.rs +++ b/config/src/external/underlay/mod.rs @@ -72,6 +72,11 @@ impl Underlay { } } + /// Validate the underlay configuration. + /// + /// # Errors + /// + /// Returns an error if any interface is invalid or VTEP configuration is wrong. pub fn validate(&mut self) -> ConfigResult { debug!("Validating underlay configuration..."); diff --git a/config/src/gwconfig.rs b/config/src/gwconfig.rs index f0ecf12d1..4b7190502 100644 --- a/config/src/gwconfig.rs +++ b/config/src/gwconfig.rs @@ -109,9 +109,11 @@ impl GwConfig { self.external.genid } - ////////////////////////////////////////////////////////////////// /// Validate a [`GwConfig`]. We only validate the external. - ////////////////////////////////////////////////////////////////// + /// + /// # Errors + /// + /// Returns a [`ConfigError`] if the external configuration fails validation. pub fn validate(&mut self) -> ConfigResult { debug!("Validating external config with genid {} ..", self.genid()); self.external.validate() diff --git a/config/src/internal/device/mod.rs b/config/src/internal/device/mod.rs index 92b7f6797..1f2c33d93 100644 --- a/config/src/internal/device/mod.rs +++ b/config/src/internal/device/mod.rs @@ -22,6 +22,11 @@ impl DeviceConfig { pub fn set_tracing(&mut self, tracing: TracingConfig) { self.tracing = Some(tracing); } + /// Validate the device configuration. + /// + /// # Errors + /// + /// Returns an error if the device configuration is invalid. pub fn validate(&self) -> ConfigResult { debug!("Validating device configuration.."); if let Some(tracing) = &self.tracing { diff --git a/config/src/internal/interfaces/interface.rs b/config/src/internal/interfaces/interface.rs index bca4a9968..2b2e2aa17 100644 --- a/config/src/internal/interfaces/interface.rs +++ b/config/src/internal/interfaces/interface.rs @@ -156,6 +156,11 @@ impl InterfaceConfig { matches!(self.iftype, InterfaceType::Vtep(_)) } + /// Validate the interface configuration. + /// + /// # Errors + /// + /// Returns an error if the interface name is empty. pub fn validate(&self) -> ConfigResult { // name is mandatory if self.name.is_empty() { diff --git a/config/src/internal/mod.rs b/config/src/internal/mod.rs index c16a4035a..f1670c846 100644 --- a/config/src/internal/mod.rs +++ b/config/src/internal/mod.rs @@ -68,6 +68,11 @@ impl InternalConfig { pub fn get_vtep(&self) -> &Option { &self.vtep } + /// Add a VRF configuration to the internal config. + /// + /// # Errors + /// + /// Returns an error if the VRF has duplicate fields (name, table ID, or VNI). pub fn add_vrf_config(&mut self, vrf_cfg: VrfConfig) -> ConfigResult { self.vrfs.add_vrf_config(vrf_cfg) } diff --git a/config/src/internal/routing/prefixlist.rs b/config/src/internal/routing/prefixlist.rs index 088e854dd..b2eddaae2 100644 --- a/config/src/internal/routing/prefixlist.rs +++ b/config/src/internal/routing/prefixlist.rs @@ -66,6 +66,11 @@ impl PrefixList { entries: BTreeMap::new(), } } + /// Add an entry to the prefix list. + /// + /// # Errors + /// + /// Returns an error if the entry has an incompatible IP version or the sequence number is a duplicate. pub fn add_entry(&mut self, seq: Option, mut entry: PrefixListEntry) -> ConfigResult { if !entry.is_version_compatible(self.ipver) { let msg = format!( @@ -94,6 +99,11 @@ impl PrefixList { self.entries.insert(seq, entry); Ok(()) } + /// Add multiple entries to the prefix list. + /// + /// # Errors + /// + /// Returns an error if any entry has an incompatible IP version or a duplicate sequence number. pub fn add_entries( &mut self, entries: impl IntoIterator, diff --git a/config/src/internal/routing/routemap.rs b/config/src/internal/routing/routemap.rs index b55a3b107..439c509f1 100644 --- a/config/src/internal/routing/routemap.rs +++ b/config/src/internal/routing/routemap.rs @@ -101,6 +101,11 @@ impl RouteMap { entries: BTreeMap::new(), } } + /// Add an entry to the route map. + /// + /// # Errors + /// + /// Returns an error if the sequence number is a duplicate. pub fn add_entry(&mut self, seq: Option, entry: RouteMapEntry) -> ConfigResult { let seq = if let Some(n) = seq { n diff --git a/config/src/internal/routing/vrf.rs b/config/src/internal/routing/vrf.rs index dd884fe53..86601af50 100644 --- a/config/src/internal/routing/vrf.rs +++ b/config/src/internal/routing/vrf.rs @@ -137,6 +137,10 @@ impl VrfConfigTable { ////////////////////////////////////////////////////////// /// Store a `VrfConfig` object in this `VrfConfigTable` + /// + /// # Errors + /// + /// Returns an error if the VRF has duplicate fields (name, table ID, or VNI). ////////////////////////////////////////////////////////// pub fn add_vrf_config(&mut self, vrf_cfg: VrfConfig) -> ConfigResult { let name = vrf_cfg.name.clone(); diff --git a/config/src/lib.rs b/config/src/lib.rs index f9c198985..51b8ba6ef 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -16,8 +16,6 @@ clippy::panic )] #![allow(clippy::redundant_closure_for_method_calls)] -#![allow(clippy::doc_markdown)] -#![allow(clippy::missing_errors_doc)] pub mod converters; pub mod display; From 8362ba988179b4e0968f6a7961f5eeb82b6580a9 Mon Sep 17 00:00:00 2001 From: Daniel Noland Date: Tue, 24 Mar 2026 20:51:45 -0600 Subject: [PATCH 06/17] style(config): fix clippy::redundant_closure_for_method_calls Remove the crate-wide #![allow(clippy::redundant_closure_for_method_calls)] from lib.rs and fix all instances: - .map(|v| v.take()) -> .map(LegalValue::take) - .map_or(..., |e| e.to_string()) -> .map_or(..., ToString::to_string) - .for_each(|group| group.sort_members()) -> .for_each(GwGroup::sort_members) - .try_for_each(|iface| iface.validate()) -> .try_for_each(InterfaceConfig::validate) - .map(|p| p.prefix()) -> .map(PrefixWithOptionalPorts::prefix) - .map(|p| p.size()) -> .map(PrefixWithOptionalPorts::size) - .any(|expose| expose.has_host_prefixes()) -> .any(VpcExpose::has_host_prefixes) Also remove unused import (crate::converters::k8s::config::peering). Co-authored-by: Claude (Opus 4.6) Signed-off-by: Daniel Noland --- .../src/converters/k8s/status/dataplane_status.rs | 2 +- config/src/display.rs | 2 +- config/src/external/gwgroup.rs | 2 +- config/src/external/overlay/vpc.rs | 1 - config/src/external/overlay/vpcpeering.rs | 14 ++++++++++---- config/src/external/underlay/mod.rs | 2 +- config/src/lib.rs | 1 - 7 files changed, 14 insertions(+), 10 deletions(-) diff --git a/config/src/converters/k8s/status/dataplane_status.rs b/config/src/converters/k8s/status/dataplane_status.rs index 78d404dec..e6ab7375c 100644 --- a/config/src/converters/k8s/status/dataplane_status.rs +++ b/config/src/converters/k8s/status/dataplane_status.rs @@ -136,7 +136,7 @@ mod tests { let last_applied_time = time_gen.generate(d)?; let status = d .produce::>>()? - .map(|v| v.take()); + .map(LegalValue::take); let last_collected_time_raw = time_gen.generate(d)?; let last_collected_time = status.as_ref().map(|_| last_collected_time_raw); let last_heartbeat_raw = time_gen.generate(d)?; diff --git a/config/src/display.rs b/config/src/display.rs index 4842ff7ed..09731c563 100644 --- a/config/src/display.rs +++ b/config/src/display.rs @@ -333,7 +333,7 @@ impl Display for GwConfigMeta { let error = self .error .as_ref() - .map_or("none".to_string(), |e| e.to_string()); + .map_or("none".to_string(), std::string::ToString::to_string); let is_rollback = if self.is_rollback { "(rollback)" } else { "" }; diff --git a/config/src/external/gwgroup.rs b/config/src/external/gwgroup.rs index e4d5dd039..c7b7e2735 100644 --- a/config/src/external/gwgroup.rs +++ b/config/src/external/gwgroup.rs @@ -343,7 +343,7 @@ mod test { fn test_bgp_community_setup() { let comtable = sample_community_table(); let mut gwtable = build_sample_gw_groups(); - gwtable.iter_mut().for_each(|group| group.sort_members()); + gwtable.iter_mut().for_each(GwGroup::sort_members); println!("{gwtable}"); println!("{comtable}"); diff --git a/config/src/external/overlay/vpc.rs b/config/src/external/overlay/vpc.rs index 55395fb9c..6107f3da3 100644 --- a/config/src/external/overlay/vpc.rs +++ b/config/src/external/overlay/vpc.rs @@ -12,7 +12,6 @@ use std::collections::BTreeMap; use std::collections::BTreeSet; use tracing::{debug, error, warn}; -use crate::converters::k8s::config::peering; use crate::external::overlay::VpcManifest; use crate::external::overlay::VpcPeeringTable; use crate::external::overlay::vpcpeering::VpcExposeNatConfig; diff --git a/config/src/external/overlay/vpcpeering.rs b/config/src/external/overlay/vpcpeering.rs index d4c6e04db..5f207648b 100644 --- a/config/src/external/overlay/vpcpeering.rs +++ b/config/src/external/overlay/vpcpeering.rs @@ -259,9 +259,15 @@ impl VpcExpose { // only V4 atm vec![Prefix::root_v4()] } else if let Some(nat) = self.nat.as_ref() { - nat.as_range.iter().map(|p| p.prefix()).collect::>() + nat.as_range + .iter() + .map(PrefixWithOptionalPorts::prefix) + .collect::>() } else { - self.ips.iter().map(|p| p.prefix()).collect::>() + self.ips + .iter() + .map(PrefixWithOptionalPorts::prefix) + .collect::>() } } @@ -456,7 +462,7 @@ impl VpcExpose { fn prefixes_size(prefixes: &PrefixPortsSet) -> PrefixWithPortsSize { prefixes .iter() - .map(|p| p.size()) + .map(PrefixWithOptionalPorts::size) .sum::() } let zero_size = ppsize_zero(); @@ -542,7 +548,7 @@ impl VpcManifest { } #[must_use] pub fn has_host_prefixes(&self) -> bool { - self.exposes.iter().any(|expose| expose.has_host_prefixes()) + self.exposes.iter().any(VpcExpose::has_host_prefixes) } fn validate_expose_collisions(&self) -> ConfigResult { // Check that prefixes in each expose don't overlap with prefixes in other exposes diff --git a/config/src/external/underlay/mod.rs b/config/src/external/underlay/mod.rs index f18c33717..3c6f61976 100644 --- a/config/src/external/underlay/mod.rs +++ b/config/src/external/underlay/mod.rs @@ -84,7 +84,7 @@ impl Underlay { self.vrf .interfaces .values() - .try_for_each(|iface| iface.validate())?; + .try_for_each(InterfaceConfig::validate)?; // set vtep information if a vtep interface has been specified in the config self.vtep = self.get_vtep_info()?; diff --git a/config/src/lib.rs b/config/src/lib.rs index 51b8ba6ef..57115bd98 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -15,7 +15,6 @@ clippy::expect_used, clippy::panic )] -#![allow(clippy::redundant_closure_for_method_calls)] pub mod converters; pub mod display; From 2da0c99de3e3f03b63bacc6d9fede03ee516093b Mon Sep 17 00:00:00 2001 From: Daniel Noland Date: Tue, 24 Mar 2026 19:27:33 -0600 Subject: [PATCH 07/17] build(net): prep for wasm32-wasip1 - Move atomic-instant-full under cfg(any(x86_64, aarch64)) target dep - Move linux-raw-sys under cfg(unix) target dep - Gate flows and packet modules with #![cfg(unix)] - Replace linux_raw_sys::if_ether::ETH_MAX_MTU with literal 65535 in mtu.rs - Add #![cfg_attr(not(unix), allow(unused))] to net/src/lib.rs Co-authored-by: Claude (Opus 4.6) Signed-off-by: Daniel Noland --- Cargo.lock | 1 - net/Cargo.toml | 5 +++-- net/src/interface/mtu.rs | 4 ++-- net/src/lib.rs | 4 ++++ net/src/packet/mod.rs | 2 ++ 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 514182d03..b71383752 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1614,7 +1614,6 @@ dependencies = [ "derive_builder 0.20.2", "downcast-rs", "etherparse", - "linux-raw-sys", "multi_index_map", "rapidhash", "rkyv", diff --git a/net/Cargo.toml b/net/Cargo.toml index 4b9df60ef..752a79c88 100644 --- a/net/Cargo.toml +++ b/net/Cargo.toml @@ -13,9 +13,11 @@ netdevsim = [] bolero = ["dep:bolero", "id/bolero"] test_buffer = [] +[target.'cfg(unix)'.dependencies] +atomic-instant-full = { workspace = true } + [dependencies] arrayvec = { workspace = true, features = ["serde", "std"] } -atomic-instant-full = { workspace = true } bitflags = { workspace = true } bolero = { workspace = true, features = ["std"], optional = true } bytecheck = { workspace = true } @@ -24,7 +26,6 @@ derive_builder = { workspace = true, features = ["alloc"] } downcast-rs = { workspace = true, features = ["sync"] } etherparse = { workspace = true, features = ["std"] } id = { workspace = true } -linux-raw-sys = { workspace = true, features = ["std", "if_ether"] } multi_index_map = { workspace = true, default-features = false, features = ["serde"] } rapidhash = { workspace = true } rkyv = { workspace = true, features = ["alloc", "bytecheck"]} diff --git a/net/src/interface/mtu.rs b/net/src/interface/mtu.rs index 2abbf1fba..e24b66383 100644 --- a/net/src/interface/mtu.rs +++ b/net/src/interface/mtu.rs @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Open Network Fabric Authors -use linux_raw_sys::if_ether; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; use std::fmt::{Display, Formatter}; @@ -16,7 +15,8 @@ pub struct Mtu(NonZero); impl Mtu { pub(crate) const MIN_U32: u32 = 1280; // 1280 IPv6 minimum MTU - pub(crate) const MAX_U32: u32 = if_ether::ETH_MAX_MTU; + pub(crate) const MAX_U32: u32 = 65535; + pub(crate) const DEFAULT_U32: u32 = 1500; /// The minimum legal MTU for an IPv6 interface is 1280 bytes. diff --git a/net/src/lib.rs b/net/src/lib.rs index 001df2670..564583a6e 100644 --- a/net/src/lib.rs +++ b/net/src/lib.rs @@ -12,12 +12,14 @@ clippy::expect_used, clippy::panic )] +#![cfg_attr(not(unix), allow(unused))] // for wasm32 builds #![allow(clippy::should_panic_without_expect)] // we panic in contract checks with simple unwrap() pub mod addr_parse_error; pub mod buffer; pub mod checksum; pub mod eth; +#[cfg(unix)] pub mod flows; pub mod headers; pub mod icmp4; @@ -28,6 +30,7 @@ pub mod ip; pub mod ip_auth; pub mod ipv4; pub mod ipv6; +#[cfg(unix)] pub mod packet; pub mod parse; pub mod pci; @@ -38,6 +41,7 @@ pub mod vlan; pub mod vxlan; // re-export +#[cfg(unix)] pub use flows::flow_key::{ self, ExtendedFlowKey, FlowKey, FlowKeyData, IcmpProtoKey, IpProtoKey, TcpProtoKey, UdpProtoKey, }; diff --git a/net/src/packet/mod.rs b/net/src/packet/mod.rs index 64e5cdab7..9970cc74e 100644 --- a/net/src/packet/mod.rs +++ b/net/src/packet/mod.rs @@ -3,6 +3,8 @@ //! Packet struct and methods +#![cfg(unix)] + mod display; mod hash; mod meta; From 84d4883a31b12d53918c3e050a30f33d345e3d8d Mon Sep 17 00:00:00 2001 From: Daniel Noland Date: Tue, 24 Mar 2026 19:28:02 -0600 Subject: [PATCH 08/17] build(hardware): gate sysfs dependency and nic module behind cfg(unix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move sysfs from unconditional [dependencies] to [target.'cfg(unix)'.dependencies]. Gate pub mod nic with #[cfg(unix)]. The sysfs crate is inherently unix-only, so a platform gate is more appropriate than a feature flag — consumers don't need to opt in or out. This allows the hardware crate to compile for wasm32-wasip1 without any feature configuration. Co-authored-by: Claude (Opus 4.6) Signed-off-by: Daniel Noland --- hardware/Cargo.toml | 4 +++- hardware/src/lib.rs | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/hardware/Cargo.toml b/hardware/Cargo.toml index 3c889842e..edd376c09 100644 --- a/hardware/Cargo.toml +++ b/hardware/Cargo.toml @@ -18,7 +18,6 @@ serde = ["dep:serde", "id/serde"] [dependencies] # internal id = { workspace = true, features = ["rkyv"] } -sysfs = { workspace = true } # external bolero = { workspace = true, optional = true, features = ["alloc"] } @@ -33,6 +32,9 @@ strum = { workspace = true, features = ["derive"] } thiserror = { workspace = true, features = ["std"] } tracing = { workspace = true, features = ["std"] } +[target.'cfg(unix)'.dependencies] +sysfs = { workspace = true } + [dev-dependencies] # internal fixin = { workspace = true, features = [] } diff --git a/hardware/src/lib.rs b/hardware/src/lib.rs index 70ed25389..10834b353 100644 --- a/hardware/src/lib.rs +++ b/hardware/src/lib.rs @@ -17,6 +17,7 @@ use crate::pci::bridge::BridgeAttributes; pub mod group; pub mod mem; +#[cfg(unix)] pub mod nic; pub mod os; pub mod pci; From b59a2aac930f1d99767d252451963643b1ead8f7 Mon Sep 17 00:00:00 2001 From: Daniel Noland Date: Tue, 24 Mar 2026 19:28:27 -0600 Subject: [PATCH 09/17] build(k8s-intf): feature-gate K8s client dependencies Add a 'client' feature (default-enabled) to k8s-intf that gates the full K8s client, watcher, and status-reporting functionality (futures, linkme, rustls, tokio, tracectl, and kube client/runtime/rustls-tls features). When disabled, only the generated CRD types are available. This allows the validator crate to depend on k8s-intf with default-features=false for wasm32-wasip1 builds that only need CRD type definitions. Update downstream consumers: - k8s-less, mgmt: explicitly enable the client feature - validator: use default-features = false Co-authored-by: Claude (Opus 4.6) Signed-off-by: Daniel Noland --- k8s-intf/Cargo.toml | 26 ++++++++++++++++++++------ k8s-intf/src/lib.rs | 3 +++ k8s-less/Cargo.toml | 2 +- mgmt/Cargo.toml | 2 +- validator/Cargo.toml | 2 +- 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/k8s-intf/Cargo.toml b/k8s-intf/Cargo.toml index 51836cddb..299047c13 100644 --- a/k8s-intf/Cargo.toml +++ b/k8s-intf/Cargo.toml @@ -6,6 +6,20 @@ publish.workspace = true version.workspace = true [features] +default = ["client"] +# Enables the full K8s client, watcher, and status-reporting functionality. +# Disable this feature (default-features = false) for WASM/WASI builds that +# only need the generated CRD types. +client = [ + "dep:futures", + "dep:linkme", + "dep:rustls", + "dep:tokio", + "dep:tracectl", + "kube/client", + "kube/runtime", + "kube/rustls-tls", +] bolero = ["dep:bolero", "dep:hardware", "dep:net", "dep:lpm", "net/test_buffer", "net/bolero", "hardware/bolero"] [dependencies] @@ -13,23 +27,23 @@ bolero = ["dep:bolero", "dep:hardware", "dep:net", "dep:lpm", "net/test_buffer", hardware = { workspace = true, optional = true, features = ["bolero"] } lpm = { workspace = true, optional = true } net = { workspace = true, optional = true, features = ["bolero"] } -tracectl = { workspace = true } +tracectl = { workspace = true, optional = true } tracing = { workspace = true } # external bolero = { workspace = true, optional = true } -futures = { workspace = true } -kube = { workspace = true, features = ["client", "derive", "runtime", "rustls-tls"] } +futures = { workspace = true, optional = true } +kube = { workspace = true, features = ["derive"] } kube-core = { workspace = true } k8s-openapi = { workspace = true, features = ["latest", "schemars", "std"] } -linkme = { workspace = true } -rustls = { workspace = true, features = ["aws-lc-rs"] } +linkme = { workspace = true, optional = true } +rustls = { workspace = true, features = ["aws-lc-rs"], optional = true } schemars = { workspace = true, features = ["derive", "std"] } serde = { workspace = true } serde_json = { workspace = true } serde_yaml_ng = { workspace = true } thiserror = { workspace = true } -tokio = { workspace = true } +tokio = { workspace = true, optional = true } [dev-dependencies] bolero = { workspace = true } diff --git a/k8s-intf/src/lib.rs b/k8s-intf/src/lib.rs index 0d5bad7a1..cf0d486d8 100644 --- a/k8s-intf/src/lib.rs +++ b/k8s-intf/src/lib.rs @@ -7,7 +7,9 @@ #[cfg(any(test, feature = "bolero"))] pub mod bolero; +#[cfg(feature = "client")] pub mod client; +#[cfg(feature = "client")] pub mod utils; #[allow(clippy::all, clippy::pedantic)] @@ -16,4 +18,5 @@ pub mod gateway_agent_crd { include!(concat!(env!("OUT_DIR"), "/gateway_agent_crd.rs")); } +#[cfg(feature = "client")] pub use client::watch_gateway_agent_crd; diff --git a/k8s-less/Cargo.toml b/k8s-less/Cargo.toml index 71250994c..c7c38407e 100644 --- a/k8s-less/Cargo.toml +++ b/k8s-less/Cargo.toml @@ -8,7 +8,7 @@ repository.workspace = true [dependencies] inotify = { workspace = true, features = ["stream"] } -k8s-intf = { workspace = true } +k8s-intf = { workspace = true, features = ["client"] } tokio = { workspace = true, features = ["macros", "rt", "fs"] } tracectl = { workspace = true } tracing = { workspace = true } diff --git a/mgmt/Cargo.toml b/mgmt/Cargo.toml index aed33e2e5..5eccb98ed 100644 --- a/mgmt/Cargo.toml +++ b/mgmt/Cargo.toml @@ -23,7 +23,7 @@ concurrency = { workspace = true } flow-filter = { workspace = true } id = { workspace = true } interface-manager = { workspace = true } -k8s-intf = { workspace = true } +k8s-intf = { workspace = true, features = ["client"] } k8s-less = { workspace = true } lpm = { workspace = true } nat = { workspace = true } diff --git a/validator/Cargo.toml b/validator/Cargo.toml index 7da65435f..b1c1c662d 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -11,7 +11,7 @@ path = "src/main.rs" [dependencies] config = { workspace = true } -k8s-intf = { workspace = true } +k8s-intf = { workspace = true, default-features = false } serde = { workspace = true } serde_yaml_ng = { workspace = true } From 987e622aa74fd61383b769c600992e0b3bbd83b2 Mon Sep 17 00:00:00 2001 From: Daniel Noland Date: Tue, 24 Mar 2026 20:53:03 -0600 Subject: [PATCH 10/17] build(config): clean up Cargo.toml dependency features - Sort k8s-intf alphabetically among internal deps - Simplify derive_builder: remove redundant default-features = false, features = ["default"] - Simplify bolero dev-dep: remove redundant default-features = false Co-authored-by: Claude (Opus 4.6) Signed-off-by: Daniel Noland --- config/Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/Cargo.toml b/config/Cargo.toml index 48bc05abd..cc4883fc8 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -9,15 +9,15 @@ version.workspace = true # internal common = { workspace = true } hardware = { workspace = true } +k8s-intf = { workspace = true } net = { workspace = true } lpm = { workspace = true } tracectl = { workspace = true } -k8s-intf = { workspace = true } # external arc-swap = { workspace = true } chrono = { workspace = true, features = ["alloc", "std"] } -derive_builder = { workspace = true, default-features = false, features = ["default"] } +derive_builder = { workspace = true, features = [] } ipnet = { workspace = true } linkme = { workspace = true } multi_index_map = { workspace = true, features = ["serde"] } @@ -33,7 +33,7 @@ hardware = { workspace = true, features = ["bolero"] } k8s-intf = { workspace = true, features = ["bolero"] } # external -bolero = { workspace = true, default-features = false, features = ["alloc"] } +bolero = { workspace = true, features = ["alloc"] } caps = { workspace = true } ipnet = { workspace = true } pretty_assertions = { workspace = true, features = ["std"] } From 2a092d4b3a945065d3b6b709db058885b6e9d4ed Mon Sep 17 00:00:00 2001 From: Daniel Noland Date: Tue, 24 Mar 2026 20:53:22 -0600 Subject: [PATCH 11/17] build(config): gate tracectl and linkme behind cfg(unix) Move linkme and tracectl to [target.'cfg(unix)'.dependencies] in config/Cargo.toml. Add #[cfg(unix)] / #[cfg(not(unix))] conditional imports and fallback definitions for LevelFilter and DEFAULT_DEFAULT_LOGLEVEL on non-unix platforms. Split TracingConfig::validate() into platform-specific implementations: - Unix: real validation via tracectl - Non-unix: no-op (returns Ok) Gate trace_target! macro and ConfigError::Tracing variant with cfg(unix). Co-authored-by: Claude (Opus 4.6) Signed-off-by: Daniel Noland --- config/Cargo.toml | 6 ++-- config/src/converters/k8s/config/tracecfg.rs | 3 ++ config/src/errors.rs | 4 +++ config/src/internal/device/tracecfg.rs | 29 ++++++++++++++++++-- config/src/lib.rs | 2 ++ 5 files changed, 40 insertions(+), 4 deletions(-) diff --git a/config/Cargo.toml b/config/Cargo.toml index cc4883fc8..be323bf85 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -12,19 +12,21 @@ hardware = { workspace = true } k8s-intf = { workspace = true } net = { workspace = true } lpm = { workspace = true } -tracectl = { workspace = true } # external arc-swap = { workspace = true } chrono = { workspace = true, features = ["alloc", "std"] } derive_builder = { workspace = true, features = [] } ipnet = { workspace = true } -linkme = { workspace = true } multi_index_map = { workspace = true, features = ["serde"] } ordermap = { workspace = true, features = ["std"] } thiserror = { workspace = true } tracing = { workspace = true, features = ["attributes"] } +[target.'cfg(unix)'.dependencies] +linkme = { workspace = true } +tracectl = { workspace = true } + [dev-dependencies] # internal pipeline = { workspace = true } # should be removed w/ NAT diff --git a/config/src/converters/k8s/config/tracecfg.rs b/config/src/converters/k8s/config/tracecfg.rs index 19f893eb2..6f000ec00 100644 --- a/config/src/converters/k8s/config/tracecfg.rs +++ b/config/src/converters/k8s/config/tracecfg.rs @@ -1,7 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Open Network Fabric Authors +#[cfg(unix)] use tracectl::LevelFilter; +#[cfg(not(unix))] +use tracing::metadata::LevelFilter; use k8s_intf::gateway_agent_crd::GatewayAgentGatewayLogs; diff --git a/config/src/errors.rs b/config/src/errors.rs index f671b61a4..ef5a4221f 100644 --- a/config/src/errors.rs +++ b/config/src/errors.rs @@ -95,8 +95,12 @@ pub enum ConfigError { Invalid(String), // tracing + #[cfg(unix)] #[error("Failed to set tracing configuration: {0}")] Tracing(#[from] tracectl::TraceCtlError), + #[cfg(not(unix))] + #[error("Failed to set tracing configuration: {0}")] + Tracing(String), // Community mappings #[error("Could not assign BGP community to VPC peering {0}")] diff --git a/config/src/internal/device/tracecfg.rs b/config/src/internal/device/tracecfg.rs index 20851dd3a..f38806bd3 100644 --- a/config/src/internal/device/tracecfg.rs +++ b/config/src/internal/device/tracecfg.rs @@ -7,9 +7,17 @@ use crate::ConfigResult; use ordermap::OrderMap; +use tracing::debug; + +#[cfg(unix)] use tracectl::DEFAULT_DEFAULT_LOGLEVEL; +#[cfg(unix)] use tracectl::{LevelFilter, get_trace_ctl}; -use tracing::debug; + +#[cfg(not(unix))] +use tracing::metadata::LevelFilter; +#[cfg(not(unix))] +const DEFAULT_DEFAULT_LOGLEVEL: LevelFilter = LevelFilter::INFO; #[derive(Clone, Debug)] pub struct TracingConfig { @@ -35,9 +43,26 @@ impl TracingConfig { pub fn add_tag(&mut self, tag: &str, level: LevelFilter) { let _ = self.tags.insert(tag.to_string(), level); } + /// Validate the tracing configuration. + /// + /// # Errors + /// + /// Returns an error if any configured trace tag is unknown. + #[cfg(unix)] pub fn validate(&self) -> ConfigResult { debug!("Validating tracing configuration.."); - let tags: Vec<&str> = self.tags.keys().map(|k| k.as_str()).collect(); + let tags: Vec<&str> = self.tags.keys().map(String::as_str).collect(); Ok(get_trace_ctl().check_tags(&tags)?) } + + /// Validate the tracing configuration (no-op on non-unix platforms). + /// + /// # Errors + /// + /// Always returns `Ok(())` on non-unix platforms. + #[cfg(not(unix))] + pub fn validate(&self) -> ConfigResult { + debug!("Validating tracing configuration (no-op on this platform).."); + Ok(()) + } } diff --git a/config/src/lib.rs b/config/src/lib.rs index 57115bd98..833755edf 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -32,5 +32,7 @@ pub use gwconfig::{GwConfig, GwConfigMeta}; pub use internal::InternalConfig; pub use internal::device::DeviceConfig; +#[cfg(unix)] use tracectl::trace_target; +#[cfg(unix)] trace_target!("mgmt", LevelFilter::DEBUG, &["management"]); From 390ff10ef24ccf0b9a83d25f97539c46d82398d8 Mon Sep 17 00:00:00 2001 From: Daniel Noland Date: Tue, 24 Mar 2026 19:29:47 -0600 Subject: [PATCH 12/17] build: wire up workspace deps for wasm32 validator build Workspace-level changes to support cross-platform builds: - Set default-features = false for k8s-intf workspace dep - Remove linux-raw-sys from workspace deps (now a platform-conditional dep in net) - Move rt-multi-thread from workspace tokio to routing/Cargo.toml - Move codec from workspace tokio-util to routing/Cargo.toml - Fix trailing whitespace on netgauze-bgp-pkt line Update Cargo.lock to reflect dependency changes. Co-authored-by: Claude (Opus 4.6) Signed-off-by: Daniel Noland --- Cargo.lock | 37 +++++++++++++++---------------------- Cargo.toml | 9 ++++----- routing/Cargo.toml | 2 +- 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b71383752..4df20559d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,7 +126,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -137,7 +137,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -146,12 +146,6 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" - [[package]] name = "arc-swap" version = "1.9.0" @@ -507,7 +501,6 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98a5782f2650f80d533f58ec339c6dce4cc5428f9c2755894f98156f52af81f2" dependencies = [ - "arbitrary", "bolero-generator-derive", "either", "getrandom 0.3.4", @@ -877,7 +870,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2090,7 +2083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc 0.2.183", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3896,7 +3889,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4910,7 +4903,7 @@ dependencies = [ "errno", "libc 0.2.183", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5425,7 +5418,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc 0.2.183", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5532,9 +5525,9 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" [[package]] name = "symbolic-common" -version = "12.17.2" +version = "12.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "751a2823d606b5d0a7616499e4130a516ebd01a44f39811be2b9600936509c23" +checksum = "52ca086c1eb5c7ee74b151ba83c6487d5d33f8c08ad991b86f3f58f6629e68d5" dependencies = [ "debugid", "memmap2 0.9.10", @@ -5544,9 +5537,9 @@ dependencies = [ [[package]] name = "symbolic-demangle" -version = "12.17.2" +version = "12.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b237cfbe320601dd24b4ac817a5b68bb28f5508e33f08d42be0682cadc8ac9" +checksum = "baa911a28a62823aaf2cc2e074212492a3ee69d0d926cc8f5b12b4a108ff5c0c" dependencies = [ "cpp_demangle", "rustc-demangle", @@ -5611,7 +5604,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5627,12 +5620,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c0c0a69df..e9a783a60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,7 +65,7 @@ hardware = { path = "./hardware", package = "dataplane-hardware", features = [] id = { path = "./id", package = "dataplane-id", features = [] } init = { path = "./init", package = "dataplane-init", features = [] } interface-manager = { path = "./interface-manager", package = "dataplane-interface-manager", features = [] } -k8s-intf = { path = "./k8s-intf", package = "dataplane-k8s-intf", features = [] } +k8s-intf = { path = "./k8s-intf", package = "dataplane-k8s-intf", default-features = false, features = [] } k8s-less = { path = "./k8s-less", package = "dataplane-k8s-less", features = [] } left-right-tlcache = { path = "./left-right-tlcache", package = "dataplane-left-right-tlcache", features = [] } lpm = { path = "./lpm", package = "dataplane-lpm", features = [] } @@ -129,7 +129,6 @@ kube-core = { version = "3.1.0", default-features = false, features = [] } left-right = { version = "0.11.7", default-features = false, features = [] } libc = { version = "1.0.0-alpha.3", default-features = false, features = [] } linkme = { version = "0.3.35", default-features = false, features = [] } -linux-raw-sys = { version = "0.12.1", default-features = false, features = [] } log = { version = "0.4.29", default-features = false, features = [] } # TODO: try to remove this loom = { version = "0.7.2", default-features = false, features = [] } memmap2 = { version = "0.9.10", default-features = false, features = [] } @@ -141,7 +140,7 @@ multi_index_map = { version = "0.15.1", default-features = false, features = [] n-vm = { git = "https://github.com/githedgehog/testn.git", tag = "v0.0.9", default-features = false, features = [], package = "n-vm" } netdev = { version = "0.41.0", default-features = false, features = [] } nix = { version = "0.31.2", default-features = false, features = [] } -netgauze-bgp-pkt = { version = "0.10.0", features = [] } +netgauze-bgp-pkt = { version = "0.10.0", features = [] } netgauze-bmp-pkt = { version = "0.10.0", features = ["codec"] } num-derive = { version = "0.4.2", default-features = false, features = [] } num-traits = { version = "0.2.19", default-features = false, features = [] } @@ -178,9 +177,9 @@ strum_macros = { version = "0.28.0", default-features = false, features = [] } syn = { version = "2.0.117", default-features = false, features = [] } thiserror = { version = "2.0.18", default-features = false, features = [] } thread_local = { version = "1.1.9", default-features = false, features = [] } -tokio = { version = "1.50.0", default-features = false, features = ["rt-multi-thread"] } +tokio = { version = "1.50.0", default-features = false, features = [] } tracing = { version = "0.1.44", default-features = false, features = [] } -tokio-util = { version = "0.7.18", default-features = false, features = ["codec"] } +tokio-util = { version = "0.7.18", default-features = false, features = [] } tonic = { version = "0.14.5", default-features = false, features = [] } tracing-error = { version = "0.2.1", default-features = false, features = [] } tracing-subscriber = { version = "0.3.23", default-features = false, features = [] } diff --git a/routing/Cargo.toml b/routing/Cargo.toml index 508e1f89f..b5ee666bd 100644 --- a/routing/Cargo.toml +++ b/routing/Cargo.toml @@ -39,7 +39,7 @@ netgauze-bmp-pkt = { workspace = true } serde = { workspace = true, features = ["derive"] } strum = { workspace = true } thiserror = { workspace = true } -tokio = { workspace = true, features = ["fs", "io-util", "sync", "rt", "net", "macros"] } +tokio = { workspace = true, features = ["fs", "io-util", "sync", "rt", "net", "macros", "rt-multi-thread"] } tokio-util = { workspace = true, features = ["codec"] } tracing = { workspace = true } From 58b7d1bc088150e265e8295b0053f5320e0346b1 Mon Sep 17 00:00:00 2001 From: Daniel Noland Date: Tue, 24 Mar 2026 19:29:56 -0600 Subject: [PATCH 13/17] build(nix): add wasm32-wasip1 rust targets - Add wasm32-wasip1 platform entry in nix/platforms.nix (arch table + info) - Accept kernel parameter in platforms.nix for wasip1 support - Add wasm32-wasip1 to rust toolchain targets in nix/overlays/llvm.nix - Add empty march.wasm32 in nix/profiles.nix Co-authored-by: Claude (Opus 4.6) Signed-off-by: Daniel Noland --- nix/overlays/llvm.nix | 1 + nix/platforms.nix | 14 ++++++++++++++ nix/profiles.nix | 1 + 3 files changed, 16 insertions(+) diff --git a/nix/overlays/llvm.nix b/nix/overlays/llvm.nix index 5bffc8823..8b22cbc74 100644 --- a/nix/overlays/llvm.nix +++ b/nix/overlays/llvm.nix @@ -42,6 +42,7 @@ let ]; targets = [ platform.info.target + "wasm32-wasip1" ]; }; rustPlatform' = final.makeRustPlatform { diff --git a/nix/platforms.nix b/nix/platforms.nix index 9d8fc92c7..872d78e3c 100644 --- a/nix/platforms.nix +++ b/nix/platforms.nix @@ -76,6 +76,11 @@ let NIX_CFLAGS_LINK = [ ]; }; }; + wasm32-wasip1 = { + arch = "wasm32"; + march = "wasm32"; + override.stdenv.env = { }; + }; }; in lib.fix ( @@ -126,6 +131,15 @@ lib.fix ( }; }; }; + wasm32 = { + wasip1 = { + unknown = { + target = "wasm32-wasip1"; + machine = "wasm32"; + nixarch = "wasi32"; + }; + }; + }; } .${final.arch}.${kernel}.${libc}; } diff --git a/nix/profiles.nix b/nix/profiles.nix index 75bc1a38c..c59fa9443 100644 --- a/nix/profiles.nix +++ b/nix/profiles.nix @@ -96,6 +96,7 @@ let march.aarch64.NIX_CXXFLAGS_COMPILE = march.aarch64.NIX_CFLAGS_COMPILE; march.aarch64.NIX_CFLAGS_LINK = [ ]; march.aarch64.RUSTFLAGS = [ ] ++ (map (flag: "-Clink-arg=${flag}") march.aarch64.NIX_CFLAGS_LINK); + march.wasm32 = { }; sanitize.address.NIX_CFLAGS_COMPILE = [ "-fsanitize=address,local-bounds" ]; From 2f3839bfa4255df8529dc76f6db38859af4eb2e0 Mon Sep 17 00:00:00 2001 From: Daniel Noland Date: Tue, 24 Mar 2026 20:54:36 -0600 Subject: [PATCH 14/17] build(nix): fix crane import and src filter - Remove unnecessary pkgs argument from crane import - Fix src filter: the path-based filters (justfile, markdown, json, cHeader, outputs) receive full paths from cleanSourceWith, but were matching against basenames. Use baseNameOf for those filters while passing the full path to craneLib.filterCargoSources which needs it. - Use lib.cleanSource as base source to pre-filter .git and other VCS artifacts - Add explicit name for reproducible store paths Co-authored-by: Claude (Opus 4.6) Signed-off-by: Daniel Noland --- default.nix | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/default.nix b/default.nix index 8b8364d78..5a826b61c 100644 --- a/default.nix +++ b/default.nix @@ -104,7 +104,7 @@ let executable = false; destination = "/.clangd"; }; - crane = import sources.crane { pkgs = pkgs; }; + crane = import sources.crane { }; craneLib = crane.craneLib.overrideToolchain pkgs.rust-toolchain; devroot = pkgs.symlinkJoin { name = "dataplane-dev-shell"; @@ -159,13 +159,17 @@ let outputsFilter = p: _type: (p != "target") && (p != "sysroot") && (p != "devroot") && (p != ".git"); src = pkgs.lib.cleanSourceWith { filter = - p: t: + full-path: t: + let + p = baseNameOf full-path; + in (justfileFilter p t) || (markdownFilter p t) || (jsonFilter p t) || (cHeaderFilter p t) - || ((outputsFilter p t) && (craneLib.filterCargoSources p t)); - src = ./.; + || ((outputsFilter p t) && (craneLib.filterCargoSources full-path t)); + src = lib.cleanSource ./.; + name = "source"; }; cargoVendorDir = craneLib.vendorMultipleCargoDeps { cargoLockList = [ From 13df6a79cbf7064b0f82f05cb866ea475ea18b51 Mon Sep 17 00:00:00 2001 From: Daniel Noland Date: Tue, 24 Mar 2026 20:54:58 -0600 Subject: [PATCH 15/17] build(nix): refactor default.nix for wasm32 support Restructure default.nix to support wasm32-wasip1 alongside native builds This commit both adds and removes "scuff" in that the branch conditions are somewhat unfortunate and fragile, but the complexity of the pkg imports is significantly reduced. I don't actually know a better way to do this so I think we need to live with the fragile conditionals. Co-authored-by: Claude (Opus 4.6) Signed-off-by: Daniel Noland --- default.nix | 228 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 130 insertions(+), 98 deletions(-) diff --git a/default.nix b/default.nix index 5a826b61c..94fa8dfe7 100644 --- a/default.nix +++ b/default.nix @@ -8,6 +8,7 @@ sanitize ? "", features ? "", default-features ? "true", + kernel ? "linux", tag ? "dev", }: let @@ -21,7 +22,12 @@ let builtins.filter (elm: builtins.isString elm) (builtins.split split-on string); lib = (import sources.nixpkgs { }).lib; platform' = import ./nix/platforms.nix { - inherit lib platform libc; + inherit + lib + platform + libc + kernel + ; }; sanitizers = split-str ",+" sanitize; cargo-features = split-str ",+" features; @@ -44,54 +50,53 @@ let profile = profile'; platform = platform'; }; - dev-pkgs = import sources.nixpkgs { - overlays = [ - overlays.rust - overlays.llvm - overlays.dataplane-dev - ]; - }; pkgs = - (import sources.nixpkgs { - overlays = [ - overlays.rust - overlays.llvm - overlays.dataplane - ]; - }).pkgsCross.${platform'.info.nixarch}; - frr-pkgs = - (import sources.nixpkgs { - overlays = [ - overlays.rust - overlays.llvm - overlays.dataplane - overlays.frr - ]; - }).pkgsCross.${platform'.info.nixarch}; - sysroot = pkgs.pkgsHostHost.symlinkJoin { - name = "sysroot"; - paths = with pkgs.pkgsHostHost; [ - pkgs.pkgsHostHost.libc.dev # fully qualified: bare `libc` resolves to the "gnu" function argument, not pkgs.pkgsHostHost.libc - pkgs.pkgsHostHost.libc.out # (same as above) - fancy.dpdk-wrapper.dev - fancy.dpdk-wrapper.out - fancy.dpdk.dev - fancy.dpdk.static - fancy.hwloc.dev - fancy.hwloc.static - fancy.libbsd.dev - fancy.libbsd.static - fancy.libmd.dev - fancy.libmd.static - fancy.libnl.dev - fancy.libnl.static - fancy.libunwind.out - fancy.numactl.dev - fancy.numactl.static - fancy.rdma-core.dev - fancy.rdma-core.static - ]; - }; + let + over = import sources.nixpkgs { + overlays = [ + overlays.rust + overlays.llvm + overlays.dataplane + overlays.dataplane-dev + overlays.frr + ]; + }; + in + if platform != "wasm32-wasip1" then over.pkgsCross.${platform'.info.nixarch} else over; + sysroot = + if platform != "wasm32-wasip1" then + pkgs.symlinkJoin { + name = "sysroot"; + paths = with pkgs.pkgsHostHost; [ + pkgs.pkgsHostHost.libc.dev # fully qualified: bare `libc` resolves to the "gnu" function argument, not pkgs.pkgsHostHost.libc + pkgs.pkgsHostHost.libc.out # (same as above) + fancy.dpdk-wrapper.dev + fancy.dpdk-wrapper.out + fancy.dpdk.dev + fancy.dpdk.static + fancy.hwloc.dev + fancy.hwloc.static + fancy.libbsd.dev + fancy.libbsd.static + fancy.libmd.dev + fancy.libmd.static + fancy.libnl.dev + fancy.libnl.static + fancy.libunwind.out + fancy.numactl.dev + fancy.numactl.static + fancy.rdma-core.dev + fancy.rdma-core.static + ]; + } + else + pkgs.symlinkJoin { + name = "sysroot"; + paths = with pkgs.pkgsHostHost; [ + fancy.hwloc.dev + fancy.hwloc.static + ]; + }; clangd-config = pkgs.writeTextFile { name = ".clangd"; text = '' @@ -117,7 +122,7 @@ let libclang.lib lld ]) - ++ (with dev-pkgs; [ + ++ (with pkgs.pkgsBuildHost; [ bash cargo-bolero cargo-deny @@ -132,6 +137,7 @@ let llvmPackages'.clang # you need the host compiler in order to link proc macros llvmPackages'.llvm # needed for coverage npins + oras pkg-config rust-toolchain skopeo @@ -149,7 +155,7 @@ let LIBRARY_PATH = "${sysroot}/lib"; PKG_CONFIG_PATH = "${sysroot}/lib/pkgconfig"; LIBCLANG_PATH = "${devroot}/lib"; - GW_CRD_PATH = "${dev-pkgs.gateway-crd}/src/fabric/config/crd/bases"; + GW_CRD_PATH = "${pkgs.pkgsBuildHost.gateway-crd}/src/fabric/config/crd/bases"; }; }; justfileFilter = p: _type: builtins.match ".*\.justfile$" p != null; @@ -177,17 +183,28 @@ let "${pkgs.rust-toolchain.passthru.availableComponents.rust-src}/lib/rustlib/src/rust/library/Cargo.lock" ]; }; - target = pkgs.stdenv'.targetPlatform.rust.rustcTarget; - is-cross-compile = dev-pkgs.stdenv.hostPlatform.rust.rustcTarget != target; - cxx = if is-cross-compile then "${target}-clang++" else "clang++"; - strip = if is-cross-compile then "${target}-strip" else "strip"; - objcopy = if is-cross-compile then "${target}-objcopy" else "objcopy"; + # For wasm32, pkgs is the host nixpkgs (no pkgsCross), so ctarget resolves to the + # host platform (e.g. x86_64-unknown-linux-gnu). That means is-cross-compile is + # false for wasm, which is intentional: we don't want native cross-compilation + # tooling (strip, objcopy, prefixed clang) — cargo + rustc handle wasm natively. + # rustc-target is the actual --target we pass to cargo, which diverges from ctarget + # only for wasm. + ctarget = pkgs.stdenv'.targetPlatform.rust.rustcTarget; + rustc-target = + if platform == "wasm32-wasip1" then + "wasm32-wasip1" + else + pkgs.stdenv'.targetPlatform.rust.rustcTarget; + is-cross-compile = pkgs.stdenv'.hostPlatform.rust.rustcTarget != ctarget; + cxx = if is-cross-compile then "${ctarget}-clang++" else "clang++"; + strip = if is-cross-compile then "${ctarget}-strip" else "strip"; + objcopy = if is-cross-compile then "${ctarget}-objcopy" else "objcopy"; package-list = builtins.fromJSON ( builtins.readFile ( pkgs.runCommandLocal "package-list" { - TOMLQ = "${dev-pkgs.yq}/bin/tomlq"; - JQ = "${dev-pkgs.jq}/bin/jq"; + TOMLQ = "${pkgs.pkgsBuildHost.yq}/bin/tomlq"; + JQ = "${pkgs.pkgsBuildHost.jq}/bin/jq"; } '' $TOMLQ -r '.workspace.members | sort[]' ${src}/Cargo.toml | while read -r p; do @@ -201,7 +218,7 @@ let "-Zunstable-options" "-Zbuild-std=compiler_builtins,core,alloc,std,panic_unwind,panic_abort,sysroot,unwind" "-Zbuild-std-features=backtrace,panic-unwind,mem,compiler-builtins-mem" - "--target=${target}" + "--target=${rustc-target}" ] ++ (if default-features == "false" then [ "--no-default-features" ] else [ ]) ++ ( @@ -238,7 +255,7 @@ let removeReferencesToVendorDir = true; nativeBuildInputs = [ - (dev-pkgs.kopium) + (pkgs.pkgsBuildHost.kopium) cargo-nextest llvmPackages'.clang llvmPackages'.lld @@ -257,28 +274,32 @@ let C_INCLUDE_PATH = "${sysroot}/include"; LIBRARY_PATH = "${sysroot}/lib"; PKG_CONFIG_PATH = "${sysroot}/lib/pkgconfig"; - GW_CRD_PATH = "${dev-pkgs.gateway-crd}/src/fabric/config/crd/bases"; + GW_CRD_PATH = "${pkgs.pkgsBuildHost.gateway-crd}/src/fabric/config/crd/bases"; RUSTC_BOOTSTRAP = "1"; - RUSTFLAGS = builtins.concatStringsSep " " ( - profile'.RUSTFLAGS - ++ [ - "-Clinker=${pkgs.pkgsBuildHost.llvmPackages'.clang}/bin/${cxx}" - "-Clink-arg=--ld-path=${pkgs.pkgsBuildHost.llvmPackages'.lld}/bin/ld.lld" - "-Clink-arg=-L${sysroot}/lib" - # NOTE: this is basically a trick to make our source code available to debuggers. - # Normally remap-path-prefix takes the form --remap-path-prefix=FROM=TO where FROM and TO are directories. - # This is intended to map source code paths to generic, relative, or redacted paths. - # We are sorta using that mechanism in reverse here in that the empty FROM in the next expression maps our - # source code in the debug info from the current working directory to ${src} (the nix store path where we - # have copied our source code). - # - # This is nice in that it should allow us to include ${src} in a container with gdb / lldb + the debug files - # we strip out of the final binaries we cook and include a gdbserver binary in some - # debug/release-with-debug-tools containers. Then, connecting from the gdb/lldb container to the - # gdb/lldbserver container should allow us to actually debug binaries deployed to test machines. - "--remap-path-prefix==${src}" - ] - ); + RUSTFLAGS = + if rustc-target != "wasm32-wasip1" then + builtins.concatStringsSep " " ( + profile'.RUSTFLAGS + ++ [ + "-Clinker=${pkgs.pkgsBuildHost.llvmPackages'.clang}/bin/${cxx}" + "-Clink-arg=--ld-path=${pkgs.pkgsBuildHost.llvmPackages'.lld}/bin/ld.lld" + "-Clink-arg=-L${sysroot}/lib" + # NOTE: this is basically a trick to make our source code available to debuggers. + # Normally remap-path-prefix takes the form --remap-path-prefix=FROM=TO where FROM and TO are directories. + # This is intended to map source code paths to generic, relative, or redacted paths. + # We are sorta using that mechanism in reverse here in that the empty FROM in the next expression maps our + # source code in the debug info from the current working directory to ${src} (the nix store path where we + # have copied our source code). + # + # This is nice in that it should allow us to include ${src} in a container with gdb / lldb + the debug files + # we strip out of the final binaries we cook and include a gdbserver binary in some + # debug/release-with-debug-tools containers. Then, connecting from the gdb/lldb container to the + # gdb/lldbserver container should allow us to actually debug binaries deployed to test machines. + "--remap-path-prefix==${src}" + ] + ) + else + ""; }; } // args @@ -295,14 +316,28 @@ let postBuild = (orig.postBuild or "") + '' unset RUSTFLAGS; ''; - postInstall = (orig.postInstall or "") + '' - mkdir -p $debug/bin - for f in $out/bin/*; do - mv "$f" "$debug/bin/$(basename "$f")" - ${strip} --strip-debug "$debug/bin/$(basename "$f")" -o "$f" - ${objcopy} --add-gnu-debuglink="$debug/bin/$(basename "$f")" "$f" - done - ''; + postInstall = + (orig.postInstall or "") + + ( + if rustc-target != "wasm32-wasip1" then + '' + mkdir -p $debug/bin + for f in $out/bin/*; do + mv "$f" "$debug/bin/$(basename "$f")" + ${strip} --strip-debug "$debug/bin/$(basename "$f")" -o "$f" + ${objcopy} --add-gnu-debuglink="$debug/bin/$(basename "$f")" "$f" + done + '' + else + '' + mkdir -p $debug/bin + for f in $out/bin/*; do + mv "$f" "$debug/bin/$(basename "$f")" + ${pkgs.pkgsBuildHost.binaryen}/bin/wasm-opt "$debug/bin/$(basename "$f")" --strip-debug -O4 -o "$f" + # sadly there is no equivalent of gnu-debuglink in wasm world yet + done + '' + ); postFixup = (orig.postFixup or "") + '' rm -f $out/target.tar.zst ''; @@ -468,12 +503,12 @@ let ln -s "${workspace.dataplane}/bin/dataplane" "$tmp/bin/dataplane" ln -s "${workspace.cli}/bin/cli" "$tmp/bin/cli" ln -s "${workspace.init}/bin/dataplane-init" "$tmp/bin/dataplane-init" - ln -s "${workspace.dataplane}/bin/dataplane" "$tmp/dataplane" - ln -s "${workspace.cli}/bin/cli" "$tmp/dataplane-cli" - ln -s "${workspace.init}/bin/dataplane-init" "$tmp/dataplane-init" for i in "${pkgs.pkgsHostHost.busybox}/bin/"*; do ln -s "${pkgs.pkgsHostHost.busybox}/bin/busybox" "$tmp/bin/$(basename "$i")" done + ln -s "${workspace.dataplane}/bin/dataplane" "$tmp/dataplane" + ln -s "${workspace.init}/bin/dataplane-init" "$tmp/dataplane-init" + ln -s "${workspace.cli}/bin/cli" "$tmp/dataplane-cli" # we take some care to make the tar file reproducible here tar \ --create \ @@ -599,7 +634,7 @@ let contents = pkgs.buildEnv { name = "dataplane-frr-env"; pathsToLink = [ "/" ]; - paths = with frr-pkgs; [ + paths = with pkgs; [ bash coreutils dockerTools.usrBinEnv @@ -619,7 +654,7 @@ let }; fakeRootCommands = '' - #!${frr-pkgs.bash}/bin/bash + #!${pkgs.bash}/bin/bash set -euxo pipefail mkdir /tmp mkdir -p /run/frr/hh @@ -646,10 +681,9 @@ let pathsToLink = [ "/" ]; - paths = with frr-pkgs; [ + paths = with pkgs; [ bash coreutils - dockerTools.fakeNss dockerTools.usrBinEnv # TODO: frr-config's docker-start launches /bin/frr-agent which is not # present in the host container. A host-specific entrypoint script may @@ -666,7 +700,7 @@ let ]; }; fakeRootCommands = '' - #!${frr-pkgs.bash}/bin/bash + #!${pkgs.bash}/bin/bash set -euxo pipefail mkdir /tmp mkdir -p /run/frr/hh @@ -690,11 +724,9 @@ in inherit clippy containers - dev-pkgs devenv devroot docs - frr-pkgs dataplane package-list pkgs From 2a08858a3269eb9599701a58be31b3199cccbfb5 Mon Sep 17 00:00:00 2001 From: Daniel Noland Date: Tue, 24 Mar 2026 20:55:34 -0600 Subject: [PATCH 16/17] build(justfile): add wasm32 validator support - Add libc and kernel variables derived from platform for wasm32 support - Pass libc and kernel to nix build invocations - Default jobs to 8 - Add validator target to build-container (builds workspace.validator) - Add validator target to push-container (pushes via oras) - Update push recipe to include validator with wasm32-wasip1 platform Co-authored-by: Claude (Opus 4.6) Signed-off-by: Daniel Noland --- justfile | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/justfile b/justfile index 835ff2275..841d3f4ad 100644 --- a/justfile +++ b/justfile @@ -12,7 +12,13 @@ debug_justfile := "false" _just_debuggable_ := if debug_justfile == "true" { "set -x" } else { "" } # number of nix jobs to run in parallel -jobs := "1" +jobs := "8" + +# libc +libc := if platform == "wasm32-wasip1" { "unknown" } else { "gnu" } + +# kernel (linux or wasip1) +kernel := if platform == "wasm32-wasip1" { "wasip1" } else { "linux" } # List out the available commands [private] @@ -70,6 +76,7 @@ oci_name := "githedgehog/dataplane" oci_frr_prefix := "githedgehog/dpdk-sys/frr" oci_image_dataplane := oci_repo + "/" + oci_name + ":" + version oci_image_dataplane_debugger := oci_repo + "/" + oci_name + "/debugger:" + version +oci_image_dataplane_validator := oci_repo + "/" + oci_name + "/validator:" + version oci_image_frr_dataplane := oci_repo + "/" + oci_frr_prefix + ":" + version oci_image_frr_host := oci_repo + "/" + oci_frr_prefix + "-host:" + version @@ -88,6 +95,8 @@ build target="dataplane.tar" *args: nix build -f default.nix "${target}" \ --argstr profile '{{ profile }}' \ --argstr sanitize '{{ sanitize }}' \ + --argstr libc '{{ libc }}' \ + --argstr kernel '{{ kernel }}' \ --argstr features '{{ features }}' \ --argstr default-features '{{ default_features }}' \ --argstr instrumentation '{{ instrument }}' \ @@ -139,7 +148,7 @@ setup-roots *args: # Build the dataplane container image [script] -build-container target="dataplane" *args: (build (if target == "dataplane" { "dataplane.tar" } else { "containers." + target }) args) +build-container target="dataplane" *args: (build (if target == "dataplane" { "dataplane.tar" } else if target == "validator" { "workspace.validator" } else { "containers." + target }) args) {{ _just_debuggable_ }} declare -xr DOCKER_HOST="${DOCKER_HOST:-unix://{{docker_sock}}}" case "{{target}}" in @@ -165,6 +174,9 @@ build-container target="dataplane" *args: (build (if target == "dataplane" { "da docker tag "ghcr.io/githedgehog/dpdk-sys/frr-host:{{version}}" "{{oci_image_frr_host}}" echo "imported {{oci_image_frr_host}}" ;; + "validator") + echo "NOTE: validator image is wasm and not containerized" + ;; *) >&2 echo "{{target}}" not a valid container exit 99 @@ -192,18 +204,33 @@ push-container target="dataplane" *args: (build-container target args) && versio skopeo copy --src-daemon-host="${DOCKER_HOST}" {{ _skopeo_dest_insecure }} docker-daemon:{{oci_image_frr_host}} docker://{{oci_image_frr_host}} echo "Pushed {{ oci_image_frr_host }}" ;; + "validator") + if [ "{{platform}}" != "wasm32-wasip1" ]; then + >&2 echo "Pushing non wasm32-wasip1 validator images is not supported, set platform=wasm32-wasip1" + exit 1 + fi + pushd ./results/workspace.validator/bin + oras push --annotation version="{{ version }}" "{{ oci_image_dataplane_validator }}" ./validator.wasm + popd + echo "Pushed {{ oci_image_dataplane_validator }}" + ;; *) >&2 echo "{{target}}" not a valid container exit 99 esac +# Note: deliberately ignores all recipe parameters save version, debug_justfile, and oci_repo. # Pushes all release container images. -# Note: deliberately ignores all recipe parameters save version and debug_justfile. [script] push: {{ _just_debuggable_ }} - for container in dataplane frr.dataplane; do - nix-shell --run "just debug_justfile={{debug_justfile}} oci_repo={{oci_repo}} version={{version}} profile=release platform=x86-64-v3 sanitize= instrument=none push-container ${container}" + for container in dataplane frr.dataplane validator; do + if [ "${container}" = "validator" ]; then + platform="wasm32-wasip1" + else + platform="x86-64-v3" + fi + nix-shell --run "just debug_justfile={{debug_justfile}} oci_repo={{oci_repo}} version={{version}} profile=release platform=${platform} sanitize= instrument=none push-container ${container}" done # Print names of container images to build or push From 39c779a5de7bf99fe8e6afe591e27a6a5701b5fa Mon Sep 17 00:00:00 2001 From: Daniel Noland Date: Tue, 24 Mar 2026 20:55:42 -0600 Subject: [PATCH 17/17] ci: update dev.yml for validator builds and nix improvements - Add validator to build matrix (release-only via exclude) - Reorder nix-install before checkout for better caching - Add nix-shell caching step to avoid attributing fetch time to first step - Add oras login alongside docker login for wasm artifact pushing - Add platform logic: wasm32-wasip1 for validator, x86-64-v3 for others - Fix script quoting: use single-quoted heredoc for nix-shell --run Co-authored-by: Claude (Opus 4.6) Signed-off-by: Daniel Noland --- .github/workflows/dev.yml | 68 +++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 482f46d08..1e5f462b3 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -132,6 +132,7 @@ jobs: - tests.all - frr.dataplane - dataplane + - validator build: - name: "debug" profile: "debug" @@ -143,6 +144,10 @@ jobs: instrument: "none" debug_justfile: - "${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_justfile || false }}" + exclude: + - nix-target: validator + build: + name: "debug" steps: - name: "login to ghcr.io" uses: "docker/login-action@v4" @@ -151,9 +156,11 @@ jobs: username: "${{ github.actor }}" password: "${{ secrets.GITHUB_TOKEN }}" - - name: "login to image cache" - run: | - echo "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USERNAME" --password-stdin "$REGISTRY_URL" + - name: "Install nix" + uses: cachix/install-nix-action@v31 + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + nix_path: nixpkgs=channel:nixpkgs-unstable - name: "Checkout" uses: "actions/checkout@v6" @@ -161,12 +168,6 @@ jobs: persist-credentials: "false" fetch-depth: "0" - - name: "Install nix" - uses: cachix/install-nix-action@v31 - with: - github_access_token: ${{ secrets.GITHUB_TOKEN }} - nix_path: nixpkgs=channel:nixpkgs-unstable - - uses: "cachix/cachix-action@v17" with: name: "hedgehog" @@ -175,6 +176,17 @@ jobs: # prettier-ignore authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' + # this step exists to avoid attributing the amount of time it takes to fetch the nix-shell dependencies to + # whatever step happens to access the nix-shell first. + - name: "setup nix shell" + run: | + nix-shell --run "echo nix shell env cached" + + - name: "login to image cache" + run: | + docker login -u "$REGISTRY_USERNAME" --password-stdin "$REGISTRY_URL" <<<"${REGISTRY_PASSWORD}" + nix-shell --run "oras login -u \"$REGISTRY_USERNAME\" --password-stdin \"$REGISTRY_URL\" <<<\"${REGISTRY_PASSWORD}\"" + - name: "run pre-flight checks" if: ${{ matrix.nix-target == 'tests.all' }} run: | @@ -195,22 +207,28 @@ jobs: - name: "push container" if: ${{ matrix.nix-target != 'tests.all' }} run: | - nix-shell --run "just \ - debug_justfile=${{matrix.debug_justfile}} \ - check-dependencies" - for v in "" "version=${{ needs.version.outputs.version }}-${{ matrix.build.profile }}"; do - nix-shell --run " - just \ - docker_sock=/run/docker/docker.sock \ - debug_justfile=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_justfile || false }} \ - profile=${{ matrix.build.profile }} \ - sanitize=${{ matrix.build.sanitize }} \ - instrument=${{ matrix.build.instrument }} \ - oci_repo=ghcr.io \ - $v \ - push-container ${{ matrix.nix-target }} - " - done + nix-shell --run ' + just \ + debug_justfile=${{matrix.debug_justfile}} \ + check-dependencies + for v in "" "version=${{ needs.version.outputs.version }}-${{ matrix.build.profile }}"; do + if [ "${{ matrix.nix-target }}" = "validator" ]; then + platform="wasm32-wasip1" + else + platform="x86-64-v3" + fi + just \ + docker_sock=/run/docker/docker.sock \ + debug_justfile=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_justfile || false }} \ + profile=${{ matrix.build.profile }} \ + platform=${platform} \ + sanitize=${{ matrix.build.sanitize }} \ + instrument=${{ matrix.build.instrument }} \ + oci_repo=ghcr.io \ + $v \ + push-container ${{ matrix.nix-target }} + done + ' - name: "Setup tmate session for debug" if: ${{ failure() && github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}