1use std::collections::{HashMap, HashSet};
6
7use {
8 fidl_fuchsia_net as fnet, fidl_fuchsia_net_dhcpv6 as fnet_dhcpv6,
9 fidl_fuchsia_net_dhcpv6_ext as fnet_dhcpv6_ext, fidl_fuchsia_net_ext as fnet_ext,
10 fidl_fuchsia_net_name as fnet_name,
11};
12
13use anyhow::Context as _;
14use async_utils::hanging_get::client::HangingGetStream;
15use async_utils::stream::{StreamMap, Tagged};
16use dns_server_watcher::{DnsServers, DnsServersUpdateSource};
17use futures::future::TryFutureExt as _;
18use futures::stream::{Stream, TryStreamExt as _};
19use log::warn;
20
21use crate::{dns, errors, DnsServerWatchers, InterfaceId};
22
23pub(super) fn duid(mac: fnet_ext::MacAddress) -> fnet_dhcpv6::Duid {
25 fnet_dhcpv6::Duid::LinkLayerAddress(fnet_dhcpv6::LinkLayerAddress::Ethernet(mac.into()))
26}
27
28#[derive(Copy, Clone, Debug, PartialEq)]
29pub(super) struct PrefixOnInterface {
30 interface_id: InterfaceId,
31 prefix: net_types::ip::Subnet<net_types::ip::Ipv6Addr>,
32 lifetimes: Lifetimes,
33}
34
35pub(super) type Prefixes = HashMap<net_types::ip::Subnet<net_types::ip::Ipv6Addr>, Lifetimes>;
36pub(super) type InterfaceIdTaggedPrefixesStream = Tagged<InterfaceId, PrefixesStream>;
37pub(super) type PrefixesStreamMap = StreamMap<InterfaceId, InterfaceIdTaggedPrefixesStream>;
38
39#[derive(Debug)]
40pub(super) struct ClientState {
41 pub(super) sockaddr: fnet::Ipv6SocketAddress,
42 pub(super) prefixes: Prefixes,
43}
44
45impl ClientState {
46 pub(super) fn new(sockaddr: fnet::Ipv6SocketAddress) -> Self {
47 Self { sockaddr, prefixes: Default::default() }
48 }
49}
50
51#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
52pub(super) struct Lifetimes {
53 preferred_until: zx::MonotonicInstant,
54 valid_until: zx::MonotonicInstant,
55}
56
57impl Into<fnet_dhcpv6::Lifetimes> for Lifetimes {
58 fn into(self) -> fnet_dhcpv6::Lifetimes {
59 let Self { preferred_until, valid_until } = self;
60 fnet_dhcpv6::Lifetimes {
61 preferred_until: preferred_until.into_nanos(),
62 valid_until: valid_until.into_nanos(),
63 }
64 }
65}
66
67pub(super) type PrefixesStream =
68 HangingGetStream<fnet_dhcpv6::ClientProxy, Vec<fnet_dhcpv6::Prefix>>;
69
70pub(super) fn from_fidl_prefixes(
71 fidl_prefixes: &[fnet_dhcpv6::Prefix],
72) -> Result<Prefixes, anyhow::Error> {
73 let prefixes = fidl_prefixes
74 .iter()
75 .map(
76 |&fnet_dhcpv6::Prefix {
77 prefix:
78 fnet::Ipv6AddressWithPrefix { addr: fnet::Ipv6Address { addr }, prefix_len },
79 lifetimes: fnet_dhcpv6::Lifetimes { valid_until, preferred_until },
80 }| {
81 let subnet = net_types::ip::Subnet::new(
82 net_types::ip::Ipv6Addr::from_bytes(addr),
83 prefix_len,
84 )
85 .map_err(|e| anyhow::anyhow!("subnet parsing error: {:?}", e))?;
86 if valid_until == 0 {
87 return Err(anyhow::anyhow!(
88 "received DHCPv6 prefix {:?} with valid-until time of 0",
89 subnet
90 ));
91 }
92 if preferred_until == 0 {
93 return Err(anyhow::anyhow!(
94 "received DHCPv6 prefix {:?} with preferred-until time of 0",
95 subnet
96 ));
97 }
98 Ok((
99 subnet,
100 Lifetimes {
101 valid_until: zx::MonotonicInstant::from_nanos(valid_until),
102 preferred_until: zx::MonotonicInstant::from_nanos(preferred_until),
103 },
104 ))
105 },
106 )
107 .collect::<Result<Prefixes, _>>()?;
108 if prefixes.len() != fidl_prefixes.len() {
109 return Err(anyhow::anyhow!(
110 "DHCPv6 prefixes {:?} contains duplicate prefix",
111 fidl_prefixes
112 ));
113 }
114 Ok(prefixes)
115}
116
117pub(super) fn start_client(
119 dhcpv6_client_provider: &fnet_dhcpv6::ClientProviderProxy,
120 interface_id: InterfaceId,
121 sockaddr: fnet::Ipv6SocketAddress,
122 duid: fnet_dhcpv6::Duid,
123 prefix_delegation_config: Option<fnet_dhcpv6::PrefixDelegationConfig>,
124) -> Result<
125 (impl Stream<Item = Result<Vec<fnet_name::DnsServer_>, fidl::Error>>, PrefixesStream),
126 errors::Error,
127> {
128 let stateful = prefix_delegation_config.is_some();
129 let params = fnet_dhcpv6_ext::NewClientParams {
130 interface_id: interface_id.get(),
131 address: sockaddr,
132 config: fnet_dhcpv6_ext::ClientConfig {
133 information_config: fnet_dhcpv6_ext::InformationConfig { dns_servers: true },
134 non_temporary_address_config: Default::default(),
135 prefix_delegation_config,
136 },
137 duid: stateful.then_some(duid),
138 };
139 let (client, server) = fidl::endpoints::create_proxy::<fnet_dhcpv6::ClientMarker>();
140
141 dhcpv6_client_provider
144 .new_client(¶ms.into(), server)
145 .context("error creating new DHCPv6 client")
146 .map_err(errors::Error::NonFatal)?;
147
148 let dns_servers_stream = futures::stream::try_unfold(client.clone(), move |proxy| {
149 proxy.watch_servers().map_ok(move |s| Some((s, proxy)))
150 });
151 let prefixes_stream =
152 PrefixesStream::new_eager_with_fn_ptr(client, fnet_dhcpv6::ClientProxy::watch_prefixes);
153
154 Ok((dns_servers_stream, prefixes_stream))
155}
156
157fn get_suitable_dhcpv6_prefix(
158 current_prefix: Option<PrefixOnInterface>,
159 interface_states: &HashMap<InterfaceId, crate::InterfaceState>,
160 allowed_upstream_device_classes: &HashSet<crate::DeviceClass>,
161 interface_config: AcquirePrefixInterfaceConfig,
162) -> Option<PrefixOnInterface> {
163 if let Some(PrefixOnInterface { interface_id, prefix, lifetimes: _ }) = current_prefix {
164 let crate::InterfaceState { config, .. } =
165 interface_states.get(&interface_id).unwrap_or_else(|| {
166 panic!(
167 "interface {} cannot be found but provides current prefix = {:?}",
168 interface_id, current_prefix,
169 )
170 });
171 match config {
172 crate::InterfaceConfigState::Host(crate::HostInterfaceState {
173 dhcpv4_client: _,
174 dhcpv6_client_state,
175 dhcpv6_pd_config: _,
176 interface_admin_auth: _,
177 interface_naming_id: _,
178 }) => {
179 let Some(ClientState { prefixes, sockaddr: _ }) = dhcpv6_client_state.as_ref()
180 else {
181 return None;
184 };
185 if let Some(lifetimes) = prefixes.get(&prefix) {
186 return Some(PrefixOnInterface { interface_id, prefix, lifetimes: *lifetimes });
187 }
188 }
189 crate::InterfaceConfigState::WlanAp(wlan_ap_state) => {
190 panic!(
191 "interface {} not expected to be WLAN AP with state: {:?}",
192 interface_id, wlan_ap_state,
193 );
194 }
195 crate::InterfaceConfigState::Blackhole(state) => {
196 panic!(
197 "interface {} not expected to be blackhole with state: {:?}",
198 interface_id, state,
199 );
200 }
201 }
202 }
203
204 interface_states
205 .iter()
206 .filter_map(|(interface_id, crate::InterfaceState { config, device_class, .. })| {
207 let prefixes = match config {
208 crate::InterfaceConfigState::Host(crate::HostInterfaceState {
209 dhcpv4_client: _,
210 dhcpv6_client_state,
211 dhcpv6_pd_config: _,
212 interface_admin_auth: _,
213 interface_naming_id: _,
214 }) => {
215 if let Some(ClientState { prefixes, sockaddr: _ }) = dhcpv6_client_state {
216 prefixes
217 } else {
218 return None;
219 }
220 }
221 crate::InterfaceConfigState::WlanAp(crate::WlanApInterfaceState {
222 interface_naming_id: _,
223 })
224 | crate::InterfaceConfigState::Blackhole(_) => {
225 return None;
226 }
227 };
228 match interface_config {
229 AcquirePrefixInterfaceConfig::Upstreams => {
230 allowed_upstream_device_classes.contains(&device_class)
231 }
232 AcquirePrefixInterfaceConfig::Id(want_id) => interface_id.get() == want_id,
233 }
234 .then(|| {
235 prefixes.iter().map(|(&prefix, &lifetimes)| PrefixOnInterface {
236 interface_id: *interface_id,
237 prefix,
238 lifetimes,
239 })
240 })
241 })
242 .flatten()
243 .max_by(
244 |PrefixOnInterface {
245 interface_id: _,
246 prefix: _,
247 lifetimes:
248 Lifetimes { preferred_until: preferred_until1, valid_until: valid_until1 },
249 },
250 PrefixOnInterface {
251 interface_id: _,
252 prefix: _,
253 lifetimes:
254 Lifetimes { preferred_until: preferred_until2, valid_until: valid_until2 },
255 }| {
256 (preferred_until1, valid_until1).cmp(&(preferred_until2, valid_until2))
259 },
260 )
261}
262
263pub(super) fn maybe_send_watch_prefix_response(
264 interface_states: &HashMap<InterfaceId, crate::InterfaceState>,
265 allowed_upstream_device_classes: &HashSet<crate::DeviceClass>,
266 prefix_provider_handler: Option<&mut PrefixProviderHandler>,
267) -> Result<(), anyhow::Error> {
268 let PrefixProviderHandler {
269 current_prefix,
270 interface_config,
271 preferred_prefix_len: _,
272 watch_prefix_responder,
273 prefix_control_request_stream: _,
274 } = if let Some(handler) = prefix_provider_handler {
275 handler
276 } else {
277 return Ok(());
278 };
279
280 let new_prefix = get_suitable_dhcpv6_prefix(
281 *current_prefix,
282 interface_states,
283 allowed_upstream_device_classes,
284 *interface_config,
285 );
286 if new_prefix == *current_prefix {
287 return Ok(());
288 }
289
290 if let Some(responder) = watch_prefix_responder.take() {
291 responder
292 .send(&new_prefix.map_or(
293 fnet_dhcpv6::PrefixEvent::Unassigned(fnet_dhcpv6::Empty),
294 |PrefixOnInterface { interface_id: _, prefix, lifetimes }| {
295 fnet_dhcpv6::PrefixEvent::Assigned(fnet_dhcpv6::Prefix {
296 prefix: fnet::Ipv6AddressWithPrefix {
297 addr: fnet::Ipv6Address { addr: prefix.network().ipv6_bytes() },
298 prefix_len: prefix.prefix(),
299 },
300 lifetimes: lifetimes.into(),
301 })
302 },
303 ))
304 .context("failed to send PrefixControl.WatchPrefix response")?;
305 *current_prefix = new_prefix;
306 }
307
308 Ok(())
309}
310
311pub(super) async fn stop_client(
315 lookup_admin: &fnet_name::LookupAdminProxy,
316 dns_servers: &mut DnsServers,
317 dns_server_watch_responders: &mut dns::DnsServerWatchResponders,
318 interface_id: InterfaceId,
319 watchers: &mut DnsServerWatchers<'_>,
320 prefixes_streams: &mut PrefixesStreamMap,
321) {
322 let source = DnsServersUpdateSource::Dhcpv6 { interface_id: interface_id.get() };
323
324 if let None = watchers.remove(&source) {
326 warn!(
330 "DNS Watcher for key not present; multiple futures stopped DHCPv6 \
331 client for key {:?}; interface_id={}",
332 source, interface_id
333 );
334 }
335 if let None = prefixes_streams.remove(&interface_id) {
336 warn!(
340 "Prefix Stream for key not present; multiple futures stopped DHCPv6 \
341 client for key {:?}; interface_id={}",
342 source, interface_id
343 );
344 }
345
346 dns::update_servers(lookup_admin, dns_servers, dns_server_watch_responders, source, vec![])
347 .await
348}
349
350#[derive(Clone, Copy, PartialEq, Eq, Hash)]
351pub(super) enum AcquirePrefixInterfaceConfig {
352 Upstreams,
353 Id(u64),
354}
355
356pub(super) struct PrefixProviderHandler {
357 pub(super) prefix_control_request_stream: fnet_dhcpv6::PrefixControlRequestStream,
358 pub(super) watch_prefix_responder: Option<fnet_dhcpv6::PrefixControlWatchPrefixResponder>,
359 pub(super) preferred_prefix_len: Option<u8>,
360 pub(super) interface_config: AcquirePrefixInterfaceConfig,
362 pub(super) current_prefix: Option<PrefixOnInterface>,
363}
364
365impl PrefixProviderHandler {
366 pub(super) fn try_next_prefix_control_request(
367 &mut self,
368 ) -> futures::stream::TryNext<'_, fnet_dhcpv6::PrefixControlRequestStream> {
369 self.prefix_control_request_stream.try_next()
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use fidl_fuchsia_net_interfaces_admin as fnet_interfaces_admin;
376
377 use net_declare::{fidl_socket_addr_v6, net_subnet_v6};
378 use test_case::test_case;
379
380 use crate::interface::{generate_identifier, InterfaceNamingIdentifier, ProvisioningAction};
381 use crate::{DeviceClass, HostInterfaceState, InterfaceConfigState, InterfaceState};
382
383 use super::*;
384
385 const ALLOWED_UPSTREAM_DEVICE_CLASS: crate::DeviceClass = crate::DeviceClass::Ethernet;
386 const DISALLOWED_UPSTREAM_DEVICE_CLASS: crate::DeviceClass = crate::DeviceClass::Virtual;
387 const LIFETIMES: Lifetimes = Lifetimes {
388 preferred_until: zx::MonotonicInstant::from_nanos(123_000_000_000),
389 valid_until: zx::MonotonicInstant::from_nanos(456_000_000_000),
390 };
391 const RENEWED_LIFETIMES: Lifetimes = Lifetimes {
392 preferred_until: zx::MonotonicInstant::from_nanos(777_000_000_000),
393 valid_until: zx::MonotonicInstant::from_nanos(888_000_000_000),
394 };
395
396 impl InterfaceState {
397 fn new_host_with_state(
398 interface_naming_id: InterfaceNamingIdentifier,
399 control: fidl_fuchsia_net_interfaces_ext::admin::Control,
400 device_class: DeviceClass,
401 dhcpv6_pd_config: Option<fnet_dhcpv6::PrefixDelegationConfig>,
402 dhcpv6_client_state: Option<ClientState>,
403 provisioning: ProvisioningAction,
404 interface_admin_auth: fnet_interfaces_admin::GrantForInterfaceAuthorization,
405 ) -> Self {
406 Self {
407 control,
408 config: InterfaceConfigState::Host(HostInterfaceState {
409 dhcpv4_client: crate::Dhcpv4ClientState::NotRunning,
410 dhcpv6_client_state,
411 dhcpv6_pd_config,
412 interface_admin_auth,
413 interface_naming_id,
414 }),
415 device_class,
416 provisioning,
417 }
418 }
419 }
420
421 fn fake_interface_grant() -> fnet_interfaces_admin::GrantForInterfaceAuthorization {
422 fnet_interfaces_admin::GrantForInterfaceAuthorization {
423 interface_id: 0,
424 token: zx::Event::create(),
425 }
426 }
427
428 #[test_case(
429 None,
430 [
431 (
432 DISALLOWED_UPSTREAM_DEVICE_CLASS,
433 Some(HashMap::from([(net_subnet_v6!("abcd::/64"), LIFETIMES)])),
434 )
435 ].into_iter(),
436 AcquirePrefixInterfaceConfig::Upstreams,
437 None;
438 "not_upstream"
439 )]
440 #[test_case(
441 None,
442 [
443 (
444 ALLOWED_UPSTREAM_DEVICE_CLASS,
445 Some(HashMap::from([(net_subnet_v6!("abcd::/64"), LIFETIMES)])),
446 )
447 ].into_iter(),
448 AcquirePrefixInterfaceConfig::Upstreams,
449 Some(PrefixOnInterface {
450 interface_id: InterfaceId::new(1).unwrap(),
451 prefix: net_subnet_v6!("abcd::/64"),
452 lifetimes: LIFETIMES,
453 });
454 "none_to_some"
455 )]
456 #[test_case(
457 Some(PrefixOnInterface {
458 interface_id: InterfaceId::new(1).unwrap(),
459 prefix: net_subnet_v6!("abcd::/64"),
460 lifetimes: LIFETIMES,
461 }),
462 [
463 (
464 ALLOWED_UPSTREAM_DEVICE_CLASS,
465 Some(HashMap::from([(net_subnet_v6!("abcd::/64"), LIFETIMES)])),
466 )
467 ].into_iter(),
468 AcquirePrefixInterfaceConfig::Upstreams,
469 Some(PrefixOnInterface {
470 interface_id: InterfaceId::new(1).unwrap(),
471 prefix: net_subnet_v6!("abcd::/64"),
472 lifetimes: LIFETIMES,
473 });
474 "same"
475 )]
476 #[test_case(
477 Some(PrefixOnInterface {
478 interface_id: InterfaceId::new(1).unwrap(),
479 prefix: net_subnet_v6!("abcd::/64"),
480 lifetimes: LIFETIMES,
481 }),
482 [
483 (
484 ALLOWED_UPSTREAM_DEVICE_CLASS,
485 Some(HashMap::from([(net_subnet_v6!("abcd::/64"), RENEWED_LIFETIMES)])),
486 )
487 ].into_iter(),
488 AcquirePrefixInterfaceConfig::Upstreams,
489 Some(PrefixOnInterface {
490 interface_id: InterfaceId::new(1).unwrap(),
491 prefix: net_subnet_v6!("abcd::/64"),
492 lifetimes: RENEWED_LIFETIMES,
493 });
494 "lifetime_changed"
495 )]
496 #[test_case(
497 Some(PrefixOnInterface {
498 interface_id: InterfaceId::new(1).unwrap(),
499 prefix: net_subnet_v6!("abcd::/64"),
500 lifetimes: LIFETIMES,
501 }),
502 [
503 (
504 ALLOWED_UPSTREAM_DEVICE_CLASS,
505 Some(HashMap::new()),
506 ),
507 (
508 ALLOWED_UPSTREAM_DEVICE_CLASS,
509 Some(HashMap::from([(net_subnet_v6!("efff::/64"), RENEWED_LIFETIMES)])),
510 )
511 ].into_iter(),
512 AcquirePrefixInterfaceConfig::Upstreams,
513 Some(PrefixOnInterface {
514 interface_id: InterfaceId::new(2).unwrap(),
515 prefix: net_subnet_v6!("efff::/64"),
516 lifetimes: RENEWED_LIFETIMES,
517 });
518 "different_interface"
519 )]
520 #[fuchsia::test]
521 async fn get_suitable_dhcpv6_prefix(
522 current_prefix: Option<PrefixOnInterface>,
523 interface_state_iter: impl IntoIterator<Item = (crate::DeviceClass, Option<Prefixes>)>,
524 interface_config: AcquirePrefixInterfaceConfig,
525 want: Option<PrefixOnInterface>,
526 ) {
527 let interface_states = (1..)
528 .flat_map(InterfaceId::new)
529 .zip(interface_state_iter.into_iter().map(|(device_class, prefixes)| {
530 let (control, _control_server_end) =
531 fidl_fuchsia_net_interfaces_ext::admin::Control::create_endpoints()
532 .expect("create endpoints");
533 InterfaceState::new_host_with_state(
534 generate_identifier(&fidl_fuchsia_net_ext::MacAddress {
535 octets: [0x1, 0x2, 0x3, 0x4, 0x5, 0x6],
536 }),
537 control,
538 device_class,
539 None,
540 prefixes.map(|prefixes| ClientState {
541 sockaddr: fidl_socket_addr_v6!("[fe80::1]:546"),
542 prefixes: prefixes,
543 }),
544 ProvisioningAction::Local,
545 fake_interface_grant(),
546 )
547 }))
548 .collect();
549 let allowed_upstream_device_classes = HashSet::from([ALLOWED_UPSTREAM_DEVICE_CLASS]);
550 assert_eq!(
551 super::get_suitable_dhcpv6_prefix(
552 current_prefix,
553 &interface_states,
554 &allowed_upstream_device_classes,
555 interface_config,
556 ),
557 want
558 );
559 }
560}