ndn_sim/
topology.rs

1//! `Simulation` — multi-node topology builder for in-process NDN simulations.
2//!
3//! Provides a high-level API for constructing networks of NDN forwarders
4//! connected by [`SimLink`](crate::SimLink)s, starting them, and managing
5//! their lifecycle.
6//!
7//! # Example
8//!
9//! ```rust,no_run
10//! use ndn_sim::{Simulation, LinkConfig};
11//! use ndn_engine::builder::EngineConfig;
12//!
13//! # async fn example() -> anyhow::Result<()> {
14//! let mut sim = Simulation::new();
15//!
16//! let n1 = sim.add_node(EngineConfig::default());
17//! let n2 = sim.add_node(EngineConfig::default());
18//! let n3 = sim.add_node(EngineConfig::default());
19//!
20//! sim.link(n1, n2, LinkConfig::lan());
21//! sim.link(n2, n3, LinkConfig::wifi());
22//!
23//! let mut running = sim.start().await?;
24//!
25//! running.add_route(n1, "/ndn/test", n2)?;
26//! running.add_route(n2, "/ndn/test", n3)?;
27//!
28//! // ... run experiment ...
29//!
30//! running.shutdown().await;
31//! # Ok(())
32//! # }
33//! ```
34
35use std::collections::HashMap;
36use std::str::FromStr;
37
38use anyhow::{Result, bail};
39use ndn_engine::ForwarderEngine;
40use ndn_engine::builder::{EngineBuilder, EngineConfig};
41use ndn_engine::engine::ShutdownHandle;
42use ndn_packet::Name;
43use ndn_transport::FaceId;
44use tracing::info;
45
46use crate::sim_link::{LinkConfig, SimLink};
47
48/// Opaque handle to a node in the simulation.
49#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
50pub struct NodeId(pub usize);
51
52impl std::fmt::Display for NodeId {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        write!(f, "node#{}", self.0)
55    }
56}
57
58/// A pending link to be created when the simulation starts.
59struct PendingLink {
60    a: NodeId,
61    b: NodeId,
62    config: LinkConfig,
63}
64
65/// A pending FIB route to be installed when the simulation starts.
66struct PendingRoute {
67    node: NodeId,
68    prefix: Name,
69    nexthop_node: NodeId,
70}
71
72/// Builder for a multi-node simulation topology.
73///
74/// Nodes and links are registered, then [`start`](Self::start) instantiates
75/// all engines, creates SimFaces, installs routes, and returns a
76/// [`RunningSimulation`] handle.
77pub struct Simulation {
78    nodes: Vec<EngineConfig>,
79    links: Vec<PendingLink>,
80    routes: Vec<PendingRoute>,
81    channel_buffer: usize,
82}
83
84impl Default for Simulation {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90impl Simulation {
91    pub fn new() -> Self {
92        Self {
93            nodes: Vec::new(),
94            links: Vec::new(),
95            routes: Vec::new(),
96            channel_buffer: 256,
97        }
98    }
99
100    /// Set the channel buffer size for SimLinks (default: 256).
101    pub fn channel_buffer(mut self, size: usize) -> Self {
102        self.channel_buffer = size;
103        self
104    }
105
106    /// Add a forwarding node and return its handle.
107    pub fn add_node(&mut self, config: EngineConfig) -> NodeId {
108        let id = NodeId(self.nodes.len());
109        self.nodes.push(config);
110        id
111    }
112
113    /// Connect two nodes with a symmetric link.
114    pub fn link(&mut self, a: NodeId, b: NodeId, config: LinkConfig) {
115        self.links.push(PendingLink { a, b, config });
116    }
117
118    /// Pre-install a FIB route: packets for `prefix` at `node` are forwarded
119    /// toward `nexthop_node` (via the SimLink face connecting them).
120    pub fn add_route(&mut self, node: NodeId, prefix: &str, nexthop_node: NodeId) {
121        self.routes.push(PendingRoute {
122            node,
123            prefix: Name::from_str(prefix).expect("valid NDN name"),
124            nexthop_node,
125        });
126    }
127
128    /// Instantiate all engines, create links, install routes, and start.
129    pub async fn start(self) -> Result<RunningSimulation> {
130        let n = self.nodes.len();
131        info!(nodes = n, links = self.links.len(), "Simulation: starting");
132
133        // Build all engines.
134        let mut engines = Vec::with_capacity(n);
135        let mut handles = Vec::with_capacity(n);
136        for config in self.nodes {
137            let (engine, handle) = EngineBuilder::new(config).build().await?;
138            engines.push(engine);
139            handles.push(handle);
140        }
141
142        // Create SimLinks and add faces to engines.
143        // Track which FaceId connects node A to node B, for route installation.
144        // Key: (from_node, to_node) -> FaceId at from_node
145        let mut face_map: HashMap<(NodeId, NodeId), FaceId> = HashMap::new();
146
147        for link in &self.links {
148            let a = link.a.0;
149            let b = link.b.0;
150            if a >= n || b >= n {
151                bail!("link references non-existent node");
152            }
153
154            let id_a = engines[a].faces().alloc_id();
155            let id_b = engines[b].faces().alloc_id();
156
157            let (face_a, face_b) =
158                SimLink::pair(id_a, id_b, link.config.clone(), self.channel_buffer);
159
160            let cancel_a = handles[a].cancel_token();
161            let cancel_b = handles[b].cancel_token();
162            engines[a].add_face(face_a, cancel_a);
163            engines[b].add_face(face_b, cancel_b);
164
165            face_map.insert((link.a, link.b), id_a);
166            face_map.insert((link.b, link.a), id_b);
167
168            info!(
169                node_a = a, face_a = %id_a,
170                node_b = b, face_b = %id_b,
171                "Simulation: link created"
172            );
173        }
174
175        // Install FIB routes.
176        for route in &self.routes {
177            let face_id = face_map.get(&(route.node, route.nexthop_node));
178            if let Some(&fid) = face_id {
179                engines[route.node.0]
180                    .fib()
181                    .add_nexthop(&route.prefix, fid, 10);
182                info!(
183                    node = route.node.0, prefix = %route.prefix, face = %fid,
184                    "Simulation: route installed"
185                );
186            } else {
187                bail!(
188                    "no link between {} and {} for route {}",
189                    route.node,
190                    route.nexthop_node,
191                    route.prefix
192                );
193            }
194        }
195
196        Ok(RunningSimulation {
197            engines,
198            handles,
199            face_map,
200        })
201    }
202}
203
204/// A running simulation with all engines active.
205pub struct RunningSimulation {
206    engines: Vec<ForwarderEngine>,
207    handles: Vec<ShutdownHandle>,
208    face_map: HashMap<(NodeId, NodeId), FaceId>,
209}
210
211impl RunningSimulation {
212    /// Get the engine for a node.
213    pub fn engine(&self, node: NodeId) -> &ForwarderEngine {
214        &self.engines[node.0]
215    }
216
217    /// Get all engines.
218    pub fn engines(&self) -> &[ForwarderEngine] {
219        &self.engines
220    }
221
222    /// Number of nodes in the simulation.
223    pub fn node_count(&self) -> usize {
224        self.engines.len()
225    }
226
227    /// Add a FIB route at runtime.
228    pub fn add_route(&self, node: NodeId, prefix: &str, nexthop: NodeId) -> Result<()> {
229        let face_id = self
230            .face_map
231            .get(&(node, nexthop))
232            .ok_or_else(|| anyhow::anyhow!("no link between {node} and {nexthop}"))?;
233        let name = Name::from_str(prefix).expect("valid NDN name");
234        self.engines[node.0].fib().add_nexthop(&name, *face_id, 10);
235        Ok(())
236    }
237
238    /// Get the FaceId connecting `from` to `to`.
239    pub fn face_between(&self, from: NodeId, to: NodeId) -> Option<FaceId> {
240        self.face_map.get(&(from, to)).copied()
241    }
242
243    /// Shut down all engines.
244    pub async fn shutdown(self) {
245        for handle in self.handles {
246            handle.shutdown().await;
247        }
248    }
249}