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