Skip to main content

fidl_fuchsia_net_ndp_ext/
lib.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use std::num::{NonZeroU32, NonZeroU64};
6
7use derivative::Derivative;
8use futures::StreamExt;
9
10use fidl_fuchsia_net_ext as fnet_ext;
11use fidl_fuchsia_net_ndp as fnet_ndp;
12
13/// Errors regarding the length of an NDP option body observed in an
14/// [`OptionWatchEntry`].
15#[derive(Debug, PartialEq, Eq, Clone, Copy)]
16pub enum BodyLengthError {
17    /// The maximum possible length of an NDP option body
18    /// ([`fidl_fuchsia_net_ndp::MAX_OPTION_BODY_LENGTH`]) was exceeded.
19    MaxLengthExceeded,
20    /// The body's length is not a multiple of 8 octets (after adding two bytes
21    /// for the type and length).
22    NotMultipleOf8,
23}
24
25/// The body of an NDP option.
26///
27/// The raw bytes of the NDP option excluding the leading two bytes for the type
28/// and the length according to [RFC 4861 section
29/// 4.6](https://datatracker.ietf.org/doc/html/rfc4861#section-4.6). The body's
30/// length is guaranteed to be such that if it were prepended with a type octet
31/// and a length octet to match the format described in RFC 4861 section 4.6,
32/// its length would be a multiple of 8 octets (as required by the RFC).
33#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Derivative)]
34#[derivative(Debug)]
35pub struct OptionBody<B = Vec<u8>> {
36    // Avoid including this in debug logs in order to avoid defeating privacy
37    // redaction.
38    #[derivative(Debug = "ignore")]
39    bytes: B,
40}
41
42impl<B> OptionBody<B> {
43    /// Returns the internal bytes storage of the `OptionBody`.
44    pub fn into_inner(self) -> B {
45        self.bytes
46    }
47}
48
49impl<B: AsRef<[u8]>> OptionBody<B> {
50    pub fn new(bytes: B) -> Result<Self, BodyLengthError> {
51        let len = bytes.as_ref().len();
52        if len > fnet_ndp::MAX_OPTION_BODY_LENGTH as usize {
53            return Err(BodyLengthError::MaxLengthExceeded);
54        }
55        if (len + 2) % 8 != 0 {
56            return Err(BodyLengthError::NotMultipleOf8);
57        }
58        Ok(Self { bytes })
59    }
60
61    fn as_ref(&self) -> &[u8] {
62        self.bytes.as_ref()
63    }
64
65    pub fn to_owned(&self) -> OptionBody {
66        let Self { bytes } = self;
67        OptionBody { bytes: bytes.as_ref().to_vec() }
68    }
69}
70
71pub type OptionBodyRef<'a> = OptionBody<&'a [u8]>;
72
73/// Errors observed while converting from FIDL types to this extension crate's
74/// domain types.
75#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
76pub enum FidlConversionError {
77    /// A required field was not set.
78    #[error("required field not set: {0}")]
79    MissingField(&'static str),
80    /// The option's body length does not conform to spec.
81    #[error("body length error: {0:?}")]
82    BodyLength(BodyLengthError),
83    /// The interface ID was zero.
84    #[error("interface ID must be non-zero")]
85    ZeroInterfaceId,
86}
87
88/// The result of attempting to parse an [`OptionWatchEntry`] as a specific NDP
89/// option.
90#[derive(Debug, PartialEq, Eq)]
91pub enum TryParseAsOptionResult<O> {
92    /// The option was successfully parsed from the option body.
93    Parsed(O),
94    /// The [`OptionWatchEntry`]'s `option_type` did not match that of the
95    /// desired option type.
96    OptionTypeMismatch,
97    /// The option type did match the desired option type, but there was an
98    /// error parsing the body.
99    ParseErr(packet::records::options::OptionParseErr),
100}
101
102/// An entry representing a single option received in an NDP message.
103///
104/// The `option_type` and `body` are not guaranteed to be validated in any way
105/// other than the `body` conforming to length requirements as specified in [RFC
106/// 4861 section
107/// 4.6](https://datatracker.ietf.org/doc/html/rfc4861#section-4.6).
108#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
109pub struct OptionWatchEntry {
110    /// The interface on which the NDP message containing the option was
111    /// received.
112    pub interface_id: NonZeroU64,
113    /// The source address of the IPv6 packet containing the NDP message in
114    /// which the option was received.
115    pub source_address: net_types::ip::Ipv6Addr,
116    /// The NDP option type.
117    pub option_type: fnet_ndp::OptionType,
118    /// The body of the NDP option.
119    pub body: OptionBody,
120}
121
122impl OptionWatchEntry {
123    /// Tries to parse this entry as a Recursive DNS Server option.
124    pub fn try_parse_as_rdnss(
125        &self,
126    ) -> TryParseAsOptionResult<packet_formats::icmp::ndp::options::RecursiveDnsServer<'_>> {
127        if self.option_type
128            != u8::from(packet_formats::icmp::ndp::options::NdpOptionType::RecursiveDnsServer)
129        {
130            return TryParseAsOptionResult::OptionTypeMismatch;
131        }
132        packet_formats::icmp::ndp::options::RecursiveDnsServer::parse(self.body.as_ref())
133            .map_or_else(TryParseAsOptionResult::ParseErr, TryParseAsOptionResult::Parsed)
134    }
135}
136
137impl TryFrom<fnet_ndp::OptionWatchEntry> for OptionWatchEntry {
138    type Error = FidlConversionError;
139
140    fn try_from(fidl_entry: fnet_ndp::OptionWatchEntry) -> Result<Self, Self::Error> {
141        let fnet_ndp::OptionWatchEntry {
142            interface_id,
143            source_address,
144            option_type,
145            body,
146            __source_breaking,
147        } = fidl_entry;
148
149        let interface_id = interface_id.ok_or(FidlConversionError::MissingField("interface_id"))?;
150        let source_address =
151            source_address.ok_or(FidlConversionError::MissingField("source_address"))?;
152        let option_type = option_type.ok_or(FidlConversionError::MissingField("option_type"))?;
153        let body = OptionBody::new(body.ok_or(FidlConversionError::MissingField("body"))?)
154            .map_err(FidlConversionError::BodyLength)?;
155        Ok(Self {
156            interface_id: NonZeroU64::new(interface_id)
157                .ok_or(FidlConversionError::ZeroInterfaceId)?,
158            source_address: fnet_ext::FromExt::from_ext(source_address),
159            option_type,
160            body,
161        })
162    }
163}
164
165impl From<OptionWatchEntry> for fnet_ndp::OptionWatchEntry {
166    fn from(value: OptionWatchEntry) -> Self {
167        let OptionWatchEntry { interface_id, source_address, option_type, body } = value;
168        Self {
169            interface_id: Some(interface_id.get()),
170            source_address: Some(fnet_ext::FromExt::from_ext(source_address)),
171            option_type: Some(option_type),
172            body: Some(body.into_inner()),
173            __source_breaking: fidl::marker::SourceBreaking,
174        }
175    }
176}
177
178/// An item in a stream of NDP option-watching hanging-get results.
179#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
180pub enum OptionWatchStreamItem {
181    /// An entry observed in the stream.
182    Entry(OptionWatchEntry),
183    /// Options have been dropped from the stream due to the hanging-get
184    /// consumer falling behind.
185    Dropped(NonZeroU32),
186}
187
188impl OptionWatchStreamItem {
189    /// Tries to convert into an [`OptionWatchEntry`], yielding `self` otherwise.
190    pub fn try_into_entry(self) -> Result<OptionWatchEntry, Self> {
191        match self {
192            Self::Entry(entry) => Ok(entry),
193            Self::Dropped(_) => Err(self),
194        }
195    }
196}
197
198/// An error in a stream of NDP option-watching hanging-get results.
199#[derive(Debug, Clone, thiserror::Error)]
200pub enum OptionWatchStreamError {
201    #[error(transparent)]
202    Fidl(#[from] fidl::Error),
203    #[error(transparent)]
204    Conversion(#[from] FidlConversionError),
205}
206
207/// Creates an option watcher and a stream of its hanging-get results.
208///
209/// Awaits a probe of the watcher returning (indicating that it has been
210/// registered with the netstack) before returning the stream. If the probe
211/// fails with ClientChannelClosed, this is interpreted to mean the
212/// RouterAdvertisementOptionWatcherProvider protocol is not present. In that
213/// event, this returns None. Otherwise, any error encountered is
214/// yielded as Some(Err(...)).
215pub async fn create_watcher_stream(
216    provider: &fnet_ndp::RouterAdvertisementOptionWatcherProviderProxy,
217    params: &fnet_ndp::RouterAdvertisementOptionWatcherParams,
218) -> Option<
219    Result<
220        impl futures::Stream<Item = Result<OptionWatchStreamItem, OptionWatchStreamError>> + use<>,
221        fidl::Error,
222    >,
223> {
224    let (proxy, server_end) = fidl::endpoints::create_proxy::<fnet_ndp::OptionWatcherMarker>();
225    if let Err(e) = provider.new_router_advertisement_option_watcher(server_end, &params) {
226        return Some(Err(e));
227    }
228    proxy
229        .probe()
230        .await
231        .map_err(|e| match e {
232            // Indicates that this protocol isn't present on the
233            // system, so the caller shouldn't use this protocol.
234            fidl::Error::ClientChannelClosed { .. } => return None,
235            err => return Some(err),
236        })
237        .ok()?;
238
239    Some(Ok(futures::stream::try_unfold(proxy, |proxy| async move {
240        Ok(Some((proxy.watch_options().await?, proxy)))
241    })
242    .flat_map(|result: Result<_, fidl::Error>| match result {
243        Err(e) => {
244            futures::stream::once(futures::future::ready(Err(OptionWatchStreamError::Fidl(e))))
245                .left_stream()
246        }
247        Ok((batch, dropped)) => futures::stream::iter(
248            NonZeroU32::new(dropped).map(|dropped| Ok(OptionWatchStreamItem::Dropped(dropped))),
249        )
250        .chain(futures::stream::iter(batch.into_iter().map(|entry| {
251            OptionWatchEntry::try_from(entry)
252                .map(OptionWatchStreamItem::Entry)
253                .map_err(OptionWatchStreamError::Conversion)
254        })))
255        .right_stream(),
256    })))
257}
258
259#[cfg(test)]
260mod test {
261    use super::*;
262
263    use packet::records::options::OptionParseErr;
264    use packet_formats::icmp::ndp::options::RecursiveDnsServer;
265    use test_case::test_case;
266
267    use fidl_fuchsia_net as fnet;
268
269    const INTERFACE_ID: NonZeroU64 = NonZeroU64::new(1).unwrap();
270    const NET_SOURCE_ADDRESS: net_types::ip::Ipv6Addr = net_declare::net_ip_v6!("fe80::1");
271    const FIDL_SOURCE_ADDRESS: fnet::Ipv6Address = net_declare::fidl_ip_v6!("fe80::1");
272    const OPTION_TYPE: u8 = 1;
273    const BODY: [u8; 6] = [1, 2, 3, 4, 5, 6];
274
275    fn valid_fidl_entry() -> fnet_ndp::OptionWatchEntry {
276        fnet_ndp::OptionWatchEntry {
277            interface_id: Some(INTERFACE_ID.get()),
278            source_address: Some(FIDL_SOURCE_ADDRESS),
279            option_type: Some(OPTION_TYPE),
280            body: Some(BODY.to_vec()),
281            __source_breaking: fidl::marker::SourceBreaking,
282        }
283    }
284
285    fn valid_ext_entry() -> OptionWatchEntry {
286        OptionWatchEntry {
287            interface_id: INTERFACE_ID,
288            source_address: NET_SOURCE_ADDRESS,
289            option_type: OPTION_TYPE,
290            body: OptionBody::new(BODY.to_vec()).expect("should be valid option body"),
291        }
292    }
293
294    #[test_case(valid_fidl_entry() => Ok(valid_ext_entry()))]
295    #[test_case(fnet_ndp::OptionWatchEntry {
296        interface_id: None,
297        ..valid_fidl_entry()
298    } => Err(FidlConversionError::MissingField("interface_id")))]
299    #[test_case(fnet_ndp::OptionWatchEntry {
300        source_address: None,
301        ..valid_fidl_entry()
302    } => Err(FidlConversionError::MissingField("source_address")))]
303    #[test_case(fnet_ndp::OptionWatchEntry {
304        option_type: None,
305        ..valid_fidl_entry()
306    } => Err(FidlConversionError::MissingField("option_type")))]
307    #[test_case(fnet_ndp::OptionWatchEntry {
308        body: None,
309        ..valid_fidl_entry()
310    } => Err(FidlConversionError::MissingField("body")))]
311    #[test_case(fnet_ndp::OptionWatchEntry {
312        interface_id: Some(0),
313        ..valid_fidl_entry()
314    } => Err(FidlConversionError::ZeroInterfaceId))]
315    #[test_case(fnet_ndp::OptionWatchEntry {
316        body: Some(vec![1; fnet_ndp::MAX_OPTION_BODY_LENGTH as usize + 1]),
317        ..valid_fidl_entry()
318    } => Err(FidlConversionError::BodyLength(BodyLengthError::MaxLengthExceeded)))]
319    #[test_case(fnet_ndp::OptionWatchEntry {
320        body: Some(vec![1; 7]),
321        ..valid_fidl_entry()
322    } => Err(FidlConversionError::BodyLength(BodyLengthError::NotMultipleOf8)))]
323    fn convert_option_watch_entry(
324        entry: fnet_ndp::OptionWatchEntry,
325    ) -> Result<OptionWatchEntry, FidlConversionError> {
326        OptionWatchEntry::try_from(entry)
327    }
328
329    fn recursive_dns_server_option_and_bytes() -> (RecursiveDnsServer<'static>, Vec<u8>) {
330        const ADDRESSES: [net_types::ip::Ipv6Addr; 2] =
331            [net_declare::net_ip_v6!("2001:db8::1"), net_declare::net_ip_v6!("2001:db8::2")];
332        let option = RecursiveDnsServer::new(u32::MAX, &ADDRESSES);
333        let builder = packet_formats::icmp::ndp::options::NdpOptionBuilder::RecursiveDnsServer(
334            option.clone(),
335        );
336        let len = packet::records::options::OptionBuilder::serialized_len(&builder);
337        let mut data = vec![0u8; len];
338        packet::records::options::OptionBuilder::serialize_into(&builder, &mut data);
339        (option, data)
340    }
341
342    #[test]
343    fn try_parse_as_rdnss_succeeds() {
344        let (option, bytes) = recursive_dns_server_option_and_bytes();
345        let entry = OptionWatchEntry {
346            interface_id: INTERFACE_ID,
347            source_address: NET_SOURCE_ADDRESS,
348            option_type: u8::from(
349                packet_formats::icmp::ndp::options::NdpOptionType::RecursiveDnsServer,
350            ),
351            body: OptionBody::new(bytes).unwrap(),
352        };
353        assert_eq!(entry.try_parse_as_rdnss(), TryParseAsOptionResult::Parsed(option));
354    }
355
356    #[test]
357    fn try_parse_as_rdnss_option_type_mismatch() {
358        let (_option, bytes) = recursive_dns_server_option_and_bytes();
359        let entry = OptionWatchEntry {
360            interface_id: INTERFACE_ID,
361            source_address: NET_SOURCE_ADDRESS,
362            option_type: u8::from(packet_formats::icmp::ndp::options::NdpOptionType::Nonce),
363            body: OptionBody::new(bytes).unwrap(),
364        };
365        assert_eq!(entry.try_parse_as_rdnss(), TryParseAsOptionResult::OptionTypeMismatch);
366    }
367
368    #[test]
369    fn try_parse_as_rdnss_fails() {
370        let (_option, bytes) = recursive_dns_server_option_and_bytes();
371        let entry = OptionWatchEntry {
372            interface_id: INTERFACE_ID,
373            source_address: NET_SOURCE_ADDRESS,
374            option_type: u8::from(
375                packet_formats::icmp::ndp::options::NdpOptionType::RecursiveDnsServer,
376            ),
377            body: OptionBody::new(vec![0u8; bytes.len()]).unwrap(),
378        };
379        assert_eq!(entry.try_parse_as_rdnss(), TryParseAsOptionResult::ParseErr(OptionParseErr));
380    }
381}