ndn_engine/stages/
strategy.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Arc;
4
5use smallvec::SmallVec;
6use tracing::trace;
7
8use crate::Fib;
9use crate::enricher::ContextEnricher;
10use crate::pipeline::{
11    Action, AnyMap, DecodedPacket, DropReason, ForwardingAction, NackReason, PacketContext,
12};
13use ndn_discovery::scope::is_link_local;
14use ndn_packet::Name;
15use ndn_store::{Pit, StrategyTable};
16use ndn_strategy::{MeasurementsTable, Strategy, StrategyContext};
17use ndn_transport::face::FaceScope;
18
19/// Object-safe version of `Strategy` that boxes its futures.
20pub trait ErasedStrategy: Send + Sync + 'static {
21    /// Canonical name identifying this strategy (e.g. `/localhost/nfd/strategy/best-route`).
22    fn name(&self) -> &Name;
23
24    /// Synchronous fast path — avoids the `Box::pin` heap allocation.
25    /// Returns `None` to fall through to the async path.
26    fn decide_sync(&self, ctx: &StrategyContext<'_>) -> Option<SmallVec<[ForwardingAction; 2]>>;
27
28    /// Async path for Interest forwarding decisions (boxed future).
29    fn after_receive_interest_erased<'a>(
30        &'a self,
31        ctx: &'a StrategyContext<'a>,
32    ) -> Pin<Box<dyn Future<Output = SmallVec<[ForwardingAction; 2]>> + Send + 'a>>;
33
34    /// Handle an incoming Nack and decide whether to retry or propagate.
35    fn on_nack_erased<'a>(
36        &'a self,
37        ctx: &'a StrategyContext<'a>,
38        reason: NackReason,
39    ) -> Pin<Box<dyn Future<Output = ForwardingAction> + Send + 'a>>;
40}
41
42impl<S: Strategy> ErasedStrategy for S {
43    fn name(&self) -> &Name {
44        Strategy::name(self)
45    }
46
47    fn decide_sync(&self, ctx: &StrategyContext<'_>) -> Option<SmallVec<[ForwardingAction; 2]>> {
48        self.decide(ctx)
49    }
50
51    fn after_receive_interest_erased<'a>(
52        &'a self,
53        ctx: &'a StrategyContext<'a>,
54    ) -> Pin<Box<dyn Future<Output = SmallVec<[ForwardingAction; 2]>> + Send + 'a>> {
55        Box::pin(self.after_receive_interest(ctx))
56    }
57
58    fn on_nack_erased<'a>(
59        &'a self,
60        ctx: &'a StrategyContext<'a>,
61        reason: NackReason,
62    ) -> Pin<Box<dyn Future<Output = ForwardingAction> + Send + 'a>> {
63        Box::pin(self.on_nack(ctx, reason))
64    }
65}
66
67/// Calls the strategy to produce a forwarding decision for Interests.
68///
69/// Performs LPM on the strategy table to find the per-prefix strategy.
70/// Falls back to `default_strategy` if no entry matches (should not happen
71/// if root is populated).
72pub struct StrategyStage {
73    pub strategy_table: Arc<StrategyTable<dyn ErasedStrategy>>,
74    pub default_strategy: Arc<dyn ErasedStrategy>,
75    pub fib: Arc<Fib>,
76    pub measurements: Arc<MeasurementsTable>,
77    pub pit: Arc<Pit>,
78    pub face_table: Arc<ndn_transport::FaceTable>,
79    /// Cross-layer enrichers run before the strategy to populate `StrategyContext::extensions`.
80    pub enrichers: Vec<Arc<dyn ContextEnricher>>,
81}
82
83impl StrategyStage {
84    /// Run the per-prefix strategy for an Interest and return a pipeline action.
85    pub async fn process(&self, mut ctx: PacketContext) -> Action {
86        match &ctx.packet {
87            DecodedPacket::Interest(_) => {}
88            // Strategy only runs for Interests in the forward path.
89            _ => return Action::Continue(ctx),
90        };
91
92        let name = match &ctx.name {
93            Some(n) => n.clone(),
94            None => return Action::Drop(DropReason::MalformedPacket),
95        };
96
97        let fib_entry_arc = self.fib.lpm(&name);
98        let fib_entry_ref = fib_entry_arc.as_deref();
99
100        if let Some(e) = fib_entry_ref {
101            trace!(face=%ctx.face_id, name=%name, nexthops=?e.nexthops.iter().map(|nh| (nh.face_id, nh.cost)).collect::<Vec<_>>(), "strategy: FIB LPM hit");
102        } else {
103            trace!(face=%ctx.face_id, name=%name, "strategy: FIB LPM miss (no route)");
104        }
105
106        // Convert engine FibEntry → strategy FibEntry.
107        let strategy_fib: Option<ndn_strategy::FibEntry> =
108            fib_entry_ref.map(|e| ndn_strategy::FibEntry {
109                nexthops: e
110                    .nexthops
111                    .iter()
112                    .map(|nh| ndn_strategy::FibNexthop {
113                        face_id: nh.face_id,
114                        cost: nh.cost,
115                    })
116                    .collect(),
117            });
118
119        // Build cross-layer extensions via registered enrichers.
120        let mut extensions = AnyMap::new();
121        for enricher in &self.enrichers {
122            enricher.enrich(strategy_fib.as_ref(), &mut extensions);
123        }
124
125        let sctx = StrategyContext {
126            name: &name,
127            in_face: ctx.face_id,
128            fib_entry: strategy_fib.as_ref(),
129            pit_token: ctx.pit_token,
130            measurements: &self.measurements,
131            extensions: &extensions,
132        };
133
134        // Per-prefix strategy lookup (LPM on strategy table).
135        let strategy = self
136            .strategy_table
137            .lpm(&name)
138            .unwrap_or_else(|| Arc::clone(&self.default_strategy));
139        trace!(face=%ctx.face_id, name=%name, strategy=%strategy.name(), "strategy: selected");
140
141        // Sync fast path: avoids Box::pin heap allocation for strategies
142        // like BestRoute / Multicast whose decisions are fully synchronous.
143        let actions = if let Some(a) = strategy.decide_sync(&sctx) {
144            a
145        } else {
146            strategy.after_receive_interest_erased(&sctx).await
147        };
148
149        // Use the first actionable ForwardingAction.
150        if let Some(action) = actions.into_iter().next() {
151            match action {
152                ForwardingAction::Forward(faces) => {
153                    trace!(face=%ctx.face_id, name=%name, out_faces=?faces, "strategy: Forward");
154                    // Link-local scope enforcement: /ndn/local/ packets must
155                    // not be forwarded to non-local (network) faces, mirroring
156                    // IPv6 fe80::/10 link-local semantics.
157                    let effective_faces: SmallVec<[ndn_transport::FaceId; 4]> = if is_link_local(
158                        &name,
159                    ) {
160                        faces.iter().copied().filter(|fid| {
161                            let keep = self.face_table.get(*fid)
162                                .map(|f| f.kind().scope() == FaceScope::Local)
163                                .unwrap_or(false);
164                            if !keep {
165                                trace!(face=%ctx.face_id, name=%name, out_face=%fid, "strategy: dropping link-local packet on non-local face");
166                            }
167                            keep
168                        }).collect()
169                    } else {
170                        faces.iter().copied().collect()
171                    };
172                    if effective_faces.is_empty() {
173                        // All nexthops filtered out — Nack.
174                        return Action::Nack(ctx, NackReason::NoRoute);
175                    }
176                    ctx.out_faces.extend_from_slice(&effective_faces);
177                    let out = ctx.out_faces.clone();
178                    return Action::Send(ctx, out);
179                }
180                ForwardingAction::ForwardAfter { faces, delay } => {
181                    trace!(face=%ctx.face_id, name=%name, out_faces=?faces, delay_ms=%delay.as_millis(), "strategy: ForwardAfter");
182                    // Spawn a delayed send: sleep, re-check PIT, then forward.
183                    let pit = Arc::clone(&self.pit);
184                    let face_table = Arc::clone(&self.face_table);
185                    let raw_bytes = ctx.raw_bytes.clone();
186                    let pit_token = ctx.pit_token;
187                    tokio::spawn(async move {
188                        tokio::time::sleep(delay).await;
189                        // Re-check PIT — if the entry was already satisfied or
190                        // expired, do not send (the Interest is no longer pending).
191                        if let Some(token) = pit_token
192                            && !pit.contains(&token)
193                        {
194                            return; // PIT entry gone — already satisfied/expired.
195                        }
196                        for face_id in &faces {
197                            if let Some(face) = face_table.get(*face_id) {
198                                let _ = face.send_bytes(raw_bytes.clone()).await;
199                            }
200                        }
201                    });
202                    return Action::Drop(DropReason::Other); // consumed by delayed task
203                }
204                ForwardingAction::Nack(reason) => {
205                    trace!(face=%ctx.face_id, name=%name, reason=?reason, "strategy: Nack");
206                    return Action::Nack(ctx, reason);
207                }
208                ForwardingAction::Suppress => {
209                    trace!(face=%ctx.face_id, name=%name, "strategy: Suppress");
210                    return Action::Drop(DropReason::Suppressed);
211                }
212            }
213        }
214
215        // No actionable forwarding decision → no route.
216        trace!(face=%ctx.face_id, name=%name, "strategy: no actionable decision, Nack NoRoute");
217        Action::Nack(ctx, NackReason::NoRoute)
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn link_local_scope_check_is_accurate() {
227        use std::str::FromStr;
228        let link_local = ndn_packet::Name::from_str("/ndn/local/nd/hello/1").unwrap();
229        let global = ndn_packet::Name::from_str("/ndn/edu/test").unwrap();
230        assert!(is_link_local(&link_local), "/ndn/local/ must be link-local");
231        assert!(!is_link_local(&global), "/ndn/edu/ must not be link-local");
232    }
233}