netcfg/
dhcpv4.rs

1// Copyright 2020 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::collections::HashSet;
6use std::pin::Pin;
7
8use fidl_fuchsia_net_dhcp_ext::{self as fnet_dhcp_ext, ClientProviderExt as _};
9use fidl_fuchsia_net_ext::FromExt as _;
10use {
11    fidl_fuchsia_net as fnet, fidl_fuchsia_net_dhcp as fnet_dhcp,
12    fidl_fuchsia_net_interfaces_admin as fnet_interfaces_admin,
13    fidl_fuchsia_net_interfaces_ext as fnet_interfaces_ext, fidl_fuchsia_net_name as fnet_name,
14    fidl_fuchsia_net_routes_admin as fnet_routes_admin,
15};
16
17use anyhow::Context as _;
18use async_utils::stream::{StreamMap, Tagged, WithTag as _};
19use dns_server_watcher::{DnsServers, DnsServersUpdateSource, DEFAULT_DNS_PORT};
20use fuchsia_async::TimeoutExt as _;
21use futures::channel::oneshot;
22use futures::stream::StreamExt as _;
23use futures::FutureExt;
24use log::{info, warn};
25use net_types::ip::Ipv4Addr;
26use net_types::SpecifiedAddr;
27use zx::HandleBased;
28
29use crate::{dns, errors, network, InterfaceId};
30
31#[derive(Debug)]
32pub(super) struct ClientState {
33    routers: HashSet<SpecifiedAddr<Ipv4Addr>>,
34    route_set: fnet_routes_admin::RouteSetV4Proxy,
35    shutdown_sender: oneshot::Sender<()>,
36}
37
38pub(super) fn new_client_params() -> fnet_dhcp::NewClientParams {
39    fnet_dhcp_ext::default_new_client_params()
40}
41
42pub(super) type ConfigurationStreamMap =
43    StreamMap<InterfaceId, InterfaceIdTaggedConfigurationStream>;
44pub(super) type InterfaceIdTaggedConfigurationStream = Tagged<InterfaceId, ConfigurationStream>;
45pub(super) type ConfigurationStream =
46    futures::stream::BoxStream<'static, Result<fnet_dhcp_ext::Configuration, fnet_dhcp_ext::Error>>;
47
48pub(super) async fn probe_for_presence(provider: &fnet_dhcp::ClientProviderProxy) -> bool {
49    match provider.check_presence().await {
50        Ok(()) => true,
51        Err(fidl::Error::ClientChannelClosed { .. }) => false,
52        Err(e) => panic!("unexpected error while probing: {e}"),
53    }
54}
55
56pub(super) async fn update_configuration(
57    interface_id: InterfaceId,
58    ClientState { shutdown_sender: _, routers: configured_routers, route_set }: &mut ClientState,
59    configuration: fnet_dhcp_ext::Configuration,
60    dns_servers: &mut DnsServers,
61    dns_server_watch_responders: &mut dns::DnsServerWatchResponders,
62    netpol_networks_service: &mut network::NetpolNetworksService,
63    control: &fnet_interfaces_ext::admin::Control,
64    lookup_admin: &fnet_name::LookupAdminProxy,
65) {
66    let fnet_dhcp_ext::Configuration {
67        address,
68        dns_servers: new_dns_servers,
69        routers: new_routers,
70        ..
71    } = configuration;
72    if let Some(address) = address {
73        match address.add_to(control) {
74            Ok(()) => {}
75            Err((address, e)) => {
76                let fnet::Ipv4AddressWithPrefix { addr, prefix_len } = address;
77                warn!(
78                    "error adding DHCPv4-acquired address {}/{} for interface {}: {}",
79                    // Make sure address is pretty-printed so that PII filtering
80                    // is properly applied on addresses.
81                    Ipv4Addr::from_ext(addr),
82                    prefix_len,
83                    interface_id,
84                    e,
85                )
86            }
87        }
88    }
89
90    dns::update_servers(
91        lookup_admin,
92        dns_servers,
93        dns_server_watch_responders,
94        netpol_networks_service,
95        DnsServersUpdateSource::Dhcpv4 { interface_id: interface_id.get() },
96        new_dns_servers
97            .iter()
98            .map(|&address| fnet_name::DnsServer_ {
99                address: Some(fnet::SocketAddress::Ipv4(fnet::Ipv4SocketAddress {
100                    address,
101                    port: DEFAULT_DNS_PORT,
102                })),
103                source: Some(fnet_name::DnsServerSource::Dhcp(fnet_name::DhcpDnsServerSource {
104                    source_interface: Some(interface_id.get()),
105                    ..fnet_name::DhcpDnsServerSource::default()
106                })),
107                ..fnet_name::DnsServer_::default()
108            })
109            .collect(),
110    )
111    .await;
112
113    fnet_dhcp_ext::apply_new_routers(
114        interface_id.into(),
115        route_set,
116        configured_routers,
117        new_routers,
118    )
119    .await
120    .unwrap_or_else(|e| {
121        warn!("error applying new DHCPv4-acquired router configuration: {:?}", e);
122    })
123}
124
125pub(super) async fn start_client(
126    interface_id: InterfaceId,
127    interface_name: &str,
128    client_provider: &fnet_dhcp::ClientProviderProxy,
129    route_set_provider: &fnet_routes_admin::RouteTableV4Proxy,
130    interface_admin_auth: &fnet_interfaces_admin::GrantForInterfaceAuthorization,
131    configuration_streams: &mut ConfigurationStreamMap,
132) -> Result<ClientState, errors::Error> {
133    info!("starting DHCPv4 client for {} (id={})", interface_name, interface_id);
134
135    let (route_set, server_end) =
136        fidl::endpoints::create_proxy::<fnet_routes_admin::RouteSetV4Marker>();
137
138    route_set_provider.new_route_set(server_end).expect("create new route set");
139
140    let fnet_interfaces_admin::GrantForInterfaceAuthorization { interface_id: id, token } =
141        interface_admin_auth;
142
143    route_set
144        .authenticate_for_interface(fnet_interfaces_admin::ProofOfInterfaceAuthorization {
145            interface_id: *id,
146            token: token
147                .duplicate_handle(zx::Rights::TRANSFER)
148                .expect("interface auth grant is guaranteed to have ZX_RIGHT_DUPLICATE"),
149        })
150        .await
151        .map_err(|err| {
152            errors::Error::NonFatal(anyhow::anyhow!(
153                "FIDL error while getting authorization: {}",
154                err
155            ))
156        })?
157        .map_err(|err| {
158            errors::Error::NonFatal(anyhow::anyhow!("error while getting authorization: {:?}", err))
159        })?;
160
161    let client = client_provider.new_client_end_ext(interface_id.into(), new_client_params());
162    let (shutdown_sender, shutdown_receiver) = oneshot::channel();
163
164    if let Some(stream) = configuration_streams.insert(
165        interface_id,
166        fnet_dhcp_ext::merged_configuration_stream(
167            client,
168            shutdown_receiver.map(|receive_result| match receive_result {
169                Ok(()) => (),
170                Err(oneshot::Canceled) => (),
171            }),
172        )
173        .boxed()
174        .tagged(interface_id),
175    ) {
176        let _: Pin<Box<InterfaceIdTaggedConfigurationStream>> = stream;
177        unreachable!("only one DHCPv4 client may exist on {} (id={})", interface_name, interface_id)
178    }
179
180    Ok(ClientState { shutdown_sender, route_set, routers: Default::default() })
181}
182
183#[derive(Debug)]
184pub(super) enum AlreadyObservedClientExit {
185    Yes,
186    No,
187}
188
189const TIMEOUT_WAITING_FOR_CLIENT_SHUTDOWN: std::time::Duration = std::time::Duration::from_secs(10);
190
191pub(super) async fn stop_client(
192    interface_id: InterfaceId,
193    interface_name: &str,
194    mut state: ClientState,
195    configuration_streams: &mut ConfigurationStreamMap,
196    dns_servers: &mut DnsServers,
197    dns_server_watch_responders: &mut dns::DnsServerWatchResponders,
198    netpol_networks_service: &mut network::NetpolNetworksService,
199    control: &fnet_interfaces_ext::admin::Control,
200    lookup_admin: &fnet_name::LookupAdminProxy,
201    already_observed_exit: AlreadyObservedClientExit,
202) {
203    info!("stopping DHCPv4 client for {} (id={})", interface_name, interface_id);
204
205    update_configuration(
206        interface_id,
207        &mut state,
208        fnet_dhcp_ext::Configuration::default(),
209        dns_servers,
210        dns_server_watch_responders,
211        netpol_networks_service,
212        control,
213        lookup_admin,
214    )
215    .await;
216
217    let ClientState { shutdown_sender, route_set: _, routers: _ } = state;
218
219    let stream: Pin<Box<InterfaceIdTaggedConfigurationStream>> =
220        configuration_streams.remove(&interface_id).unwrap_or_else(|| {
221            unreachable!(
222                "DHCPv4 client must exist when stopping on {} (id={})",
223                interface_name, interface_id,
224            )
225        });
226
227    match already_observed_exit {
228        AlreadyObservedClientExit::Yes => {
229            // We already saw the client exit due to some error, so just
230            // drop its configuration stream after having cleaned up
231            // any configuration we obtained from it.
232            drop(stream);
233        }
234        AlreadyObservedClientExit::No => {
235            // Tell the client to shut down.
236            shutdown_sender.send(()).expect(
237                "shutdown_receiver should not have been dropped, \
238                      as we still hold a stream holding it",
239            );
240
241            // Await the shutdown event.
242            let terminal_event_result = stream
243                .filter_map(|(_interface_id, item)| match item {
244                    Ok(config) => {
245                        warn!(
246                            "observed DHCPv4 config stream while awaiting shutdown on \
247                            {interface_name} (id={interface_id}): \
248                            {config:?}"
249                        );
250                        futures::future::ready(None)
251                    }
252                    Err(err) => futures::future::ready(Some(err)),
253                })
254                .next()
255                .map(Some)
256                // Avoid blocking forever on the DHCP client if it's not
257                // shutting down.
258                .on_timeout(TIMEOUT_WAITING_FOR_CLIENT_SHUTDOWN, || None)
259                .await;
260
261            match terminal_event_result {
262                None => {
263                    log::error!(
264                        "did not observe terminal event for DHCPv4 client on \
265                        {interface_name} (id={interface_id})"
266                    );
267                }
268                Some(err) => match err {
269                    None => {
270                        info!(
271                            "DHCPv4 client gracefully shut down on {interface_name} \
272                               (id={interface_id})"
273                        );
274                    }
275                    Some(err) => {
276                        warn!(
277                            "DHCPv4 client exited on {interface_name} (id={interface_id}) \
278                                (error={err:?})"
279                        );
280                    }
281                },
282            }
283        }
284    }
285}
286
287/// Start the DHCP server.
288pub(super) async fn start_server(
289    dhcp_server: &fnet_dhcp::Server_Proxy,
290) -> Result<(), errors::Error> {
291    dhcp_server
292        .start_serving()
293        .await
294        .context("error sending start DHCP server request")
295        .map_err(errors::Error::NonFatal)?
296        .map_err(zx::Status::from_raw)
297        .context("error starting DHCP server")
298        .map_err(errors::Error::NonFatal)
299}
300
301/// Stop the DHCP server.
302pub(super) async fn stop_server(
303    dhcp_server: &fnet_dhcp::Server_Proxy,
304) -> Result<(), errors::Error> {
305    dhcp_server
306        .stop_serving()
307        .await
308        .context("error sending stop DHCP server request")
309        .map_err(errors::Error::NonFatal)
310}