diff --git a/dataplane/src/main.rs b/dataplane/src/main.rs index 795775445..08820b907 100644 --- a/dataplane/src/main.rs +++ b/dataplane/src/main.rs @@ -208,6 +208,7 @@ fn main() { hostname: gwname.clone(), processor_params: ConfigProcessorParams { router_ctl: setup.router.get_ctl_tx(), + pipeline_data: pipeline_factory().get_data(), vpcmapw: setup.vpcmapw, nattablesw: setup.nattablesw, natallocatorw: setup.natallocatorw, diff --git a/dataplane/src/packet_processor/mod.rs b/dataplane/src/packet_processor/mod.rs index 2e0fdba0e..ed943e507 100644 --- a/dataplane/src/packet_processor/mod.rs +++ b/dataplane/src/packet_processor/mod.rs @@ -21,8 +21,8 @@ use nat::stateless::NatTablesWriter; use nat::{IcmpErrorHandler, StatefulNat, StatelessNat}; use net::buffer::PacketBufferMut; -use pipeline::DynPipeline; use pipeline::sample_nfs::PacketDumper; +use pipeline::{DynPipeline, PipelineData}; use routing::{Router, RouterError, RouterParams}; @@ -73,8 +73,10 @@ pub(crate) fn start_router( let nattabler_factory = nattablesw.get_reader_factory(); let natallocator_factory = natallocatorw.get_reader_factory(); let portfw_factory = portfw_w.reader().factory(); + let pdata = Arc::from(PipelineData::new(0)); let pipeline_builder = move || { + let pdata_clone = pdata.clone(); // Build network functions let stage_ingress = Ingress::new("Ingress", iftr_factory.handle()); let stage_egress = Egress::new("Egress", iftr_factory.handle(), atabler_factory.handle()); @@ -101,6 +103,7 @@ pub(crate) fn start_router( // Build the pipeline for a router. The composition of the pipeline (in stages) is currently // hard-coded. In any pipeline, the Stats and ExpirationsNF stages should go last DynPipeline::new() + .set_data(pdata_clone) .add_stage(stage_ingress) .add_stage(iprouter1) .add_stage(flow_lookup) diff --git a/flow-filter/src/lib.rs b/flow-filter/src/lib.rs index 4dca8167c..9bd13b3b5 100644 --- a/flow-filter/src/lib.rs +++ b/flow-filter/src/lib.rs @@ -21,11 +21,12 @@ use net::flows::FlowStatus; use net::flows::flow_info_item::ExtractRef; use net::headers::{Transport, TryIp, TryTransport}; use net::packet::{DoneReason, Packet, VpcDiscriminant}; -use pipeline::NetworkFunction; +use pipeline::{NetworkFunction, PipelineData}; use std::collections::HashSet; use std::fmt::{Display, Write}; use std::net::IpAddr; use std::num::NonZero; +use std::sync::Arc; use tracing::{debug, error}; mod filter_rw; @@ -45,6 +46,7 @@ trace_target!("flow-filter", LevelFilter::INFO, &["pipeline"]); pub struct FlowFilter { name: String, tablesr: FlowFilterTableReader, + pipeline_data: Arc, } impl FlowFilter { @@ -53,6 +55,7 @@ impl FlowFilter { Self { name: name.to_string(), tablesr, + pipeline_data: Arc::from(PipelineData::default()), } } @@ -95,6 +98,45 @@ impl FlowFilter { Ok(Some(*dst_vpcd)) } + fn bypass_with_flow_info( + &self, + packet: &mut Packet, + genid: i64, + ) -> bool { + let Some(flow_info) = &packet.meta().flow_info else { + debug!("Packet does not contain any flow-info"); + return false; + }; + let flow_genid = flow_info.genid(); + if flow_genid < genid { + debug!("Packet has flow-info ({flow_genid} < {genid}). Need to re-evaluate..."); + return false; + } + let status = flow_info.status(); + if status != FlowStatus::Active { + debug!("Found flow-info but its status is {status}. Need to re-evaluate..."); + return false; + } + + let vpcd = flow_info + .locked + .read() + .unwrap() + .dst_vpcd + .as_ref() + .and_then(|d| d.extract_ref::()) + .copied(); + + debug!("Packet can bypass filter due to flow {flow_info}"); + + if set_nat_requirements_from_flow_info(packet).is_err() { + debug!("Failed to set nat requirements"); + return false; + } + packet.meta_mut().dst_vpcd = vpcd; + true + } + /// Process a packet. fn process_packet( &self, @@ -102,6 +144,12 @@ impl FlowFilter { packet: &mut Packet, ) { let nfi = &self.name; + let genid = self.pipeline_data.genid(); + + // bypass flow-filter if packet has flow-info and it is not outdated + if self.bypass_with_flow_info(packet, genid) { + return; + } let Some(net) = packet.try_ip() else { debug!("{nfi}: No IP headers found, dropping packet"); @@ -179,10 +227,19 @@ impl FlowFilter { // Drop the packet since we don't know destination and it is not an icmp error let Some(dst_vpcd) = dst_vpcd else { debug!("Could not determine dst vpcd. Dropping packet"); + // if packet referred to a flow, invalidate it + if let Some(flow_info) = packet.meta().flow_info.as_ref() { + flow_info.invalidate_pair(); + } packet.done(DoneReason::Filtered); return; }; + // packet is allowed. If it refers to a flow, update its genid, and that of the related flow if any + if let Some(flow_info) = &packet.meta().flow_info { + flow_info.set_genid_pair(genid); + } + debug!("{nfi}: Flow {tuple} is allowed, setting packet dst_vpcd to {dst_vpcd}"); packet.meta_mut().dst_vpcd = Some(dst_vpcd); } @@ -297,6 +354,10 @@ impl NetworkFunction for FlowFilter { packet.enforce() }) } + + fn set_data(&mut self, data: Arc) { + self.pipeline_data = data; + } } // Only used for Display diff --git a/mgmt/Cargo.toml b/mgmt/Cargo.toml index 6acefda0d..f39f381c6 100644 --- a/mgmt/Cargo.toml +++ b/mgmt/Cargo.toml @@ -28,6 +28,7 @@ k8s-less = { workspace = true } lpm = { workspace = true } nat = { workspace = true } net = { workspace = true } +pipeline = { workspace = true } rekon = { workspace = true } routing = { workspace = true } stats = { workspace = true } diff --git a/mgmt/src/processor/proc.rs b/mgmt/src/processor/proc.rs index 3bdbd4dfd..b66f02c36 100644 --- a/mgmt/src/processor/proc.rs +++ b/mgmt/src/processor/proc.rs @@ -26,6 +26,7 @@ use nat::portfw::build_port_forwarding_configuration; use nat::stateful::NatAllocatorWriter; use nat::stateless::NatTablesWriter; use nat::stateless::setup::build_nat_configuration; +use pipeline::PipelineData; use crate::processor::display::ConfigHistory; use crate::processor::gwconfigdb::GwConfigDatabase; @@ -77,6 +78,9 @@ pub struct ConfigProcessorParams { // channel to router pub router_ctl: RouterCtlSender, + // access data associated to pipeline + pub pipeline_data: Arc, + // writer for vpc mapping table pub vpcmapw: VpcMapWriter, @@ -623,6 +627,9 @@ impl ConfigProcessor { /* apply config in router */ apply_router_config(&kernel_vrfs, config, router_ctl).await?; + /* update the pipeline generation id, iff config was applied */ + self.proc_params.pipeline_data.set_genid(genid); + info!("Successfully applied config for genid {genid}"); Ok(()) } diff --git a/mgmt/src/tests/mgmt.rs b/mgmt/src/tests/mgmt.rs index f0741fc30..7b774baf0 100644 --- a/mgmt/src/tests/mgmt.rs +++ b/mgmt/src/tests/mgmt.rs @@ -17,6 +17,7 @@ pub mod test { use nat::stateless::NatTablesWriter; use net::eth::mac::Mac; use net::interface::Mtu; + use pipeline::PipelineData; use std::net::IpAddr; use std::net::Ipv4Addr; use std::str::FromStr; @@ -465,9 +466,13 @@ pub mod test { /* create VPC stats store (Arc) */ let vpc_stats_store = VpcStatsStore::new(); + /* pipeline data */ + let pipeline_data = Arc::from(PipelineData::default()); + /* build configuration of mgmt config processor */ let processor_config = ConfigProcessorParams { router_ctl, + pipeline_data, vpcmapw, nattablesw, natallocatorw, diff --git a/nat/src/portfw/flow_state.rs b/nat/src/portfw/flow_state.rs index 97139806a..98ab65cde 100644 --- a/nat/src/portfw/flow_state.rs +++ b/nat/src/portfw/flow_state.rs @@ -217,15 +217,9 @@ pub(crate) fn get_packet_port_fw_state( /// Invalidate the flow that this packet matched and the related one if any. pub(crate) fn invalidate_flow_state(packet: &Packet) { - let Some(flow_info) = packet.meta().flow_info.as_ref() else { - return; - }; - flow_info.invalidate(); - flow_info - .related - .as_ref() - .and_then(Weak::upgrade) - .inspect(|related| related.invalidate()); + if let Some(flow_info) = packet.meta().flow_info.as_ref() { + flow_info.invalidate_pair(); + } } /// Update the port-forwarding state of a flow entry after processing a packet. diff --git a/nat/src/portfw/mod.rs b/nat/src/portfw/mod.rs index 824414113..20d367e31 100644 --- a/nat/src/portfw/mod.rs +++ b/nat/src/portfw/mod.rs @@ -19,3 +19,6 @@ pub use portfwtable::access::{PortFwTableReader, PortFwTableReaderFactory, PortF pub use portfwtable::objects::{PortFwEntry, PortFwKey, PortFwTable}; pub use portfwtable::portrange::PortRange; pub use portfwtable::setup::build_port_forwarding_configuration; + +use tracectl::trace_target; +trace_target!("port-forwarding", LevelFilter::INFO, &["nat", "pipeline"]); diff --git a/nat/src/portfw/nf.rs b/nat/src/portfw/nf.rs index 90505d5ba..72a6369cc 100644 --- a/nat/src/portfw/nf.rs +++ b/nat/src/portfw/nf.rs @@ -11,7 +11,7 @@ use net::flows::{ExtractMut, ExtractRef, FlowInfo}; use net::headers::{TryIp, TryTcp, TryTransport}; use net::ip::{NextHeader, UnicastIpAddr}; use net::packet::{DoneReason, Packet, VpcDiscriminant}; -use pipeline::NetworkFunction; +use pipeline::{NetworkFunction, PipelineData}; use std::num::NonZero; use std::sync::Arc; use std::time::Instant; @@ -28,14 +28,12 @@ use crate::portfw::packet::{dnat_packet, nat_packet}; #[allow(unused)] use tracing::{debug, error, trace, warn}; -use tracectl::trace_target; -trace_target!("port-forwarding", LevelFilter::INFO, &["nat", "pipeline"]); - /// A port-forwarding network function pub struct PortForwarder { name: String, flow_table: Arc, fwtable: PortFwTableReader, + pipeline_data: Arc, } impl PortForwarder { @@ -46,6 +44,7 @@ impl PortForwarder { name: name.to_string(), flow_table, fwtable, + pipeline_data: Arc::from(PipelineData::default()), } } @@ -369,4 +368,8 @@ impl NetworkFunction for PortForwarder { packet.enforce() }) } + + fn set_data(&mut self, data: Arc) { + self.pipeline_data = data; + } } diff --git a/nat/src/stateful/mod.rs b/nat/src/stateful/mod.rs index 27d575945..1a0a9194a 100644 --- a/nat/src/stateful/mod.rs +++ b/nat/src/stateful/mod.rs @@ -22,8 +22,8 @@ use net::flow_key::{IcmpProtoKey, Uni}; use net::flows::{ExtractRef, FlowInfo}; use net::headers::{Net, Transport, TryIp, TryIpMut, TryTransportMut}; use net::packet::{DoneReason, Packet, VpcDiscriminant}; -use net::{FlowKey, FlowKeyData, IpProtoKey}; -use pipeline::NetworkFunction; +use net::{FlowKey, IpProtoKey}; +use pipeline::{NetworkFunction, PipelineData}; use std::fmt::{Debug, Display}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::time::{Duration, Instant}; @@ -84,6 +84,7 @@ pub struct StatefulNat { name: String, sessions: Arc, allocator: NatAllocatorReader, + pipeline_data: Arc, } impl StatefulNat { @@ -94,6 +95,7 @@ impl StatefulNat { name: name.to_string(), sessions, allocator, + pipeline_data: Arc::from(PipelineData::default()), } } @@ -171,28 +173,13 @@ impl StatefulNat { Instant::now() + timeout } - fn create_session( - &self, - flow_key: &FlowKey, + fn setup_flow_nat_state( + flow_info: &FlowInfo, state: NatFlowState, dst_vpcd: VpcDiscriminant, - idle_timeout: Duration, ) { - // Clear the destination VPC so we can make lookups without knowing it - let new_flow_key = FlowKey::Unidirectional(FlowKeyData::new( - flow_key.data().src_vpcd(), - *flow_key.data().src_ip(), - *flow_key.data().dst_ip(), - *flow_key.data().proto_key_info(), - )); - debug!( - "{}: Creating new flow session entry: {} -> {}", - self.name(), - new_flow_key.data(), - state - ); - - let flow_info = FlowInfo::new(Self::session_timeout_time(idle_timeout)); + let flow_key = flow_info.flowkey().unwrap_or_else(|| unreachable!()); + debug!("Setting up new flow: {flow_key} -> {state}"); if let Ok(mut write_guard) = flow_info.locked.write() { write_guard.nat_state = Some(Box::new(state)); write_guard.dst_vpcd = Some(Box::new(dst_vpcd)); @@ -200,7 +187,39 @@ impl StatefulNat { // flow info is just locally created unreachable!() } - self.sessions.insert(new_flow_key, flow_info); + } + + fn create_flow_pair( + &self, + packet: &mut Packet, + flow_key: &FlowKey, + alloc: AllocationResult>, + ) -> Result<(), StatefulNatError> { + // Given that at least one of alloc.src or alloc.dst is set, we should always have at least one timeout set. + let idle_timeout = alloc.idle_timeout().unwrap_or_else(|| unreachable!()); + + // src and dst vpc of this packet + let src_vpc_id = packet.meta().src_vpcd.unwrap_or_else(|| unreachable!()); + let dst_vpc_id = packet.meta().dst_vpcd.unwrap_or_else(|| unreachable!()); + + // build key for reverse flow + let reverse_key = Self::new_reverse_session(flow_key, &alloc, dst_vpc_id)?; + + // build NAT state for both flows + let (forward_state, reverse_state) = Self::new_states_from_alloc(alloc, idle_timeout); + + // build a flow pair from the keys (without NAT state) + let expires_at = Self::session_timeout_time(idle_timeout); + let (forward, reverse) = FlowInfo::related_pair(expires_at, *flow_key, reverse_key); + + // set up their NAT state + Self::setup_flow_nat_state(&forward, forward_state, dst_vpc_id); + Self::setup_flow_nat_state(&reverse, reverse_state, src_vpc_id); + + // insert in flow-table + self.sessions.insert_from_arc(*flow_key, &forward); + self.sessions.insert_from_arc(reverse_key, &reverse); + Ok(()) } #[allow(clippy::unnecessary_wraps)] @@ -431,7 +450,6 @@ impl StatefulNat { let flow_key = FlowKey::try_from(Uni(&*packet)).map_err(|_| StatefulNatError::TupleParseError)?; - let src_vpc_id = packet.meta().src_vpcd.unwrap_or_else(|| unreachable!()); let dst_vpc_id = packet.meta().dst_vpcd.unwrap_or_else(|| unreachable!()); // build extended flow key, with the dst vpc discriminant @@ -447,22 +465,9 @@ impl StatefulNat { debug!("{}: Allocated translation data: {alloc}", self.name()); - // Given that at least one of alloc.src or alloc.dst is set, we should always have at - // least one timeout set. - let idle_timeout = alloc.idle_timeout().unwrap_or_else(|| unreachable!()); - let translation_data = Self::get_translation_data(&alloc.src, &alloc.dst); - let reverse_flow_key = Self::new_reverse_session(&flow_key, &alloc, dst_vpc_id)?; - let (forward_state, reverse_state) = Self::new_states_from_alloc(alloc, idle_timeout); - - self.create_session(&flow_key, forward_state, dst_vpc_id, idle_timeout); - self.create_session( - &reverse_flow_key, - reverse_state.clone(), - src_vpc_id, - idle_timeout, - ); + self.create_flow_pair(packet, &flow_key, alloc)?; Self::stateful_translate::(self.name(), packet, &translation_data).and(Ok(true)) } @@ -579,6 +584,10 @@ impl NetworkFunction for StatefulNat { packet.enforce() }) } + + fn set_data(&mut self, data: Arc) { + self.pipeline_data = data; + } } #[cfg(test)] diff --git a/net/src/flows/flow_info.rs b/net/src/flows/flow_info.rs index 1e8100d32..2b4bfa024 100644 --- a/net/src/flows/flow_info.rs +++ b/net/src/flows/flow_info.rs @@ -8,7 +8,7 @@ use concurrency::sync::RwLock; use concurrency::sync::Weak; use std::fmt::{Debug, Display}; use std::mem::MaybeUninit; -use std::sync::atomic::{AtomicU8, Ordering}; +use std::sync::atomic::{AtomicI64, AtomicU8, Ordering}; use std::time::{Duration, Instant}; use super::{AtomicInstant, FlowInfoItem}; @@ -138,10 +138,18 @@ pub struct FlowInfoLocked { pub port_fw_state: Option>, } +/// Object that represents a flow of packets. +/// `related` is a `Weak` reference to another flow that is related to this one (e.g. +/// a flow in the reverse direction). `FlowKey` is optional, but any flow we store in +/// the flow table gets a key automatically. `genid` is the last generation id where +/// this flow is valid (accepted by the flow-filter). As such, it increases on config +/// changes (if the flow is acceptable under a new configuration), or the flow should +/// no longer have status `Active`. #[derive(Debug)] pub struct FlowInfo { expires_at: AtomicInstant, flowkey: Option, + genid: AtomicI64, status: AtomicFlowStatus, pub locked: RwLock, pub related: Option>, @@ -156,6 +164,7 @@ impl FlowInfo { Self { expires_at: AtomicInstant::new(expires_at), flowkey: None, + genid: AtomicI64::new(0), status: AtomicFlowStatus::from(FlowStatus::Active), locked: RwLock::new(FlowInfoLocked::default()), related: None, @@ -171,6 +180,25 @@ impl FlowInfo { self.flowkey.as_ref() } + /// Set the generation Id of a flow + pub fn set_genid(&self, genid: i64) { + self.genid.store(genid, Ordering::Relaxed); + } + + /// Set the generation Id of a flow and that of its related flow if any + pub fn set_genid_pair(&self, genid: i64) { + self.genid.store(genid, Ordering::Relaxed); + self.related + .as_ref() + .and_then(Weak::upgrade) + .inspect(|r| r.set_genid(genid)); + } + + /// Read the generation Id of a flow. + pub fn genid(&self) -> i64 { + self.genid.load(Ordering::Relaxed) + } + /// We want to create a pair of `FlowInfo`s that are mutually related via a `Weak` references so that no lookup /// is needed to find one from the other. This is tricky because the `FlowInfo`s are shared and we /// need concurrent access to them. One option to build such relationships is to let those `Weak` @@ -224,6 +252,7 @@ impl FlowInfo { one_p.write(Self { expires_at: AtomicInstant::new(expires_at), flowkey: Some(key1), + genid: AtomicI64::new(0), status: AtomicFlowStatus::from(FlowStatus::Active), locked: RwLock::new(FlowInfoLocked::default()), related: Some(two_weak), @@ -231,6 +260,7 @@ impl FlowInfo { two_p.write(Self { expires_at: AtomicInstant::new(expires_at), flowkey: Some(key2), + genid: AtomicI64::new(0), status: AtomicFlowStatus::from(FlowStatus::Active), locked: RwLock::new(FlowInfoLocked::default()), related: Some(one_weak), @@ -339,6 +369,19 @@ impl FlowInfo { self.update_status(FlowStatus::Cancelled); } + /// Invalidate a flow and also its related flow if any. + /// + /// # Thread Safety + /// + /// This method is thread-safe. + pub fn invalidate_pair(&self) { + self.invalidate(); + self.related + .as_ref() + .and_then(Weak::upgrade) + .inspect(|related| related.invalidate()); + } + /// Update the flow status. /// /// # Thread Safety @@ -369,6 +412,7 @@ impl Display for FlowInfo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let expires_at = self.expires_at.load(Ordering::Relaxed); let expires_in = expires_at.saturating_duration_since(Instant::now()); + let genid = self.genid(); writeln!(f)?; if let Ok(info) = self.locked.try_read() { write!(f, "{info}")?; @@ -383,7 +427,7 @@ impl Display for FlowInfo { writeln!( f, - " status: {:?}, expires in {}s, related: {has_related}", + " status: {:?}, expires in {}s, related: {has_related}, genid: {genid}", self.status, expires_in.as_secs(), ) diff --git a/pipeline/src/lib.rs b/pipeline/src/lib.rs index e8cf1914f..25f19328b 100644 --- a/pipeline/src/lib.rs +++ b/pipeline/src/lib.rs @@ -116,7 +116,7 @@ pub(crate) mod test_utils; #[allow(unused)] pub use dyn_nf::{DynNetworkFunction, nf_dyn}; #[allow(unused)] -pub use pipeline::{DynPipeline, StageId}; +pub use pipeline::{DynPipeline, PipelineData, StageId}; #[allow(unused)] pub use static_nf::{NetworkFunction, StaticChain}; diff --git a/pipeline/src/pipeline.rs b/pipeline/src/pipeline.rs index 6227ef687..8c23083d3 100644 --- a/pipeline/src/pipeline.rs +++ b/pipeline/src/pipeline.rs @@ -11,10 +11,37 @@ use net::buffer::PacketBufferMut; use net::packet::Packet; use ordermap::OrderMap; use std::any::Any; +use std::sync::Arc; +use std::sync::atomic::AtomicI64; /// A type that represents an Id for a stage or NF pub type StageId = Id>>; +/// Data associated to a `Pipeline` +#[derive(Default, Debug)] +pub struct PipelineData { + /// Current generation Id + pub genid: AtomicI64, +} +impl PipelineData { + #[must_use] + /// Build a new `PipelineData` object + pub fn new(genid: i64) -> Self { + Self { + genid: AtomicI64::new(genid), + } + } + /// Read the generation id + pub fn genid(&self) -> i64 { + self.genid.load(std::sync::atomic::Ordering::Relaxed) + } + /// Set the generation id + pub fn set_genid(&self, genid: i64) { + self.genid + .store(genid, std::sync::atomic::Ordering::Relaxed); + } +} + /// A dynamic pipeline that can be updated at runtime. /// /// This struct is used to create a dynamic pipeline that can be updated at runtime. @@ -25,6 +52,7 @@ pub type StageId = Id>>; #[derive(Default)] pub struct DynPipeline { nfs: OrderMap, Box>>, + data: Arc, } #[derive(Debug, thiserror::Error)] @@ -39,15 +67,30 @@ impl DynPipeline { pub fn new() -> Self { Self { nfs: OrderMap::new(), + data: Arc::from(PipelineData::default()), } } + #[must_use] + /// Set `PipelineData` to a `DynPipeline` + pub fn set_data(mut self, data: Arc) -> Self { + self.data = data; + self + } + + #[must_use] + /// Get `PipelineData` from a pipeline + pub fn get_data(&self) -> Arc { + self.data.clone() + } + /// Add a static network function to the pipeline. /// /// This method takes a [`NetworkFunction`] and adds it to the pipeline. /// #[must_use] - pub fn add_stage + 'static>(self, nf: NF) -> Self { + pub fn add_stage + 'static>(self, mut nf: NF) -> Self { + nf.set_data(self.data.clone()); self.add_stage_dyn(nf_dyn(nf)) } diff --git a/pipeline/src/static_nf.rs b/pipeline/src/static_nf.rs index e37221f28..9a4bb8d5d 100644 --- a/pipeline/src/static_nf.rs +++ b/pipeline/src/static_nf.rs @@ -4,6 +4,9 @@ use net::buffer::PacketBufferMut; use net::packet::Packet; use std::marker::PhantomData; +use std::sync::Arc; + +use crate::PipelineData; /// Trait for an object that processes a stream of packets. pub trait NetworkFunction { @@ -23,6 +26,9 @@ pub trait NetworkFunction { &'a mut self, input: Input, ) -> impl Iterator> + 'a; + + /// Let NFs access some `PipelineData` if they wish on their creation + fn set_data(&mut self, _data: Arc) {} } struct StaticChainImpl, NF2: NetworkFunction> {