fidl_fuchsia_net_dhcpv6_ext/
lib.rs

1// Copyright 2022 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
5#![deny(missing_docs)]
6
7//! Extension types and helpers for the fuchsia.net.dhcpv6 FIDL library.
8
9use fidl_table_validation::{ValidFidlTable, Validate};
10use futures::future::Either;
11use futures::FutureExt as _;
12
13/// Parameters to configure a new client.
14///
15/// See [`fidl_fuchsia_net_dhcpv6::NewClientParams`].
16#[derive(ValidFidlTable, Debug, Clone, PartialEq)]
17#[fidl_table_src(fidl_fuchsia_net_dhcpv6::NewClientParams)]
18#[fidl_table_strict]
19pub struct NewClientParams {
20    /// The ID of the interface the client will run on.
21    ///
22    /// See [`fidl_fuchsia_net_dhcpv6::NewClientParams::interface_id`].
23    pub interface_id: u64,
24    /// The socket address to use when communicating with servers.
25    ///
26    /// DHCPv6 servers listen for link-local multicasts, so not using a
27    /// link-local address here may cause interoperability issues.
28    ///
29    /// Client creation will fail with `INVALID_ARGS` if:
30    ///
31    /// * a multicast address is provided;
32    /// * or a link-local address is provided, and its zone index
33    ///     doesn't match `interface_id` (Fuchsia has a 1:1 mapping from
34    ///     zone index to interface ID).
35    ///
36    /// Client creation will fail if it fails to bind a socket to this
37    /// address.
38    ///
39    /// See [`fidl_fuchsia_net_dhcpv6::NewClientParams::address`].
40    pub address: fidl_fuchsia_net::Ipv6SocketAddress,
41    /// Configuration for starting the DHCPv6 client.
42    ///
43    /// If the configuration requests both addresses and other
44    /// configuration parameters, all information is requested in the
45    /// same message exchange, running in stateful mode. If only
46    /// configuration parameters are requested (no addresses), the
47    /// client runs in stateless mode, as described in
48    /// [RFC 8415, Section 6.1].
49    ///
50    /// Client creation will fail if `config` is not requesting any
51    /// information (all fields are empty), or if it contains invalid
52    /// fields.
53    ///
54    /// See [`fidl_fuchsia_net_dhcpv6::NewClientParams::config`].
55    ///
56    /// [RFC 8415, Section 6.1]: https://tools.ietf.org/html/rfc8415#section-6.1
57    pub config: ClientConfig,
58    #[fidl_field_type(optional)]
59    /// DHCP Unique Identifier (DUID) configuration.
60    ///
61    /// The DUID is used by the client to identify itself to servers, as defined
62    /// in [RFC 8415 section 11].
63    ///
64    /// [RFC 8415 section 11]: https://datatracker.ietf.org/doc/html/rfc8415#section-11
65    pub duid: Option<fidl_fuchsia_net_dhcpv6::Duid>,
66}
67
68/// Configuration for what the client should request from DHCPv6 server(s).
69///
70/// See [`fidl_fuchsia_net_dhcpv6::ClientConfig`].
71#[derive(ValidFidlTable, Debug, Clone, PartialEq)]
72#[fidl_table_src(fidl_fuchsia_net_dhcpv6::ClientConfig)]
73#[fidl_table_strict]
74pub struct ClientConfig {
75    #[fidl_field_type(default)]
76    /// Configuration for requesting configuration information.
77    ///
78    /// See [`fidl_fuchsia_net_dhcpv6::ClientConfig::information_config`].
79    pub information_config: InformationConfig,
80    #[fidl_field_type(default)]
81    /// Non-temporary address configuration.
82    ///
83    /// Configures the client to negotiate non-temporary
84    /// addresses (IA_NA), as defined in
85    /// [RFC 8415, section 6.2].
86    ///
87    /// See [`fidl_fuchsia_net_dhcpv6::ClientConfig::non_temporary_address_config`].
88    ///
89    /// [RFC 8415, section 6.2]: https://tools.ietf.org/html/rfc8415#section-6.2
90    pub non_temporary_address_config: AddressConfig,
91    #[fidl_field_type(optional)]
92    /// Prefix delegation configuration.
93    ///
94    /// Configures the client to negotiate a delegated prefix
95    /// (IA_PD), as defined in [RFC 8415, section 6.3][RFC 8415 6.3].
96    ///
97    /// Optional. If not set, delegated prefixes will not be
98    /// requested. If invalid, client creation will fail and
99    /// the pipelined channel will be closed.
100    ///
101    /// See [`fidl_fuchsia_net_dhcpv6::ClientConfig::prefix_delegation_config`].
102    ///
103    /// [RFC 8415 6.3]: https://datatracker.ietf.org/doc/html/rfc8415#section-6.3
104    pub prefix_delegation_config: Option<fidl_fuchsia_net_dhcpv6::PrefixDelegationConfig>,
105}
106
107/// Configuration for informational data to request.
108///
109/// See [`fidl_fuchsia_net_dhcpv6::InformationConfig`].
110#[derive(ValidFidlTable, Debug, Clone, PartialEq, Default)]
111#[fidl_table_src(fidl_fuchsia_net_dhcpv6::InformationConfig)]
112#[fidl_table_strict]
113pub struct InformationConfig {
114    #[fidl_field_type(default)]
115    /// See [`fidl_fuchsia_net_dhcpv6::InformationConfig::dns_servers`].
116    pub dns_servers: bool,
117}
118
119/// [`AddressConfig`] custom validation error.
120#[derive(thiserror::Error, Debug)]
121pub enum AddressConfigCustomValidationError {
122    /// More preferred addresses than address count.
123    #[error("more preferred addresses in {preferred_addresses:?} than count {address_count}")]
124    TooManyPreferredAddresses {
125        /// Address count.
126        address_count: u8,
127        /// Preferred addresses.
128        preferred_addresses: Vec<fidl_fuchsia_net::Ipv6Address>,
129    },
130}
131
132/// Custom [`AddressConfig`] validator.
133pub struct AddressConfigValidator;
134
135impl Validate<AddressConfig> for AddressConfigValidator {
136    type Error = AddressConfigCustomValidationError;
137
138    fn validate(
139        AddressConfig { address_count, preferred_addresses }: &AddressConfig,
140    ) -> Result<(), Self::Error> {
141        match preferred_addresses.as_ref() {
142            Some(preferred_addresses) => {
143                if preferred_addresses.len() > (*address_count).into() {
144                    Err(AddressConfigCustomValidationError::TooManyPreferredAddresses {
145                        address_count: *address_count,
146                        preferred_addresses: preferred_addresses.clone(),
147                    })
148                } else {
149                    Ok(())
150                }
151            }
152            None => Ok(()),
153        }
154    }
155}
156
157/// Configuration for requesting addresses.
158///
159/// See [`fidl_fuchsia_net_dhcpv6::AddressConfig`].
160#[derive(ValidFidlTable, Debug, Clone, PartialEq, Default)]
161#[fidl_table_src(fidl_fuchsia_net_dhcpv6::AddressConfig)]
162#[fidl_table_validator(AddressConfigValidator)]
163#[fidl_table_strict]
164pub struct AddressConfig {
165    #[fidl_field_type(default)]
166    /// Number of addresses.
167    ///
168    /// If the value is 0, the client will not negotiate
169    /// non-temporary addresses, i.e. its messages to the
170    /// server will not contain the IA_NA option.
171    ///
172    /// See [`fidl_fuchsia_net_dhcpv6::AddressConfig::address_count`].
173    pub address_count: u8,
174    #[fidl_field_type(optional)]
175    /// Preferred addresses.
176    ///
177    /// The addresses are used as hints by DHCPv6 servers,
178    /// but may be ignored.
179    ///
180    /// The size of `preferred_addresses` must be less than
181    /// or equal to `address_count`, otherwise the
182    /// `AddressConfig` is invalid.
183    ///
184    /// Optional field. If not set, or if
185    /// `preferred_addresses` is empty, no address hints are
186    /// provided.
187    ///
188    /// See [`fidl_fuchsia_net_dhcpv6::AddressConfig::preferred_addresses`].
189    pub preferred_addresses: Option<Vec<fidl_fuchsia_net::Ipv6Address>>,
190}
191
192/// Responses from watch methods on `fuchsia.net.dhcpv6/Client`.
193#[derive(Debug)]
194pub enum WatchItem {
195    /// Return value of `fuchsia.net.dhcpv6/Client.WatchServers`.
196    DnsServers(Vec<fidl_fuchsia_net_name::DnsServer_>),
197    /// Return value of `fuchsia.net.dhcpv6/Client.WatchAddress`.
198    Address {
199        /// The address bits and prefix.
200        addr: fidl_fuchsia_net::Subnet,
201        /// Address parameters.
202        parameters: fidl_fuchsia_net_interfaces_admin::AddressParameters,
203        /// Server end of a `fuchsia.net.interfaces.admin/AddressStateProvider`
204        /// protocol channel.
205        address_state_provider_server_end: fidl::endpoints::ServerEnd<
206            fidl_fuchsia_net_interfaces_admin::AddressStateProviderMarker,
207        >,
208    },
209}
210
211impl WatchItem {
212    /// Constructs a new [`WatchItem::Address`].
213    pub fn new_address(
214        addr: fidl_fuchsia_net::Subnet,
215        parameters: fidl_fuchsia_net_interfaces_admin::AddressParameters,
216        address_state_provider_server_end: fidl::endpoints::ServerEnd<
217            fidl_fuchsia_net_interfaces_admin::AddressStateProviderMarker,
218        >,
219    ) -> Self {
220        Self::Address { addr, parameters, address_state_provider_server_end }
221    }
222}
223
224/// Turns a [`fidl_fuchsia_net_dhcpv6::ClientProxy`] into a stream of items
225/// yielded by calling all hanging get methods on the protocol.
226///
227/// [`fidl_fuchsia_net_dhcpv6::ClientProxy::watch_servers`] and
228/// [`fidl_fuchsia_net_dhcpv6::ClientProxy::watch_address`] must never be
229/// called on the protocol channel `client_proxy` belongs to once this function
230/// returns until the stream ends or returns an error, as only one pending call
231/// is allowed at a time.
232pub fn into_watch_stream(
233    client_proxy: fidl_fuchsia_net_dhcpv6::ClientProxy,
234) -> impl futures::Stream<Item = Result<WatchItem, fidl::Error>> + Unpin {
235    let watch_servers_fut = client_proxy.watch_servers();
236    let watch_address_fut = client_proxy.watch_address();
237    futures::stream::try_unfold(
238        (client_proxy, watch_servers_fut, watch_address_fut),
239        |(client_proxy, watch_servers_fut, watch_address_fut)| {
240            futures::future::select(watch_servers_fut, watch_address_fut).map(|either| {
241                match either {
242                    Either::Left((servers_res, watch_address_fut)) => servers_res.map(|servers| {
243                        let watch_servers_fut = client_proxy.watch_servers();
244                        Some((
245                            WatchItem::DnsServers(servers),
246                            (client_proxy, watch_servers_fut, watch_address_fut),
247                        ))
248                    }),
249                    Either::Right((address_res, watch_servers_fut)) => {
250                        address_res.map(|(addr, parameters, address_state_provider_server_end)| {
251                            let watch_address_fut = client_proxy.watch_address();
252                            Some((
253                                WatchItem::new_address(
254                                    addr,
255                                    parameters,
256                                    address_state_provider_server_end,
257                                ),
258                                (client_proxy, watch_servers_fut, watch_address_fut),
259                            ))
260                        })
261                    }
262                }
263                .or_else(|e| if e.is_closed() { Ok(None) } else { Err(e) })
264            })
265        },
266    )
267}
268
269#[cfg(test)]
270mod tests {
271    use super::{into_watch_stream, WatchItem};
272
273    use assert_matches::assert_matches;
274    use futures::{StreamExt as _, TryStreamExt as _};
275    use net_declare::fidl_ip_v6;
276    use test_case::test_case;
277
278    #[test_case(fidl_fuchsia_net_dhcpv6::AddressConfig {
279        address_count: Some(0),
280        preferred_addresses: Some(vec![fidl_ip_v6!("2001:db8::1")]),
281        ..Default::default()
282    })]
283    #[test_case(fidl_fuchsia_net_dhcpv6::AddressConfig {
284        address_count: Some(1),
285        preferred_addresses: Some(vec![fidl_ip_v6!("2001:db8::1"), fidl_ip_v6!("2001:db8::2")]),
286        ..Default::default()
287    })]
288    #[fuchsia::test]
289    fn address_config_custom_validation(address_config: fidl_fuchsia_net_dhcpv6::AddressConfig) {
290        let (want_address_count, want_preferred_addresses) = assert_matches!(
291            address_config.clone(),
292            fidl_fuchsia_net_dhcpv6::AddressConfig {
293            address_count: Some(address_count),
294            preferred_addresses: Some(preferred_addresses),
295            ..
296        } => (address_count, preferred_addresses));
297
298        assert_matches!(
299            crate::AddressConfig::try_from(address_config),
300            Err(crate::AddressConfigValidationError::Logical(
301                crate::AddressConfigCustomValidationError::TooManyPreferredAddresses {
302                    address_count,
303                    preferred_addresses,
304                }
305            )) => {
306                assert_eq!(address_count, want_address_count);
307                assert_eq!(preferred_addresses, want_preferred_addresses);
308            }
309        );
310    }
311
312    #[derive(Debug, Clone, Copy)]
313    enum WatchType {
314        DnsServers,
315        Address,
316    }
317
318    const SUBNET: fidl_fuchsia_net::Subnet = net_declare::fidl_subnet!("abcd::1/128");
319
320    /// Run a fake server which reads requests from `request_stream` and
321    /// makes responses in the order as given in `response_types`.
322    async fn run_fake_server(
323        request_stream: &mut fidl_fuchsia_net_dhcpv6::ClientRequestStream,
324        response_types: &[WatchType],
325    ) {
326        let (_, _, _): (
327            &mut fidl_fuchsia_net_dhcpv6::ClientRequestStream,
328            Option<fidl_fuchsia_net_dhcpv6::ClientWatchServersResponder>,
329            Option<fidl_fuchsia_net_dhcpv6::ClientWatchAddressResponder>,
330        ) = futures::stream::iter(response_types)
331            .fold(
332                (request_stream, None, None),
333                |(request_stream, mut dns_servers_responder, mut address_responder),
334                 watch_type_to_unblock| async move {
335                    while dns_servers_responder.is_none() || address_responder.is_none() {
336                        match request_stream
337                            .try_next()
338                            .await
339                            .expect("FIDL error")
340                            .expect("request stream ended")
341                        {
342                            fidl_fuchsia_net_dhcpv6::ClientRequest::WatchServers { responder } => {
343                                assert_matches!(dns_servers_responder.replace(responder), None);
344                            }
345                            fidl_fuchsia_net_dhcpv6::ClientRequest::WatchAddress { responder } => {
346                                assert_matches!(address_responder.replace(responder), None);
347                            }
348                            fidl_fuchsia_net_dhcpv6::ClientRequest::WatchPrefixes {
349                                responder: _,
350                            } => {
351                                panic!("WatchPrefix method should not be called");
352                            }
353                            fidl_fuchsia_net_dhcpv6::ClientRequest::Shutdown { responder: _ } => {
354                                panic!("Shutdown method should not be called");
355                            }
356                        }
357                    }
358                    match watch_type_to_unblock {
359                        WatchType::Address => {
360                            let (_, server_end) = fidl::endpoints::create_endpoints::<
361                                fidl_fuchsia_net_interfaces_admin::AddressStateProviderMarker,
362                            >();
363                            address_responder
364                                .take()
365                                .expect("must have address responder")
366                                .send(&SUBNET, &Default::default(), server_end)
367                                .expect("FIDL error");
368                        }
369                        WatchType::DnsServers => {
370                            dns_servers_responder
371                                .take()
372                                .expect("must have DNS servers responder")
373                                .send(&[])
374                                .expect("FIDL error");
375                        }
376                    };
377                    (request_stream, dns_servers_responder, address_responder)
378                },
379            )
380            .await;
381    }
382
383    /// Tests that polling the watcher stream causes all hanging get methods
384    /// to be called, and the items yielded by the stream are in the order
385    /// as the fake server is instructed to unblock the calls.
386    #[test_case(&[WatchType::DnsServers, WatchType::DnsServers]; "dns_servers")]
387    #[test_case(&[WatchType::Address, WatchType::Address]; "address")]
388    #[test_case(&[WatchType::DnsServers, WatchType::Address]; "dns_servers_then_address")]
389    #[test_case(&[WatchType::Address, WatchType::DnsServers]; "address_then_dns_servers")]
390    #[fuchsia::test]
391    async fn watch_stream(watch_types: &[WatchType]) {
392        let (client_proxy, mut request_stream) =
393            fidl::endpoints::create_proxy_and_stream::<fidl_fuchsia_net_dhcpv6::ClientMarker>();
394        let mut watch_stream = into_watch_stream(client_proxy);
395        let client_fut = watch_stream.by_ref().take(watch_types.len()).try_collect::<Vec<_>>();
396        let (r, ()) = futures::future::join(
397            client_fut,
398            run_fake_server(request_stream.by_ref(), watch_types),
399        )
400        .await;
401        let items = r.expect("watch stream error");
402        assert_eq!(items.len(), watch_types.len());
403        for (item, watch_type) in items.into_iter().zip(watch_types) {
404            match watch_type {
405                WatchType::Address => {
406                    assert_matches!(
407                        item,
408                        WatchItem::Address {
409                            addr,
410                            parameters: fidl_fuchsia_net_interfaces_admin::AddressParameters {
411                                initial_properties: None,
412                                temporary: None,
413                                ..
414                            },
415                            address_state_provider_server_end: _,
416                        } if addr == SUBNET
417                    );
418                }
419                WatchType::DnsServers => {
420                    assert_matches!(
421                        item,
422                        WatchItem::DnsServers(dns_servers) if dns_servers.is_empty()
423                    );
424                }
425            }
426        }
427
428        drop(request_stream);
429        assert_matches!(
430            watch_stream.try_collect::<Vec<_>>().await.expect("watch stream error").as_slice(),
431            &[]
432        );
433    }
434}