dhcp_client/
client.rs

1// Copyright 2023 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::cell::RefCell;
6use std::num::NonZeroU64;
7use std::sync::Arc;
8
9use dhcp_client_core::inspect::Counters;
10use diagnostics_traits::Inspector;
11use fidl::endpoints;
12use fidl_fuchsia_net_dhcp::{
13    self as fdhcp, ClientExitReason, ClientRequestStream, ClientWatchConfigurationResponse,
14    ConfigurationToRequest, NewClientParams,
15};
16use fidl_fuchsia_net_ext::IntoExt as _;
17use futures::channel::mpsc;
18use futures::{StreamExt, TryStreamExt as _};
19use indexmap::IndexSet;
20use net_types::ip::{Ipv4, Ipv4Addr, PrefixLength};
21use net_types::{SpecifiedAddr, Witness as _};
22use rand::SeedableRng as _;
23use {
24    fidl_fuchsia_net as fnet, fidl_fuchsia_net_interfaces as fnet_interfaces,
25    fidl_fuchsia_net_interfaces_admin as fnet_interfaces_admin,
26    fidl_fuchsia_net_interfaces_ext as fnet_interfaces_ext, fuchsia_async as fasync,
27};
28
29use crate::inspect::{Inspect, LeaseChangeInspect, LeaseInspectProperties, StateInspect};
30
31#[derive(thiserror::Error, Debug)]
32pub(crate) enum Error {
33    #[error("DHCP client exiting: {0:?}")]
34    Exit(ClientExitReason),
35
36    #[error("error observed by DHCP client core: {0:?}")]
37    Core(dhcp_client_core::client::Error),
38
39    #[error("fidl error: {0}")]
40    Fidl(fidl::Error),
41}
42
43impl Error {
44    fn from_core(core_error: dhcp_client_core::client::Error) -> Self {
45        match core_error {
46            dhcp_client_core::client::Error::Socket(socket_error) => match socket_error {
47                dhcp_client_core::deps::SocketError::NoInterface
48                | dhcp_client_core::deps::SocketError::UnsupportedHardwareType => {
49                    Self::Exit(ClientExitReason::InvalidInterface)
50                }
51                dhcp_client_core::deps::SocketError::FailedToOpen(e) => {
52                    log::error!("error while trying to open socket: {:?}", e);
53                    Self::Exit(ClientExitReason::UnableToOpenSocket)
54                }
55                dhcp_client_core::deps::SocketError::HostUnreachable
56                | dhcp_client_core::deps::SocketError::Other(_) => {
57                    Self::Core(dhcp_client_core::client::Error::Socket(socket_error))
58                }
59                dhcp_client_core::deps::SocketError::NetworkUnreachable => {
60                    Self::Exit(ClientExitReason::NetworkUnreachable)
61                }
62            },
63            dhcp_client_core::client::Error::AddressEventReceiverEnded => {
64                Self::Exit(ClientExitReason::AddressStateProviderError)
65            }
66        }
67    }
68}
69
70pub(crate) async fn serve_client(
71    mac: net_types::ethernet::Mac,
72    interface_id: NonZeroU64,
73    provider: &crate::packetsocket::PacketSocketProviderImpl,
74    udp_socket_provider: &impl dhcp_client_core::deps::UdpSocketProvider,
75    params: NewClientParams,
76    requests: ClientRequestStream,
77    inspect_root: &fuchsia_inspect::Node,
78) -> Result<(), Error> {
79    let (stop_sender, stop_receiver) = mpsc::unbounded();
80    let stop_sender = &stop_sender;
81    let debug_log_prefix = dhcp_client_core::client::DebugLogPrefix { interface_id };
82    let inspect = Arc::new(Inspect::new());
83    let client = RefCell::new(Client::new(
84        mac,
85        interface_id,
86        params,
87        rand::rngs::StdRng::seed_from_u64(rand::random()),
88        stop_receiver,
89        debug_log_prefix,
90    )?);
91    let counters = Arc::new(Counters::default());
92    let _node = inspect_root.create_lazy_child(interface_id.get().to_string(), {
93        let counters = counters.clone();
94        let inspect = inspect.clone();
95        move || {
96            let inspector = fuchsia_inspect::Inspector::default();
97            {
98                let mut inspector =
99                    diagnostics_traits::FuchsiaInspector::<'_, ()>::new(inspector.root());
100                inspector.record_uint("InterfaceId", interface_id.get());
101                inspect.record(&mut inspector);
102                inspector.record_child("Counters", |inspector| {
103                    counters.record(inspector);
104                });
105            }
106            Box::pin(futures::future::ready(Ok(inspector)))
107        }
108    });
109
110    let counters = counters.as_ref();
111    let inspect = inspect.as_ref();
112    requests
113        .map_err(Error::Fidl)
114        .try_for_each_concurrent(None, |request| {
115            let client = &client;
116            async move {
117                match request {
118                    fidl_fuchsia_net_dhcp::ClientRequest::WatchConfiguration { responder } => {
119                        let mut client = client.try_borrow_mut().map_err(|_| {
120                            Error::Exit(ClientExitReason::WatchConfigurationAlreadyPending)
121                        })?;
122                        responder
123                            .send(
124                                client
125                                    .watch_configuration(
126                                        provider,
127                                        udp_socket_provider,
128                                        counters,
129                                        inspect,
130                                    )
131                                    .await?,
132                            )
133                            .map_err(Error::Fidl)?;
134                        Ok(())
135                    }
136                    fidl_fuchsia_net_dhcp::ClientRequest::Shutdown { control_handle: _ } => {
137                        match stop_sender.unbounded_send(()) {
138                            Ok(()) => stop_sender.close_channel(),
139                            Err(try_send_error) => {
140                                // Note that `try_send_error` cannot be exhaustively matched on.
141                                if try_send_error.is_disconnected() {
142                                    log::warn!(
143                                        "{debug_log_prefix} tried to send shutdown request on \
144                                        already-closed channel to client core"
145                                    );
146                                } else {
147                                    log::error!(
148                                        "{debug_log_prefix} error while sending shutdown request \
149                                        to client core: {:?}",
150                                        try_send_error
151                                    );
152                                }
153                            }
154                        }
155                        Ok(())
156                    }
157                }
158            }
159        })
160        .await
161}
162
163struct Clock;
164
165impl dhcp_client_core::deps::Clock for Clock {
166    type Instant = fasync::MonotonicInstant;
167
168    fn now(&self) -> Self::Instant {
169        fasync::MonotonicInstant::now()
170    }
171
172    async fn wait_until(&self, time: Self::Instant) {
173        fasync::Timer::new(time).await
174    }
175}
176
177/// Encapsulates all DHCP client state.
178struct Client {
179    config: dhcp_client_core::client::ClientConfig,
180    core: dhcp_client_core::client::State<fasync::MonotonicInstant>,
181    rng: rand::rngs::StdRng,
182    stop_receiver: mpsc::UnboundedReceiver<()>,
183    current_lease: Option<Lease>,
184    interface_id: NonZeroU64,
185}
186
187struct Lease {
188    address_state_provider: fnet_interfaces_admin::AddressStateProviderProxy,
189    // The stream of address_assignment state changes for the address.
190    // This stream is stateful and intertwined with the above
191    // address_state_provider proxy. Care must be taken not to call either
192    // `take_event_stream()` or `watch_address_assignment_state()` directly on
193    // the proxy.
194    assignment_state_stream: futures::stream::BoxStream<
195        'static,
196        Result<
197            fnet_interfaces::AddressAssignmentState,
198            fnet_interfaces_ext::admin::AddressStateProviderError,
199        >,
200    >,
201    ip_address: SpecifiedAddr<net_types::ip::Ipv4Addr>,
202}
203
204impl Client {
205    fn new(
206        mac: net_types::ethernet::Mac,
207        interface_id: NonZeroU64,
208        NewClientParams { configuration_to_request, request_ip_address, .. }: NewClientParams,
209        rng: rand::rngs::StdRng,
210        stop_receiver: mpsc::UnboundedReceiver<()>,
211        debug_log_prefix: dhcp_client_core::client::DebugLogPrefix,
212    ) -> Result<Self, Error> {
213        if !request_ip_address.unwrap_or(false) {
214            log::error!(
215                "{debug_log_prefix} client creation failed: \
216                DHCPINFORM is unimplemented"
217            );
218            return Err(Error::Exit(ClientExitReason::InvalidParams));
219        }
220        let ConfigurationToRequest { routers, dns_servers, .. } =
221            configuration_to_request.unwrap_or_else(ConfigurationToRequest::default);
222
223        let config = dhcp_client_core::client::ClientConfig {
224            client_hardware_address: mac,
225            client_identifier: None,
226            requested_parameters: std::iter::once((
227                dhcp_protocol::OptionCode::SubnetMask,
228                dhcp_client_core::parse::OptionRequested::Required,
229            ))
230            .chain(routers.unwrap_or(false).then_some((
231                dhcp_protocol::OptionCode::Router,
232                dhcp_client_core::parse::OptionRequested::Optional,
233            )))
234            .chain(dns_servers.unwrap_or(false).then_some((
235                dhcp_protocol::OptionCode::DomainNameServer,
236                dhcp_client_core::parse::OptionRequested::Optional,
237            )))
238            .collect::<dhcp_client_core::parse::OptionCodeMap<_>>(),
239            preferred_lease_time_secs: None,
240            requested_ip_address: None,
241            debug_log_prefix,
242        };
243        Ok(Self {
244            core: dhcp_client_core::client::State::default(),
245            rng,
246            config,
247            stop_receiver,
248            current_lease: None,
249            interface_id,
250        })
251    }
252
253    async fn handle_newly_acquired_lease(
254        &mut self,
255        dhcp_client_core::client::NewlyAcquiredLease {
256            ip_address,
257            start_time,
258            lease_time,
259            parameters,
260        }: dhcp_client_core::client::NewlyAcquiredLease<fasync::MonotonicInstant>,
261    ) -> Result<(ClientWatchConfigurationResponse, LeaseChangeInspect), Error> {
262        let Self {
263            core: _,
264            rng: _,
265            config: dhcp_client_core::client::ClientConfig { debug_log_prefix, .. },
266            stop_receiver: _,
267            current_lease,
268            interface_id: _,
269        } = self;
270
271        // Store the DNS Servers & Routers in an `IndexSet` to preserve
272        // insertion order & guarantee uniqueness.
273        let mut dns_servers: Option<IndexSet<_>> = None;
274        let mut routers: Option<IndexSet<_>> = None;
275        let mut prefix_len: Option<PrefixLength<Ipv4>> = None;
276        let mut unrequested_options = Vec::new();
277
278        for option in parameters {
279            match option {
280                dhcp_protocol::DhcpOption::SubnetMask(len) => {
281                    let previous_prefix_len = prefix_len.replace(len);
282                    if let Some(prev) = previous_prefix_len {
283                        log::warn!("expected previous_prefix_len to be None, got {prev:?}");
284                    }
285                }
286                dhcp_protocol::DhcpOption::DomainNameServer(list) => {
287                    // Note: The DomainNameServer option may occur multiple
288                    // times. Merge the options into a single list.
289                    dns_servers.get_or_insert_default().extend(list);
290                }
291                dhcp_protocol::DhcpOption::Router(list) => {
292                    // Note: The Router option may occur multiple times. Merge
293                    // the options into a single list.
294                    routers.get_or_insert_default().extend(list);
295                }
296                _ => {
297                    unrequested_options.push(option);
298                }
299            }
300        }
301
302        if !unrequested_options.is_empty() {
303            log::warn!(
304                "{debug_log_prefix} Received options from core that we didn't ask for: {:#?}",
305                unrequested_options
306            );
307        }
308
309        let prefix_len = prefix_len
310            .expect(
311                "subnet mask should be present \
312                because it was specified to core as required",
313            )
314            .get();
315
316        let (asp_proxy, asp_server_end) =
317            endpoints::create_proxy::<fnet_interfaces_admin::AddressStateProviderMarker>();
318
319        let previous_lease = current_lease.replace(Lease {
320            address_state_provider: asp_proxy.clone(),
321            assignment_state_stream: fnet_interfaces_ext::admin::assignment_state_stream(asp_proxy)
322                .boxed(),
323            ip_address,
324        });
325
326        if let Some(previous_lease) = previous_lease {
327            self.remove_address_for_lease(previous_lease).await?;
328        }
329
330        let lease_inspect_properties = LeaseInspectProperties {
331            ip_address,
332            lease_length: lease_time.into(),
333            dns_server_count: dns_servers.as_ref().map(|list| list.len()).unwrap_or(0),
334            routers_count: routers.as_ref().map(|list| list.len()).unwrap_or(0),
335        };
336
337        Ok((
338            ClientWatchConfigurationResponse {
339                address: Some(fdhcp::Address {
340                    address: Some(fnet::Ipv4AddressWithPrefix {
341                        addr: ip_address.get().into_ext(),
342                        prefix_len,
343                    }),
344                    address_parameters: Some(fnet_interfaces_admin::AddressParameters {
345                        initial_properties: Some(fnet_interfaces_admin::AddressProperties {
346                            preferred_lifetime_info: None,
347                            valid_lifetime_end: Some(
348                                zx::MonotonicInstant::from(start_time + lease_time.into())
349                                    .into_nanos(),
350                            ),
351                            ..Default::default()
352                        }),
353                        add_subnet_route: Some(true),
354                        perform_dad: Some(true),
355                        ..Default::default()
356                    }),
357                    address_state_provider: Some(asp_server_end),
358                    ..Default::default()
359                }),
360                dns_servers: dns_servers.map(into_fidl_list),
361                routers: routers.map(into_fidl_list),
362                ..Default::default()
363            },
364            LeaseChangeInspect::LeaseAdded {
365                start_time,
366                prefix_len,
367                properties: lease_inspect_properties,
368            },
369        ))
370    }
371
372    async fn handle_lease_renewal(
373        &mut self,
374        dhcp_client_core::client::LeaseRenewal {
375            start_time,
376            lease_time,
377            parameters,
378        }: dhcp_client_core::client::LeaseRenewal<fasync::MonotonicInstant>,
379    ) -> Result<(ClientWatchConfigurationResponse, LeaseChangeInspect), Error> {
380        let Self {
381            core: _,
382            rng: _,
383            config: dhcp_client_core::client::ClientConfig { debug_log_prefix, .. },
384            stop_receiver: _,
385            current_lease,
386            interface_id: _,
387        } = self;
388
389        // Store the DNS Servers & Routers in an `IndexSet` to preserve
390        // insertion order & guarantee uniqueness.
391        let mut dns_servers: Option<IndexSet<_>> = None;
392        let mut routers: Option<IndexSet<_>> = None;
393        let mut unrequested_options = Vec::new();
394
395        for option in parameters {
396            match option {
397                dhcp_protocol::DhcpOption::SubnetMask(len) => {
398                    log::info!(
399                        "{debug_log_prefix} ignoring prefix length={:?} for renewed lease",
400                        len
401                    );
402                }
403                dhcp_protocol::DhcpOption::DomainNameServer(list) => {
404                    // Note: The DomainNameServer option may occur multiple
405                    // times. Merge the options into a single list.
406                    dns_servers.get_or_insert_default().extend(list);
407                }
408                dhcp_protocol::DhcpOption::Router(list) => {
409                    // Note: The Router option may occur multiple times. Merge
410                    // the options into a single list.
411                    routers.get_or_insert_default().extend(list);
412                }
413                option => {
414                    unrequested_options.push(option);
415                }
416            }
417        }
418
419        if !unrequested_options.is_empty() {
420            log::warn!(
421                "{debug_log_prefix} Received options from core that we didn't ask for: {:#?}",
422                unrequested_options
423            );
424        }
425
426        let Lease { address_state_provider, assignment_state_stream: _, ip_address } =
427            current_lease.as_mut().expect("should have current lease if we're handling a renewal");
428
429        address_state_provider
430            .update_address_properties(&fnet_interfaces_admin::AddressProperties {
431                preferred_lifetime_info: None,
432                valid_lifetime_end: Some(
433                    zx::MonotonicInstant::from(start_time + lease_time.into()).into_nanos(),
434                ),
435                ..Default::default()
436            })
437            .await
438            .map_err(Error::Fidl)?;
439
440        let lease_inspect_properties = LeaseInspectProperties {
441            ip_address: *ip_address,
442            lease_length: lease_time.into(),
443            dns_server_count: dns_servers.as_ref().map(|list| list.len()).unwrap_or(0),
444            routers_count: routers.as_ref().map(|list| list.len()).unwrap_or(0),
445        };
446
447        Ok((
448            ClientWatchConfigurationResponse {
449                address: None,
450                dns_servers: dns_servers.map(into_fidl_list),
451                routers: routers.map(into_fidl_list),
452                ..Default::default()
453            },
454            LeaseChangeInspect::LeaseRenewed {
455                renewed_time: start_time,
456                properties: lease_inspect_properties,
457            },
458        ))
459    }
460
461    async fn remove_address_for_lease(&mut self, lease: Lease) -> Result<(), Error> {
462        let Lease { address_state_provider, assignment_state_stream, ip_address } = lease;
463        address_state_provider.remove().map_err(Error::Fidl)?;
464        // Wait to observe an error on the assignment_state_stream.
465        let watch_result = assignment_state_stream
466            .filter_map(|result| futures::future::ready(result.err()))
467            .next()
468            .await;
469        let debug_log_prefix = &self.config.debug_log_prefix;
470
471        match watch_result {
472            None => log::error!(
473                "{debug_log_prefix} assignment_state_stream unexpectedly ended \
474                while watching for AddressRemovalReason after explicitly \
475                removing address {ip_address}",
476            ),
477            Some(fnet_interfaces_ext::admin::AddressStateProviderError::ChannelClosed) => {
478                log::error!(
479                    "{debug_log_prefix} channel closed while watching for \
480                    AddressRemovalReason after explicitly removing address {ip_address}",
481                )
482            }
483            Some(fnet_interfaces_ext::admin::AddressStateProviderError::Fidl(e)) => log::error!(
484                "{debug_log_prefix} error watching for \
485                AddressRemovalReason after explicitly removing address {ip_address}: {e:?}",
486            ),
487            Some(fnet_interfaces_ext::admin::AddressStateProviderError::AddressRemoved(reason)) => {
488                match reason {
489                    fnet_interfaces_admin::AddressRemovalReason::UserRemoved => (),
490                    reason @ (fnet_interfaces_admin::AddressRemovalReason::Invalid
491                    | fnet_interfaces_admin::AddressRemovalReason::InvalidProperties
492                    | fnet_interfaces_admin::AddressRemovalReason::AlreadyAssigned
493                    | fnet_interfaces_admin::AddressRemovalReason::DadFailed
494                    | fnet_interfaces_admin::AddressRemovalReason::Forfeited
495                    | fnet_interfaces_admin::AddressRemovalReason::InterfaceRemoved) => {
496                        log::error!(
497                            "{debug_log_prefix} unexpected removal reason \
498                            after explicitly removing address {ip_address}: {reason:?}",
499                        );
500                    }
501                }
502            }
503        };
504        Ok(())
505    }
506
507    async fn watch_configuration(
508        &mut self,
509        packet_socket_provider: &crate::packetsocket::PacketSocketProviderImpl,
510        udp_socket_provider: &impl dhcp_client_core::deps::UdpSocketProvider,
511        counters: &Counters,
512        inspect: &Inspect,
513    ) -> Result<ClientWatchConfigurationResponse, Error> {
514        loop {
515            let step = self
516                .watch_configuration_step(packet_socket_provider, udp_socket_provider, counters)
517                .await?;
518            let HandledWatchConfigurationStep { state_inspect, lease_inspect, response_to_return } =
519                self.handle_watch_configuration_step(step, packet_socket_provider).await?;
520
521            // watch_configuration_step only resolves once there's been a state
522            // transition, so we should always update the state inspect and its
523            // history.
524            inspect.update(state_inspect, lease_inspect, self.config.debug_log_prefix);
525            if let Some(response) = response_to_return {
526                return Ok(response);
527            }
528        }
529    }
530
531    async fn handle_watch_configuration_step(
532        &mut self,
533        step: dhcp_client_core::client::Step<fasync::MonotonicInstant, ClientExitReason>,
534        _packet_socket_provider: &crate::packetsocket::PacketSocketProviderImpl,
535    ) -> Result<HandledWatchConfigurationStep, Error> {
536        let Self { core, rng: _, config, stop_receiver: _, current_lease: _, interface_id: _ } =
537            self;
538        match step {
539            dhcp_client_core::client::Step::NextState(transition) => {
540                let (next_core, effect) = core.apply(config, transition);
541                *core = next_core;
542                match effect {
543                    Some(dhcp_client_core::client::TransitionEffect::DropLease {
544                        address_rejected,
545                    }) => {
546                        let current_lease =
547                            self.current_lease.take().expect("should have current lease");
548                        // Skip manual removal of the address, if it was
549                        // rejected (and hence already removed).
550                        if !address_rejected {
551                            self.remove_address_for_lease(current_lease).await?;
552                        }
553                        Ok(HandledWatchConfigurationStep {
554                            state_inspect: StateInspect {
555                                state: next_core,
556                                time: fasync::MonotonicInstant::now(),
557                            },
558                            lease_inspect: LeaseChangeInspect::LeaseDropped,
559                            response_to_return: None,
560                        })
561                    }
562                    Some(dhcp_client_core::client::TransitionEffect::HandleNewLease(
563                        newly_acquired_lease,
564                    )) => {
565                        let (response, lease_inspect) =
566                            self.handle_newly_acquired_lease(newly_acquired_lease).await?;
567                        let start_time = fasync::MonotonicInstant::now();
568                        Ok(HandledWatchConfigurationStep {
569                            state_inspect: StateInspect { state: next_core, time: start_time },
570                            lease_inspect,
571                            response_to_return: Some(response),
572                        })
573                    }
574                    Some(dhcp_client_core::client::TransitionEffect::HandleRenewedLease(
575                        lease_renewal,
576                    )) => {
577                        let (response, lease_inspect) =
578                            self.handle_lease_renewal(lease_renewal).await?;
579                        Ok(HandledWatchConfigurationStep {
580                            state_inspect: StateInspect {
581                                state: next_core,
582                                time: fasync::MonotonicInstant::now(),
583                            },
584                            lease_inspect,
585                            response_to_return: Some(response),
586                        })
587                    }
588                    None => Ok(HandledWatchConfigurationStep {
589                        state_inspect: StateInspect {
590                            state: *core,
591                            time: fasync::MonotonicInstant::now(),
592                        },
593                        lease_inspect: LeaseChangeInspect::NoChange,
594                        response_to_return: None,
595                    }),
596                }
597            }
598            dhcp_client_core::client::Step::Exit(reason) => match reason {
599                dhcp_client_core::client::ExitReason::GracefulShutdown => {
600                    if let Some(current_lease) = self.current_lease.take() {
601                        // TODO(https://fxbug.dev/42079439): Send DHCPRELEASE.
602                        self.remove_address_for_lease(current_lease).await?;
603                    }
604                    return Err(Error::Exit(ClientExitReason::GracefulShutdown));
605                }
606                dhcp_client_core::client::ExitReason::AddressRemoved(reason) => {
607                    return Err(Error::Exit(reason));
608                }
609            },
610        }
611    }
612
613    async fn watch_configuration_step(
614        &mut self,
615        packet_socket_provider: &crate::packetsocket::PacketSocketProviderImpl,
616        udp_socket_provider: &impl dhcp_client_core::deps::UdpSocketProvider,
617        counters: &Counters,
618    ) -> Result<dhcp_client_core::client::Step<fasync::MonotonicInstant, ClientExitReason>, Error>
619    {
620        let Self { core, rng, config, stop_receiver, current_lease, interface_id } = self;
621        let clock = Clock;
622
623        let mut address_event_stream = match current_lease {
624            None => futures::stream::pending().left_stream(),
625            Some(Lease { address_state_provider: _, assignment_state_stream, ip_address }) => {
626                assignment_state_stream
627                    .map(|event| into_address_event(event, config, ip_address, interface_id))
628                    .right_stream()
629            }
630        }
631        .fuse();
632
633        core.run(
634            config,
635            packet_socket_provider,
636            udp_socket_provider,
637            rng,
638            &clock,
639            stop_receiver,
640            &mut address_event_stream,
641            counters,
642        )
643        .await
644        .map_err(Error::from_core)
645    }
646}
647
648/// Convert an event on the AddressStateProvider assignment_state_stream
649/// into a [`dhcp_client_core::client::AddressEvent`].
650fn into_address_event(
651    event: Result<
652        fnet_interfaces::AddressAssignmentState,
653        fnet_interfaces_ext::admin::AddressStateProviderError,
654    >,
655    config: &dhcp_client_core::client::ClientConfig,
656    ip_address: &SpecifiedAddr<Ipv4Addr>,
657    interface_id: &NonZeroU64,
658) -> dhcp_client_core::client::AddressEvent<ClientExitReason> {
659    let debug_log_prefix = &config.debug_log_prefix;
660    match event {
661        Ok(state) => {
662            let new_state = match state {
663                fnet_interfaces::AddressAssignmentState::Assigned => {
664                    dhcp_client_core::client::AddressAssignmentState::Assigned
665                }
666                fnet_interfaces::AddressAssignmentState::Tentative => {
667                    dhcp_client_core::client::AddressAssignmentState::Tentative
668                }
669                fnet_interfaces::AddressAssignmentState::Unavailable => {
670                    dhcp_client_core::client::AddressAssignmentState::Unavailable
671                }
672            };
673            dhcp_client_core::client::AddressEvent::AssignmentStateChanged(new_state)
674        }
675        Err(fnet_interfaces_ext::admin::AddressStateProviderError::AddressRemoved(reason)) => {
676            match reason {
677                r @ fnet_interfaces_admin::AddressRemovalReason::Invalid
678                | r @ fnet_interfaces_admin::AddressRemovalReason::InvalidProperties => {
679                    panic!("invalid address removal: {r:?}")
680                }
681                fnet_interfaces_admin::AddressRemovalReason::InterfaceRemoved => {
682                    log::warn!("{debug_log_prefix} interface removed");
683                    dhcp_client_core::client::AddressEvent::Removed(
684                        ClientExitReason::InvalidInterface,
685                    )
686                }
687                fnet_interfaces_admin::AddressRemovalReason::UserRemoved => {
688                    log::warn!("{debug_log_prefix} address administratively removed");
689                    dhcp_client_core::client::AddressEvent::Removed(
690                        ClientExitReason::AddressRemovedByUser,
691                    )
692                }
693                r @ fnet_interfaces_admin::AddressRemovalReason::AlreadyAssigned
694                | r @ fnet_interfaces_admin::AddressRemovalReason::DadFailed
695                | r @ fnet_interfaces_admin::AddressRemovalReason::Forfeited => {
696                    log::warn!("{debug_log_prefix} address rejected: {r:?}");
697                    dhcp_client_core::client::AddressEvent::Rejected
698                }
699            }
700        }
701        Err(fnet_interfaces_ext::admin::AddressStateProviderError::Fidl(e)) => {
702            log::error!(
703                "{debug_log_prefix} observed error {e:?} while watching for \
704                address event for address {ip_address} on interface {interface_id}; \
705                removing address",
706            );
707            // Note: treat AddressStateProvider FIDL errors the same as address
708            // removal. This ultimately leads to the client exiting.
709            dhcp_client_core::client::AddressEvent::Removed(
710                ClientExitReason::AddressStateProviderError,
711            )
712        }
713        Err(fnet_interfaces_ext::admin::AddressStateProviderError::ChannelClosed) => {
714            log::error!(
715                "{debug_log_prefix} observed channel closed while watching for \
716                address event for address {ip_address} on interface {interface_id};\
717                removing address",
718            );
719            // Note: treat AddressStateProvider channel closure the same as
720            // address removal. This ultimately leads to the client exiting.
721            dhcp_client_core::client::AddressEvent::Removed(
722                ClientExitReason::AddressStateProviderError,
723            )
724        }
725    }
726}
727
728struct HandledWatchConfigurationStep {
729    state_inspect: StateInspect,
730    lease_inspect: LeaseChangeInspect,
731    response_to_return: Option<ClientWatchConfigurationResponse>,
732}
733
734fn into_fidl_list(
735    list: impl IntoIterator<Item = std::net::Ipv4Addr>,
736) -> Vec<fidl_fuchsia_net::Ipv4Address> {
737    list.into_iter().map(|addr| net_types::ip::Ipv4Addr::from(addr).into_ext()).collect()
738}