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