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