ndn_discovery/hello/
ether.rs

1//! `EtherNeighborDiscovery` — NDN neighbor discovery over raw Ethernet.
2//!
3//! Implements [`DiscoveryProtocol`] using periodic hello Interest broadcasts on
4//! a [`MulticastEtherFace`] and unicast [`NamedEtherFace`] creation per peer.
5//!
6//! # Protocol (doc format)
7//!
8//! **Hello Interest** (broadcast on multicast face):
9//! ```text
10//! Name: /ndn/local/nd/hello/<nonce-u32>
11//! (no AppParams)
12//! ```
13//!
14//! **Hello Data** (reply sent back on multicast face):
15//! ```text
16//! Name:    /ndn/local/nd/hello/<nonce-u32>
17//! Content: HelloPayload TLV
18//!   NODE-NAME     = /ndn/site/mynode
19//!   SERVED-PREFIX = ...        (optional, InHello mode)
20//!   CAPABILITIES  = [flags]    (optional)
21//!   NEIGHBOR-DIFF = [...]      (SWIM gossip piggyback, optional)
22//! ```
23//!
24//! The sender's MAC is extracted from `meta.source` (populated by the engine
25//! via `MulticastEtherFace::recv_with_source`), not from the packet payload.
26//!
27//! On receiving a Hello Interest a node:
28//! 1. Reads the sender MAC from `meta.source` (`LinkAddr::Ether`).
29//! 2. Triggers `PassiveDetection` on the strategy when the MAC is new.
30//! 3. Replies with a Hello Data carrying its own `HelloPayload`.
31//!
32//! On receiving a Hello Data the sender:
33//! 1. Decodes `HelloPayload` from Content.
34//! 2. Reads responder MAC from `meta.source`.
35//! 3. Creates a [`NamedEtherFace`] to the responder if needed.
36//! 4. Updates the neighbor to `Established` and records RTT.
37//! 5. Installs FIB routes for `served_prefixes` (if `InHello` mode).
38//! 6. Applies any piggybacked `NEIGHBOR-DIFF` entries.
39
40use std::ops::{Deref, DerefMut};
41use std::time::{Duration, Instant};
42
43use bytes::Bytes;
44use ndn_faces::l2::{NamedEtherFace, RadioFaceMetadata};
45use ndn_transport::{FaceId, MacAddr};
46
47use crate::hello::medium::{HELLO_PREFIX_DEPTH, HelloCore, HelloState, LinkMedium};
48use crate::wire::{parse_raw_interest, write_name_tlv, write_nni};
49use crate::{
50    DiscoveryConfig, DiscoveryContext, DiscoveryProfile, DiscoveryProtocol, HelloPayload,
51    HelloProtocol, InboundMeta, LinkAddr, NeighborEntry, NeighborUpdate, ProtocolId,
52};
53use ndn_packet::{Name, tlv_type};
54use ndn_tlv::TlvWriter;
55use tracing::{debug, warn};
56
57const PROTOCOL: ProtocolId = ProtocolId("ether-nd");
58
59/// Ethernet-specific link medium for [`HelloProtocol`].
60///
61/// Handles unsigned hello Data, MAC address extraction from inbound
62/// metadata, passive detection of new MACs, and unicast face creation
63/// via [`NamedEtherFace`].
64pub struct EtherMedium {
65    /// Multicast face used for hello broadcasts.
66    multicast_face_id: FaceId,
67    /// Network interface name (e.g. "wlan0").
68    iface: String,
69    /// Our Ethernet MAC address (stored for future use, e.g. source filtering).
70    #[allow(dead_code)]
71    local_mac: MacAddr,
72}
73
74/// Ethernet neighbor discovery — newtype wrapper around `HelloProtocol<EtherMedium>`.
75pub struct EtherNeighborDiscovery(HelloProtocol<EtherMedium>);
76
77impl Deref for EtherNeighborDiscovery {
78    type Target = HelloProtocol<EtherMedium>;
79    fn deref(&self) -> &Self::Target {
80        &self.0
81    }
82}
83
84impl DerefMut for EtherNeighborDiscovery {
85    fn deref_mut(&mut self) -> &mut Self::Target {
86        &mut self.0
87    }
88}
89
90impl DiscoveryProtocol for EtherNeighborDiscovery {
91    fn protocol_id(&self) -> ProtocolId {
92        self.0.protocol_id()
93    }
94    fn claimed_prefixes(&self) -> &[Name] {
95        self.0.claimed_prefixes()
96    }
97    fn tick_interval(&self) -> Duration {
98        self.0.tick_interval()
99    }
100    fn on_face_up(&self, face_id: FaceId, ctx: &dyn DiscoveryContext) {
101        self.0.on_face_up(face_id, ctx)
102    }
103    fn on_face_down(&self, face_id: FaceId, ctx: &dyn DiscoveryContext) {
104        self.0.on_face_down(face_id, ctx)
105    }
106    fn on_inbound(
107        &self,
108        raw: &Bytes,
109        incoming_face: FaceId,
110        meta: &InboundMeta,
111        ctx: &dyn DiscoveryContext,
112    ) -> bool {
113        self.0.on_inbound(raw, incoming_face, meta, ctx)
114    }
115    fn on_tick(&self, now: Instant, ctx: &dyn DiscoveryContext) {
116        self.0.on_tick(now, ctx)
117    }
118}
119
120impl EtherNeighborDiscovery {
121    /// Create a new instance with the default LAN profile.
122    pub fn new(
123        multicast_face_id: FaceId,
124        iface: impl Into<String>,
125        node_name: Name,
126        local_mac: MacAddr,
127    ) -> Self {
128        Self::new_with_config(
129            multicast_face_id,
130            iface,
131            node_name,
132            local_mac,
133            DiscoveryConfig::for_profile(&DiscoveryProfile::Lan),
134        )
135    }
136
137    /// Create with an explicit [`DiscoveryConfig`].
138    pub fn new_with_config(
139        multicast_face_id: FaceId,
140        iface: impl Into<String>,
141        node_name: Name,
142        local_mac: MacAddr,
143        config: DiscoveryConfig,
144    ) -> Self {
145        let medium = EtherMedium {
146            multicast_face_id,
147            iface: iface.into(),
148            local_mac,
149        };
150        Self(HelloProtocol::create(medium, node_name, config))
151    }
152
153    /// Create with a named deployment profile.
154    pub fn from_profile(
155        multicast_face_id: FaceId,
156        iface: impl Into<String>,
157        node_name: Name,
158        local_mac: MacAddr,
159        profile: &DiscoveryProfile,
160    ) -> Self {
161        Self::new_with_config(
162            multicast_face_id,
163            iface,
164            node_name,
165            local_mac,
166            DiscoveryConfig::for_profile(profile),
167        )
168    }
169}
170
171impl EtherMedium {
172    fn ensure_peer(
173        &self,
174        ctx: &dyn DiscoveryContext,
175        peer_name: &Name,
176        peer_mac: MacAddr,
177    ) -> Option<FaceId> {
178        let existing = ctx.neighbors().face_for_peer(&peer_mac, &self.iface);
179
180        let face_id = if let Some(fid) = existing {
181            fid
182        } else {
183            let fid = ctx.alloc_face_id();
184            match NamedEtherFace::new(
185                fid,
186                peer_name.clone(),
187                peer_mac,
188                self.iface.clone(),
189                RadioFaceMetadata::default(),
190            ) {
191                Ok(face) => {
192                    let registered = ctx.add_face(std::sync::Arc::new(face));
193                    debug!("EtherND: created unicast face {registered:?} -> {peer_name}");
194                    registered
195                }
196                Err(e) => {
197                    warn!("EtherND: failed to create unicast face to {peer_name}: {e}");
198                    return None;
199                }
200            }
201        };
202
203        if ctx.neighbors().get(peer_name).is_none() {
204            ctx.update_neighbor(NeighborUpdate::Upsert(NeighborEntry::new(
205                peer_name.clone(),
206            )));
207        }
208
209        ctx.update_neighbor(NeighborUpdate::AddFace {
210            name: peer_name.clone(),
211            face_id,
212            mac: peer_mac,
213            iface: self.iface.clone(),
214        });
215
216        ctx.add_fib_entry(peer_name, face_id, 0, PROTOCOL);
217        Some(face_id)
218    }
219}
220
221impl LinkMedium for EtherMedium {
222    fn protocol_id(&self) -> ProtocolId {
223        PROTOCOL
224    }
225
226    fn build_hello_data(&self, core: &HelloCore, interest_name: &Name) -> Bytes {
227        let mut payload = crate::HelloPayload::new(core.node_name.clone());
228
229        if core.config.read().unwrap().prefix_announcement == crate::PrefixAnnouncementMode::InHello
230        {
231            let sp = core.served_prefixes.lock().unwrap();
232            payload.served_prefixes = sp.clone();
233        }
234
235        {
236            let st = core.state.lock().unwrap();
237            if !st.recent_diffs.is_empty() {
238                payload.neighbor_diffs.push(crate::NeighborDiff {
239                    entries: st.recent_diffs.iter().cloned().collect(),
240                });
241            }
242        }
243
244        let content = payload.encode();
245        let freshness_ms = core
246            .config
247            .read()
248            .unwrap()
249            .hello_interval_base
250            .as_millis()
251            .min(u32::MAX as u128) as u64
252            * 2;
253
254        let mut w = TlvWriter::new();
255        w.write_nested(tlv_type::DATA, |w: &mut TlvWriter| {
256            write_name_tlv(w, interest_name);
257            w.write_nested(tlv_type::META_INFO, |w: &mut TlvWriter| {
258                write_nni(w, tlv_type::FRESHNESS_PERIOD, freshness_ms);
259            });
260            w.write_tlv(tlv_type::CONTENT, &content);
261            w.write_nested(tlv_type::SIGNATURE_INFO, |w: &mut TlvWriter| {
262                w.write_tlv(tlv_type::SIGNATURE_TYPE, &[0u8]);
263            });
264            w.write_tlv(tlv_type::SIGNATURE_VALUE, &[0u8; 32]);
265        });
266        w.finish()
267    }
268
269    fn handle_hello_interest(
270        &self,
271        raw: &Bytes,
272        _incoming_face: FaceId,
273        meta: &InboundMeta,
274        core: &HelloCore,
275        ctx: &dyn DiscoveryContext,
276    ) -> bool {
277        let parsed = match parse_raw_interest(raw) {
278            Some(p) => p,
279            None => return false,
280        };
281
282        let name = &parsed.name;
283        if !name.has_prefix(&core.hello_prefix) {
284            return false;
285        }
286        if name.components().len() != HELLO_PREFIX_DEPTH + 1 {
287            return false;
288        }
289
290        // Extract sender MAC from link-layer metadata.
291        let sender_mac = match &meta.source {
292            Some(LinkAddr::Ether(mac)) => *mac,
293            _ => {
294                debug!("EtherND: hello Interest has no source MAC in meta — ignoring");
295                return true;
296            }
297        };
298
299        // Trigger PassiveDetection when a previously-unknown MAC sends a hello.
300        let is_new = ctx
301            .neighbors()
302            .face_for_peer(&sender_mac, &self.iface)
303            .is_none();
304        if is_new {
305            core.strategy
306                .lock()
307                .unwrap()
308                .trigger(crate::TriggerEvent::PassiveDetection);
309        }
310
311        let reply = self.build_hello_data(core, name);
312        ctx.send_on(self.multicast_face_id, reply);
313
314        debug!(
315            "EtherND: received hello Interest from {:?}, sent Data reply",
316            sender_mac
317        );
318        true
319    }
320
321    fn verify_and_ensure_peer(
322        &self,
323        _raw: &Bytes,
324        payload: &HelloPayload,
325        meta: &InboundMeta,
326        _core: &HelloCore,
327        ctx: &dyn DiscoveryContext,
328    ) -> Option<(Name, Option<FaceId>)> {
329        let responder_name = payload.node_name.clone();
330
331        let responder_mac = match &meta.source {
332            Some(LinkAddr::Ether(mac)) => *mac,
333            _ => {
334                debug!("EtherND: hello Data has no source MAC in meta — ignoring");
335                return None;
336            }
337        };
338
339        let peer_face_id = self.ensure_peer(ctx, &responder_name, responder_mac);
340        Some((responder_name, peer_face_id))
341    }
342
343    fn send_multicast(&self, ctx: &dyn DiscoveryContext, pkt: Bytes) {
344        ctx.send_on(self.multicast_face_id, pkt);
345    }
346
347    fn is_multicast_face(&self, face_id: FaceId) -> bool {
348        face_id == self.multicast_face_id
349    }
350
351    fn on_face_down(&self, _face_id: FaceId, _state: &mut HelloState, _ctx: &dyn DiscoveryContext) {
352        // Ethernet has no link-specific state to clean up on face down.
353    }
354
355    fn on_peer_removed(&self, _entry: &NeighborEntry, _state: &mut HelloState) {
356        // Ethernet has no link-specific state to clean up on peer removal.
357    }
358}
359
360// ── Tests ──────────────────────────────────────────────────────────────────────
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365    use crate::wire::parse_raw_data;
366    use std::str::FromStr;
367
368    fn make_nd() -> EtherNeighborDiscovery {
369        EtherNeighborDiscovery::new(
370            FaceId(1),
371            "eth0",
372            Name::from_str("/ndn/test/node").unwrap(),
373            MacAddr::new([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]),
374        )
375    }
376
377    #[test]
378    fn hello_interest_format() {
379        let nd = make_nd();
380        let nonce: u32 = 0xDEAD_BEEF;
381        let pkt = nd.build_hello_interest(nonce);
382
383        let parsed = parse_raw_interest(&pkt).unwrap();
384        let comps = parsed.name.components();
385
386        assert_eq!(
387            comps.len(),
388            HELLO_PREFIX_DEPTH + 1,
389            "unexpected component count: {}",
390            comps.len()
391        );
392
393        let last = &comps[HELLO_PREFIX_DEPTH];
394        let decoded_nonce = u32::from_be_bytes(last.value[..4].try_into().unwrap());
395        assert_eq!(decoded_nonce, nonce);
396
397        assert!(
398            parsed.app_params.is_none(),
399            "Interest must have no AppParams"
400        );
401    }
402
403    #[test]
404    fn hello_data_carries_hello_payload() {
405        let nd = make_nd();
406        let interest_name = Name::from_str("/ndn/local/nd/hello/DEADBEEF").unwrap();
407        let pkt = nd.medium.build_hello_data(&nd.core, &interest_name);
408
409        let parsed = parse_raw_data(&pkt).unwrap();
410        assert_eq!(parsed.name, interest_name);
411
412        let content = parsed.content.unwrap();
413        let payload = HelloPayload::decode(&content).unwrap();
414        assert_eq!(payload.node_name, nd.core.node_name);
415    }
416
417    #[test]
418    fn in_hello_served_prefixes_encoded() {
419        let nd = make_nd();
420        nd.set_served_prefixes(vec![
421            Name::from_str("/ndn/edu/test").unwrap(),
422            Name::from_str("/ndn/edu/test2").unwrap(),
423        ]);
424
425        let interest_name = Name::from_str("/ndn/local/nd/hello/DEADBEEF").unwrap();
426        let pkt = nd.medium.build_hello_data(&nd.core, &interest_name);
427
428        let parsed = parse_raw_data(&pkt).unwrap();
429        let payload = HelloPayload::decode(&parsed.content.unwrap()).unwrap();
430        assert_eq!(payload.served_prefixes.len(), 2);
431        assert_eq!(
432            payload.served_prefixes[0],
433            Name::from_str("/ndn/edu/test").unwrap()
434        );
435    }
436
437    #[test]
438    fn neighbor_diffs_piggybacked() {
439        let nd = make_nd();
440        {
441            let mut st = nd.core.state.lock().unwrap();
442            st.recent_diffs.push_back(crate::DiffEntry::Add(
443                Name::from_str("/ndn/peer/alpha").unwrap(),
444            ));
445        }
446        let interest_name = Name::from_str("/ndn/local/nd/hello/1").unwrap();
447        let pkt = nd.medium.build_hello_data(&nd.core, &interest_name);
448        let parsed = parse_raw_data(&pkt).unwrap();
449        let payload = HelloPayload::decode(&parsed.content.unwrap()).unwrap();
450        assert_eq!(payload.neighbor_diffs.len(), 1);
451        assert_eq!(payload.neighbor_diffs[0].entries.len(), 1);
452    }
453
454    #[test]
455    fn protocol_id_and_prefix() {
456        let nd = make_nd();
457        assert_eq!(nd.medium.protocol_id(), PROTOCOL);
458        assert_eq!(nd.core.claimed.len(), 1);
459        assert_eq!(
460            nd.core.claimed[0],
461            Name::from_str(crate::hello::medium::HELLO_PREFIX_STR).unwrap()
462        );
463    }
464
465    #[test]
466    fn from_profile_sets_config() {
467        let nd = EtherNeighborDiscovery::from_profile(
468            FaceId(1),
469            "wlan0",
470            Name::from_str("/ndn/test/node").unwrap(),
471            MacAddr::new([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]),
472            &DiscoveryProfile::HighMobility,
473        );
474        assert!(nd.core.config.read().unwrap().hello_interval_base < Duration::from_millis(100));
475    }
476}