1mod capture;
6mod filter;
7mod opts;
8mod ser;
9
10use anyhow::{Context as _, Error, anyhow};
11use fidl_fuchsia_net_ext as fnet_ext;
12#[cfg(not(feature = "fdomain"))]
13use fidl_fuchsia_net_interfaces_ext as finterfaces_ext;
14#[cfg(feature = "fdomain")]
15use fidl_fuchsia_net_interfaces_ext_fdomain as finterfaces_ext;
16#[cfg(not(feature = "fdomain"))]
17use fidl_fuchsia_net_matchers_ext as fnet_matchers_ext;
18#[cfg(feature = "fdomain")]
19use fidl_fuchsia_net_matchers_ext_fdomain as fnet_matchers_ext;
20#[cfg(not(feature = "fdomain"))]
21use fidl_fuchsia_net_neighbor_ext as fneighbor_ext;
22#[cfg(feature = "fdomain")]
23use fidl_fuchsia_net_neighbor_ext_fdomain as fneighbor_ext;
24#[cfg(not(feature = "fdomain"))]
25use fidl_fuchsia_net_routes_ext as froutes_ext;
26#[cfg(feature = "fdomain")]
27use fidl_fuchsia_net_routes_ext_fdomain as froutes_ext;
28use fidl_fuchsia_net_stack_ext::{self as fstack_ext, FidlReturn as _};
29use flex_client::ProxyHasDomain;
30use flex_fuchsia_net as fnet;
31use flex_fuchsia_net_debug as fdebug;
32use flex_fuchsia_net_dhcp as fdhcp;
33use flex_fuchsia_net_filter as fnet_filter;
34use flex_fuchsia_net_interfaces as finterfaces;
35use flex_fuchsia_net_interfaces_admin as finterfaces_admin;
36use flex_fuchsia_net_name as fname;
37use flex_fuchsia_net_neighbor as fneighbor;
38use flex_fuchsia_net_resources as fnet_resources;
39use flex_fuchsia_net_root as froot;
40use flex_fuchsia_net_routes as froutes;
41use flex_fuchsia_net_stack as fstack;
42use futures::{FutureExt as _, StreamExt as _, TryFutureExt as _, TryStreamExt as _};
43use itertools::Itertools as _;
44use log::{info, warn};
45use net_types::SpecifiedAddr;
46use net_types::ip::{Ip, IpAddress as _, Ipv4, Ipv4Addr, Ipv6, Ipv6Addr, Subnet};
47use prettytable::{Row, Table, format, row};
48use ser::AddressAssignmentState;
49use serde_json::json;
50use serde_json::value::Value;
51use std::borrow::Cow;
52use std::collections::hash_map::HashMap;
53use std::convert::TryFrom as _;
54use std::iter::FromIterator as _;
55use std::num::NonZeroU64;
56use std::ops::Deref;
57use std::pin::pin;
58use std::str::FromStr as _;
59use writer::ToolIO as _;
60use zx_status as zx;
61
62pub use capture::DEFAULT_CAPTURE_NAME;
63pub use opts::{
64 Command, CommandEnum, UserFacingError, underlying_user_facing_error, user_facing_error,
65};
66
67fn add_row(t: &mut Table, row: Row) {
68 let _: &mut Row = t.add_row(row);
69}
70
71#[async_trait::async_trait]
73pub trait ServiceConnector<S: flex_client::fidl::ProtocolMarker> {
74 async fn connect(&self) -> Result<S::Proxy, Error>;
76}
77
78pub trait CaptureDeps {
79 type OutputWriter: std::io::Write + Send + 'static;
80 fn create_output_writer(
81 &self,
82 path: &std::path::Path,
83 ) -> Result<Self::OutputWriter, anyhow::Error>;
84}
85
86pub trait NetCliDepsConnector:
91 ServiceConnector<fdebug::InterfacesMarker>
92 + ServiceConnector<froot::InterfacesMarker>
93 + ServiceConnector<froot::RoutesV4Marker>
94 + ServiceConnector<froot::RoutesV6Marker>
95 + ServiceConnector<froot::FilterMarker>
96 + ServiceConnector<fdhcp::Server_Marker>
97 + ServiceConnector<finterfaces::StateMarker>
98 + ServiceConnector<finterfaces_admin::InstallerMarker>
99 + ServiceConnector<fneighbor::ControllerMarker>
100 + ServiceConnector<fneighbor::ViewMarker>
101 + ServiceConnector<fstack::LogMarker>
102 + ServiceConnector<fstack::StackMarker>
103 + ServiceConnector<froutes::StateV4Marker>
104 + ServiceConnector<froutes::StateV6Marker>
105 + ServiceConnector<fname::LookupMarker>
106 + ServiceConnector<fnet_filter::StateMarker>
107 + ServiceConnector<fdebug::PacketCaptureProviderMarker>
108{
109}
110
111impl<O> NetCliDepsConnector for O where
112 O: ServiceConnector<fdebug::InterfacesMarker>
113 + ServiceConnector<froot::InterfacesMarker>
114 + ServiceConnector<froot::RoutesV4Marker>
115 + ServiceConnector<froot::RoutesV6Marker>
116 + ServiceConnector<froot::FilterMarker>
117 + ServiceConnector<fdhcp::Server_Marker>
118 + ServiceConnector<finterfaces::StateMarker>
119 + ServiceConnector<finterfaces_admin::InstallerMarker>
120 + ServiceConnector<fneighbor::ControllerMarker>
121 + ServiceConnector<fneighbor::ViewMarker>
122 + ServiceConnector<fstack::LogMarker>
123 + ServiceConnector<fstack::StackMarker>
124 + ServiceConnector<froutes::StateV4Marker>
125 + ServiceConnector<froutes::StateV6Marker>
126 + ServiceConnector<fname::LookupMarker>
127 + ServiceConnector<fnet_filter::StateMarker>
128 + ServiceConnector<fdebug::PacketCaptureProviderMarker>
129{
130}
131
132pub async fn do_root<C, D>(
133 mut out: writer::JsonWriter<serde_json::Value>,
134 Command { cmd }: Command,
135 connector: &C,
136 deps: &D,
137) -> Result<(), Error>
138where
139 C: NetCliDepsConnector,
140 D: CaptureDeps,
141{
142 match cmd {
143 CommandEnum::Capture(cmd) => capture::do_capture(out, cmd, connector, deps)
144 .await
145 .context("failed during capture command"),
146 CommandEnum::If(opts::If { if_cmd: cmd }) => {
147 do_if(&mut out, cmd, connector).await.context("failed during if command")
148 }
149 CommandEnum::Route(opts::Route { route_cmd: cmd }) => {
150 do_route(&mut out, cmd, connector).await.context("failed during route command")
151 }
152 CommandEnum::Rule(opts::Rule { rule_cmd: cmd }) => {
153 do_rule(&mut out, cmd, connector).await.context("failed during rule command")
154 }
155 CommandEnum::Filter(opts::filter::Filter { filter_cmd: cmd }) => {
156 filter::do_filter(out, cmd, connector).await.context("failed during filter command")
157 }
158 CommandEnum::Log(opts::Log { log_cmd: cmd }) => {
159 do_log(cmd, connector).await.context("failed during log command")
160 }
161 CommandEnum::Dhcp(opts::Dhcp { dhcp_cmd: cmd }) => {
162 do_dhcp(cmd, connector).await.context("failed during dhcp command")
163 }
164 CommandEnum::Dhcpd(opts::dhcpd::Dhcpd { dhcpd_cmd: cmd }) => {
165 do_dhcpd(cmd, connector).await.context("failed during dhcpd command")
166 }
167 CommandEnum::Neigh(opts::Neigh { neigh_cmd: cmd }) => {
168 do_neigh(out, cmd, connector).await.context("failed during neigh command")
169 }
170 CommandEnum::Dns(opts::dns::Dns { dns_cmd: cmd }) => {
171 do_dns(out, cmd, connector).await.context("failed during dns command")
172 }
173 }
174}
175
176fn shortlist_interfaces(
177 name_pattern: &str,
178 interfaces: &mut HashMap<
179 u64,
180 finterfaces_ext::PropertiesAndState<(), finterfaces_ext::AllInterest>,
181 >,
182) {
183 interfaces.retain(|_: &u64, properties_and_state| {
184 properties_and_state.properties.name.contains(name_pattern)
185 })
186}
187
188fn write_tabulated_interfaces_info<
189 W: std::io::Write,
190 I: IntoIterator<Item = ser::InterfaceView>,
191>(
192 mut out: W,
193 interfaces: I,
194) -> Result<(), Error> {
195 let mut t = Table::new();
196 t.set_format(format::FormatBuilder::new().padding(2, 2).build());
197 for (
198 i,
199 ser::InterfaceView {
200 nicid,
201 name,
202 device_class,
203 online,
204 addresses,
205 mac,
206 has_default_ipv4_route,
207 has_default_ipv6_route,
208 port_identity_koid,
209 },
210 ) in interfaces.into_iter().enumerate()
211 {
212 if i > 0 {
213 add_row(&mut t, row![]);
214 }
215
216 add_row(&mut t, row!["nicid", nicid]);
217 add_row(&mut t, row!["name", name]);
218 add_row(
219 &mut t,
220 row![
221 "device class",
222 match device_class {
223 ser::DeviceClass::Loopback => "loopback",
224 ser::DeviceClass::Blackhole => "blackhole",
225 ser::DeviceClass::Virtual => "virtual",
226 ser::DeviceClass::Ethernet => "ethernet",
227 ser::DeviceClass::WlanClient => "wlan-client",
228 ser::DeviceClass::Ppp => "ppp",
229 ser::DeviceClass::Bridge => "bridge",
230 ser::DeviceClass::WlanAp => "wlan-ap",
231 ser::DeviceClass::Lowpan => "lowpan",
232 }
233 ],
234 );
235 add_row(&mut t, row!["online", online]);
236
237 let default_routes: std::borrow::Cow<'_, _> =
238 if has_default_ipv4_route || has_default_ipv6_route {
239 itertools::Itertools::intersperse(
240 has_default_ipv4_route
241 .then_some("IPv4")
242 .into_iter()
243 .chain(has_default_ipv6_route.then_some("IPv6")),
244 ",",
245 )
246 .collect::<String>()
247 .into()
248 } else {
249 "-".into()
250 };
251 add_row(&mut t, row!["default routes", default_routes]);
252
253 for ser::Address {
254 subnet: ser::Subnet { addr, prefix_len },
255 valid_until,
256 assignment_state,
257 } in addresses.all_addresses()
258 {
259 let valid_until = valid_until.map(|v| {
260 let v = std::time::Duration::from_nanos(v.try_into().unwrap_or(0)).as_secs_f32();
261 std::borrow::Cow::Owned(format!("valid until [{v}s]"))
262 });
263 let assignment_state: Option<std::borrow::Cow<'_, _>> = match assignment_state {
264 AddressAssignmentState::Assigned => None,
265 AddressAssignmentState::Tentative => Some("TENTATIVE".into()),
266 AddressAssignmentState::Unavailable => Some("UNAVAILABLE".into()),
267 };
268 let extra_bits = itertools::Itertools::intersperse(
269 assignment_state.into_iter().chain(valid_until),
270 " ".into(),
271 )
272 .collect::<String>();
273
274 add_row(&mut t, row!["addr", format!("{addr}/{prefix_len}"), extra_bits]);
275 }
276 match mac {
277 None => add_row(&mut t, row!["mac", "-"]),
278 Some(mac) => add_row(&mut t, row!["mac", mac]),
279 }
280 match port_identity_koid {
281 None => add_row(&mut t, row!["port_identity_koid", "-"]),
282 Some(port_identity_koid) => {
283 add_row(&mut t, row!["port_identity_koid", port_identity_koid])
284 }
285 }
286 }
287 writeln!(out, "{}", t)?;
288 Ok(())
289}
290
291pub(crate) async fn connect_with_context<S, C>(connector: &C) -> Result<S::Proxy, Error>
292where
293 C: ServiceConnector<S>,
294 S: flex_client::fidl::ProtocolMarker,
295{
296 connector.connect().await.with_context(|| format!("failed to connect to {}", S::DEBUG_NAME))
297}
298
299async fn get_control<C>(connector: &C, id: u64) -> Result<finterfaces_ext::admin::Control, Error>
300where
301 C: ServiceConnector<froot::InterfacesMarker>,
302{
303 let root_interfaces = connect_with_context::<froot::InterfacesMarker, _>(connector).await?;
304 let (proxy, server_end) =
305 root_interfaces.domain().create_proxy::<finterfaces_admin::ControlMarker>();
306 let control = finterfaces_ext::admin::Control::new(proxy);
307 root_interfaces.get_admin(id, server_end).context("send get admin request")?;
308 Ok(control)
309}
310
311fn configuration_with_ip_forwarding_set(
312 ip_version: fnet::IpVersion,
313 forwarding: bool,
314) -> finterfaces_admin::Configuration {
315 match ip_version {
316 fnet::IpVersion::V4 => finterfaces_admin::Configuration {
317 ipv4: Some(finterfaces_admin::Ipv4Configuration {
318 unicast_forwarding: Some(forwarding),
319 ..Default::default()
320 }),
321 ..Default::default()
322 },
323 fnet::IpVersion::V6 => finterfaces_admin::Configuration {
324 ipv6: Some(finterfaces_admin::Ipv6Configuration {
325 unicast_forwarding: Some(forwarding),
326 ..Default::default()
327 }),
328 ..Default::default()
329 },
330 }
331}
332
333fn extract_ip_forwarding(
334 finterfaces_admin::Configuration {
335 ipv4: ipv4_config, ipv6: ipv6_config, ..
336 }: finterfaces_admin::Configuration,
337 ip_version: fnet::IpVersion,
338) -> Result<bool, Error> {
339 match ip_version {
340 fnet::IpVersion::V4 => {
341 let finterfaces_admin::Ipv4Configuration { unicast_forwarding, .. } =
342 ipv4_config.context("get IPv4 configuration")?;
343 unicast_forwarding.context("get IPv4 forwarding configuration")
344 }
345 fnet::IpVersion::V6 => {
346 let finterfaces_admin::Ipv6Configuration { unicast_forwarding, .. } =
347 ipv6_config.context("get IPv6 configuration")?;
348 unicast_forwarding.context("get IPv6 forwarding configuration")
349 }
350 }
351}
352
353fn extract_igmp_version(
354 finterfaces_admin::Configuration { ipv4: ipv4_config, .. }: finterfaces_admin::Configuration,
355) -> Result<Option<finterfaces_admin::IgmpVersion>, Error> {
356 let finterfaces_admin::Ipv4Configuration { igmp, .. } =
357 ipv4_config.context("get IPv4 configuration")?;
358 let finterfaces_admin::IgmpConfiguration { version: igmp_version, .. } =
359 igmp.context("get IGMP configuration")?;
360 Ok(igmp_version)
361}
362
363fn extract_mld_version(
364 finterfaces_admin::Configuration { ipv6: ipv6_config, .. }: finterfaces_admin::Configuration,
365) -> Result<Option<finterfaces_admin::MldVersion>, Error> {
366 let finterfaces_admin::Ipv6Configuration { mld, .. } =
367 ipv6_config.context("get IPv6 configuration")?;
368 let finterfaces_admin::MldConfiguration { version: mld_version, .. } =
369 mld.context("get MLD configuration")?;
370 Ok(mld_version)
371}
372
373fn extract_nud_config(
374 finterfaces_admin::Configuration { ipv4, ipv6, .. }: finterfaces_admin::Configuration,
375 ip_version: fnet::IpVersion,
376) -> Result<finterfaces_admin::NudConfiguration, Error> {
377 match ip_version {
378 fnet::IpVersion::V4 => {
379 let finterfaces_admin::Ipv4Configuration { arp, .. } =
380 ipv4.context("get IPv4 configuration")?;
381 let finterfaces_admin::ArpConfiguration { nud, .. } =
382 arp.context("get ARP configuration")?;
383 nud.context("get NUD configuration")
384 }
385 fnet::IpVersion::V6 => {
386 let finterfaces_admin::Ipv6Configuration { ndp, .. } =
387 ipv6.context("get IPv6 configuration")?;
388 let finterfaces_admin::NdpConfiguration { nud, .. } =
389 ndp.context("get NDP configuration")?;
390 nud.context("get NUD configuration")
391 }
392 }
393}
394
395async fn do_if<C: NetCliDepsConnector>(
396 out: &mut writer::JsonWriter<serde_json::Value>,
397 cmd: opts::IfEnum,
398 connector: &C,
399) -> Result<(), Error> {
400 match cmd {
401 opts::IfEnum::List(opts::IfList { name_pattern }) => {
402 let root_interfaces =
403 connect_with_context::<froot::InterfacesMarker, _>(connector).await?;
404 let interface_state =
405 connect_with_context::<finterfaces::StateMarker, _>(connector).await?;
406 let stream = finterfaces_ext::event_stream_from_state::<finterfaces_ext::AllInterest>(
407 &interface_state,
408 Default::default(),
409 )?;
410 let mut response = finterfaces_ext::existing(
411 stream,
412 HashMap::<u64, finterfaces_ext::PropertiesAndState<(), _>>::new(),
413 )
414 .await?;
415 if let Some(name_pattern) = name_pattern {
416 shortlist_interfaces(&name_pattern, &mut response);
417 }
418 let response = response.into_values().map(
419 |finterfaces_ext::PropertiesAndState { properties, state: () }| async {
420 let mac = root_interfaces
421 .get_mac(properties.id.get())
422 .await
423 .context("call get_mac")?;
424 Ok::<_, Error>((properties, mac))
425 },
426 );
427 let response = futures::future::try_join_all(response).await?;
428 let mut response: Vec<_> = response
429 .into_iter()
430 .filter_map(|(properties, mac)| match mac {
431 Err(froot::InterfacesGetMacError::NotFound) => None,
432 Ok(mac) => {
433 let mac = mac.map(|box_| *box_);
434 Some((properties, mac).into())
435 }
436 })
437 .collect();
438 response.sort_by_key(|ser::InterfaceView { nicid, .. }| *nicid);
439 if out.is_machine() {
440 out.machine(&serde_json::to_value(&response)?)?;
441 } else {
442 write_tabulated_interfaces_info(out, response.into_iter())
443 .context("error tabulating interface info")?;
444 }
445 }
446 opts::IfEnum::Get(opts::IfGet { interface }) => {
447 let id = interface.find_nicid(connector).await?.get();
448 let root_interfaces =
449 connect_with_context::<froot::InterfacesMarker, _>(connector).await?;
450 let interface_state =
451 connect_with_context::<finterfaces::StateMarker, _>(connector).await?;
452 let stream = finterfaces_ext::event_stream_from_state::<finterfaces_ext::AllInterest>(
453 &interface_state,
454 Default::default(),
455 )?;
456 let response = finterfaces_ext::existing(
457 stream,
458 finterfaces_ext::InterfaceState::<(), _>::Unknown(id),
459 )
460 .await?;
461 match response {
462 finterfaces_ext::InterfaceState::Unknown(id) => {
463 return Err(user_facing_error(format!("interface with id={} not found", id)));
464 }
465 finterfaces_ext::InterfaceState::Known(finterfaces_ext::PropertiesAndState {
466 properties,
467 state: _,
468 }) => {
469 let finterfaces_ext::Properties { id, .. } = &properties;
470 let mac = root_interfaces.get_mac(id.get()).await.context("call get_mac")?;
471 match mac {
472 Err(froot::InterfacesGetMacError::NotFound) => {
473 return Err(user_facing_error(format!(
474 "interface with id={} not found",
475 id
476 )));
477 }
478 Ok(mac) => {
479 let mac = mac.map(|box_| *box_);
480 let view = (properties, mac).into();
481 if out.is_machine() {
482 out.machine(&serde_json::to_value(&view)?)?;
483 } else {
484 write_tabulated_interfaces_info(out, std::iter::once(view))
485 .context("error tabulating interface info")?;
486 }
487 }
488 };
489 }
490 }
491 }
492 opts::IfEnum::Igmp(opts::IfIgmp { cmd }) => match cmd {
493 opts::IfIgmpEnum::Get(opts::IfIgmpGet { interface }) => {
494 let id = interface.find_nicid(connector).await.context("find nicid")?.get();
495 let control = get_control(connector, id).await.context("get control")?;
496 let configuration = control
497 .get_configuration()
498 .await
499 .map_err(anyhow::Error::new)
500 .and_then(|res| {
501 res.map_err(|e: finterfaces_admin::ControlGetConfigurationError| {
502 anyhow!("{:?}", e)
503 })
504 })
505 .context("get configuration")?;
506
507 out.line(format!("IGMP configuration on interface {}:", id))?;
508 out.line(format!(
509 " Version: {:?}",
510 extract_igmp_version(configuration).context("get IGMP version")?
511 ))?;
512 }
513 opts::IfIgmpEnum::Set(opts::IfIgmpSet { interface, version }) => {
514 let id = interface.find_nicid(connector).await.context("find nicid")?.get();
515 let control = get_control(connector, id).await.context("get control")?;
516 let prev_config = control
517 .set_configuration(&finterfaces_admin::Configuration {
518 ipv4: Some(finterfaces_admin::Ipv4Configuration {
519 igmp: Some(finterfaces_admin::IgmpConfiguration {
520 version,
521 ..Default::default()
522 }),
523 ..Default::default()
524 }),
525 ..Default::default()
526 })
527 .await
528 .map_err(anyhow::Error::new)
529 .and_then(|res| {
530 res.map_err(|e: finterfaces_admin::ControlSetConfigurationError| {
531 anyhow!("{:?}", e)
532 })
533 })
534 .context("set configuration")?;
535
536 info!(
537 "IGMP version set to {:?} on interface {}; previously set to {:?}",
538 version,
539 id,
540 extract_igmp_version(prev_config).context("set IGMP version")?,
541 );
542 }
543 },
544 opts::IfEnum::Mld(opts::IfMld { cmd }) => match cmd {
545 opts::IfMldEnum::Get(opts::IfMldGet { interface }) => {
546 let id = interface.find_nicid(connector).await.context("find nicid")?.get();
547 let control = get_control(connector, id).await.context("get control")?;
548 let configuration = control
549 .get_configuration()
550 .await
551 .map_err(anyhow::Error::new)
552 .and_then(|res| {
553 res.map_err(|e: finterfaces_admin::ControlGetConfigurationError| {
554 anyhow!("{:?}", e)
555 })
556 })
557 .context("get configuration")?;
558
559 out.line(format!("MLD configuration on interface {}:", id))?;
560 out.line(format!(
561 " Version: {:?}",
562 extract_mld_version(configuration).context("get MLD version")?
563 ))?;
564 }
565 opts::IfMldEnum::Set(opts::IfMldSet { interface, version }) => {
566 let id = interface.find_nicid(connector).await.context("find nicid")?.get();
567 let control = get_control(connector, id).await.context("get control")?;
568 let prev_config = control
569 .set_configuration(&finterfaces_admin::Configuration {
570 ipv6: Some(finterfaces_admin::Ipv6Configuration {
571 mld: Some(finterfaces_admin::MldConfiguration {
572 version,
573 ..Default::default()
574 }),
575 ..Default::default()
576 }),
577 ..Default::default()
578 })
579 .await
580 .map_err(anyhow::Error::new)
581 .and_then(|res| {
582 res.map_err(|e: finterfaces_admin::ControlSetConfigurationError| {
583 anyhow!("{:?}", e)
584 })
585 })
586 .context("set configuration")?;
587
588 info!(
589 "MLD version set to {:?} on interface {}; previously set to {:?}",
590 version,
591 id,
592 extract_mld_version(prev_config).context("set MLD version")?,
593 );
594 }
595 },
596 opts::IfEnum::IpForward(opts::IfIpForward { cmd }) => match cmd {
597 opts::IfIpForwardEnum::Get(opts::IfIpForwardGet { interface, ip_version }) => {
598 let id = interface.find_nicid(connector).await.context("find nicid")?.get();
599 let control = get_control(connector, id).await.context("get control")?;
600 let configuration = control
601 .get_configuration()
602 .await
603 .map_err(anyhow::Error::new)
604 .and_then(|res| {
605 res.map_err(|e: finterfaces_admin::ControlGetConfigurationError| {
606 anyhow!("{:?}", e)
607 })
608 })
609 .context("get configuration")?;
610
611 out.line(format!(
612 "IP forwarding for {:?} is {} on interface {}",
613 ip_version,
614 extract_ip_forwarding(configuration, ip_version)
615 .context("extract IP forwarding configuration")?,
616 id
617 ))?;
618 }
619 opts::IfIpForwardEnum::Set(opts::IfIpForwardSet { interface, ip_version, enable }) => {
620 let id = interface.find_nicid(connector).await.context("find nicid")?.get();
621 let control = get_control(connector, id).await.context("get control")?;
622 let prev_config = control
623 .set_configuration(&configuration_with_ip_forwarding_set(ip_version, enable))
624 .await
625 .map_err(anyhow::Error::new)
626 .and_then(|res| {
627 res.map_err(|e: finterfaces_admin::ControlSetConfigurationError| {
628 anyhow!("{:?}", e)
629 })
630 })
631 .context("set configuration")?;
632 info!(
633 "IP forwarding for {:?} set to {} on interface {}; previously set to {}",
634 ip_version,
635 enable,
636 id,
637 extract_ip_forwarding(prev_config, ip_version)
638 .context("set IP forwarding configuration")?
639 );
640 }
641 },
642 opts::IfEnum::Enable(opts::IfEnable { interface }) => {
643 let id = interface.find_nicid(connector).await?.get();
644 let control = get_control(connector, id).await?;
645 let did_enable = control
646 .enable()
647 .await
648 .map_err(anyhow::Error::new)
649 .and_then(|res| {
650 res.map_err(|e: finterfaces_admin::ControlEnableError| anyhow!("{:?}", e))
651 })
652 .context("error enabling interface")?;
653 if did_enable {
654 info!("Interface {} enabled", id);
655 } else {
656 info!("Interface {} already enabled", id);
657 }
658 }
659 opts::IfEnum::Disable(opts::IfDisable { interface }) => {
660 let id = interface.find_nicid(connector).await?.get();
661 let control = get_control(connector, id).await?;
662 let did_disable = control
663 .disable()
664 .await
665 .map_err(anyhow::Error::new)
666 .and_then(|res| {
667 res.map_err(|e: finterfaces_admin::ControlDisableError| anyhow!("{:?}", e))
668 })
669 .context("error disabling interface")?;
670 if did_disable {
671 info!("Interface {} disabled", id);
672 } else {
673 info!("Interface {} already disabled", id);
674 }
675 }
676 opts::IfEnum::Addr(opts::IfAddr { addr_cmd }) => match addr_cmd {
677 opts::IfAddrEnum::Add(opts::IfAddrAdd { interface, addr, prefix, no_subnet_route }) => {
678 let id = interface.find_nicid(connector).await?.get();
679 let control = get_control(connector, id).await?;
680 let addr = fnet_ext::IpAddress::from_str(&addr)?.into();
681 let subnet = fnet_ext::Subnet { addr, prefix_len: prefix };
682 let root_interfaces =
683 connect_with_context::<froot::InterfacesMarker, _>(connector).await?;
684 let (address_state_provider, server_end) =
685 root_interfaces
686 .domain()
687 .create_proxy::<finterfaces_admin::AddressStateProviderMarker>();
688 control
689 .add_address(
690 &subnet.into(),
691 &finterfaces_admin::AddressParameters {
692 add_subnet_route: Some(!no_subnet_route),
693 ..Default::default()
694 },
695 server_end,
696 )
697 .context("call add address")?;
698
699 address_state_provider.detach().context("detach address lifetime")?;
700 let state_stream =
701 finterfaces_ext::admin::assignment_state_stream(address_state_provider);
702
703 state_stream
704 .try_filter_map(|state| {
705 futures::future::ok(match state {
706 finterfaces::AddressAssignmentState::Tentative => None,
707 finterfaces::AddressAssignmentState::Assigned => Some(()),
708 finterfaces::AddressAssignmentState::Unavailable => Some(()),
709 })
710 })
711 .try_next()
712 .await
713 .context("error after adding address")?
714 .ok_or_else(|| {
715 anyhow!(
716 "Address assignment state stream unexpectedly ended \
717 before reaching Assigned or Unavailable state. \
718 This is probably a bug."
719 )
720 })?;
721
722 info!("Address {}/{} added to interface {}", addr, prefix, id);
723 }
724 opts::IfAddrEnum::Del(opts::IfAddrDel { interface, addr, prefix }) => {
725 let id = interface.find_nicid(connector).await?.get();
726 let control = get_control(connector, id).await?;
727 let addr = fnet_ext::IpAddress::from_str(&addr)?;
728 let did_remove = {
729 let addr = addr.into();
730 let subnet = fnet::Subnet {
731 addr,
732 prefix_len: prefix.unwrap_or_else(|| {
733 8 * u8::try_from(match addr {
734 fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr }) => addr.len(),
735 fnet::IpAddress::Ipv6(fnet::Ipv6Address { addr }) => addr.len(),
736 })
737 .expect("prefix length doesn't fit u8")
738 }),
739 };
740 control
741 .remove_address(&subnet)
742 .await
743 .map_err(anyhow::Error::new)
744 .and_then(|res| {
745 res.map_err(|e: finterfaces_admin::ControlRemoveAddressError| {
746 anyhow!("{:?}", e)
747 })
748 })
749 .context("call remove address")?
750 };
751 if !did_remove {
752 return Err(user_facing_error(format!(
753 "Address {} not found on interface {}",
754 addr, id
755 )));
756 }
757 info!("Address {} deleted from interface {}", addr, id);
758 }
759 opts::IfAddrEnum::Wait(opts::IfAddrWait { interface, ipv6 }) => {
760 let id = interface.find_nicid(connector).await?.get();
761 let interfaces_state =
762 connect_with_context::<finterfaces::StateMarker, _>(connector).await?;
763 let mut state = finterfaces_ext::InterfaceState::<(), _>::Unknown(id);
764
765 let assigned_addr = finterfaces_ext::wait_interface_with_id(
766 finterfaces_ext::event_stream_from_state::<finterfaces_ext::AllInterest>(
767 &interfaces_state,
768 Default::default(),
769 )?,
770 &mut state,
771 |finterfaces_ext::PropertiesAndState { properties, state: _ }| {
772 let finterfaces_ext::Properties { addresses, .. } = properties;
773 let addr = if ipv6 {
774 addresses.iter().find_map(
775 |finterfaces_ext::Address {
776 addr: fnet::Subnet { addr, .. },
777 ..
778 }| {
779 match addr {
780 fnet::IpAddress::Ipv4(_) => None,
781 fnet::IpAddress::Ipv6(_) => Some(addr),
782 }
783 },
784 )
785 } else {
786 addresses.first().map(
787 |finterfaces_ext::Address {
788 addr: fnet::Subnet { addr, .. },
789 ..
790 }| addr,
791 )
792 };
793 addr.map(|addr| {
794 let fnet_ext::IpAddress(addr) = (*addr).into();
795 addr
796 })
797 },
798 )
799 .await
800 .context("wait for assigned address")?;
801
802 out.line(format!("{assigned_addr}"))?;
803 info!("Address {} assigned on interface {}", assigned_addr, id);
804 }
805 },
806 opts::IfEnum::Config(opts::IfConfig { interface, cmd }) => {
807 let id = interface.find_nicid(connector).await.context("find nicid")?.get();
808 let control = get_control(connector, id).await.context("get control")?;
809
810 match cmd {
811 opts::IfConfigEnum::Set(opts::IfConfigSet { options }) => {
812 do_if_config_set(control, options).await?;
813 }
814 opts::IfConfigEnum::Get(opts::IfConfigGet {}) => {
815 let configuration = control
816 .get_configuration()
817 .await
818 .map_err(anyhow::Error::new)
819 .and_then(|res| {
820 res.map_err(|e: finterfaces_admin::ControlGetConfigurationError| {
821 anyhow::anyhow!("{:?}", e)
822 })
823 })
824 .context("get configuration")?;
825 out.line(format!("{:#?}", configuration))?;
828 }
829 }
830 }
831 opts::IfEnum::Add(opts::IfAdd {
832 cmd: opts::IfAddEnum::Blackhole(opts::IfBlackholeAdd { name }),
833 }) => {
834 let installer =
835 ServiceConnector::<finterfaces_admin::InstallerMarker>::connect(connector)
836 .await
837 .expect("connect should succeed");
838
839 let (proxy, server_end) =
840 installer.domain().create_proxy::<finterfaces_admin::ControlMarker>();
841 let control = finterfaces_ext::admin::Control::new(proxy);
842 installer
843 .install_blackhole_interface(
844 server_end,
845 finterfaces_admin::Options { name: Some(name), ..Default::default() },
846 )
847 .expect("install blackhole interface should succeed");
848 control.detach().expect("detach should succeed");
849 }
850 opts::IfEnum::Remove(opts::IfRemove { interface }) => {
851 let id = interface.find_nicid(connector).await.context("find nicid")?.get();
852 let control = get_control(connector, id).await.context("get control")?;
853 control
854 .remove()
855 .await
856 .expect("should not get FIDL error")
857 .expect("remove should succeed");
858 }
859 }
860 Ok(())
861}
862
863async fn do_if_config_set(
864 control: finterfaces_ext::admin::Control,
865 options: Vec<String>,
866) -> Result<(), Error> {
867 if options.len() % 2 != 0 {
868 return Err(user_facing_error(format!(
869 "if config set expects property value pairs and thus an even number of arguments"
870 )));
871 }
872 let config = options.iter().tuples().try_fold(
873 finterfaces_admin::Configuration::default(),
874 |mut config, (property, value)| {
875 match property.as_str() {
876 "ipv6.ndp.slaac.temporary_address_enabled" => {
877 let enabled = value.parse::<bool>().map_err(|e| {
878 user_facing_error(format!("failed to parse {value} as bool: {e}"))
879 })?;
880 config
881 .ipv6
882 .get_or_insert_default()
883 .ndp
884 .get_or_insert_default()
885 .slaac
886 .get_or_insert_default()
887 .temporary_address = Some(enabled);
888 }
889 "ipv6.ndp.dad.transmits" => {
890 let transmits = value.parse::<u16>().map_err(|e| {
891 user_facing_error(format!("failed to parse {value} as u16: {e}"))
892 })?;
893 config
894 .ipv6
895 .get_or_insert_default()
896 .ndp
897 .get_or_insert_default()
898 .dad
899 .get_or_insert_default()
900 .transmits = Some(transmits);
901 }
902 "ipv6.ndp.router_solicitations.max" => {
903 let max_router_solicitations = value.parse::<u8>().map_err(|e| {
904 user_facing_error(format!("failed to parse {value} as u8: {e}"))
905 })?;
906 config
907 .ipv6
908 .get_or_insert_default()
909 .ndp
910 .get_or_insert_default()
911 .router_solicitations
912 .get_or_insert_default()
913 .max = Some(max_router_solicitations);
914 }
915 unknown_property => {
916 return Err(user_facing_error(format!(
917 "unknown configuration parameter: {unknown_property}"
918 )));
919 }
920 }
921 Ok(config)
922 },
923 )?;
924
925 let _: finterfaces_admin::Configuration = control
928 .set_configuration(&config)
929 .await
930 .map_err(anyhow::Error::new)
931 .and_then(|res| {
932 res.map_err(|e: finterfaces_admin::ControlSetConfigurationError| {
933 anyhow::anyhow!("{:?}", e)
934 })
935 })
936 .context("set configuration")?;
937
938 Ok(())
939}
940
941enum IpSubnetAndGateway {
942 V4(Subnet<Ipv4Addr>, Option<Ipv4Addr>),
943 V6(Subnet<Ipv6Addr>, Option<Ipv6Addr>),
944}
945
946fn try_into_matched_subnet_and_gateway(
947 destination: std::net::IpAddr,
948 prefix_len: u8,
949 gateway: Option<std::net::IpAddr>,
950) -> Result<IpSubnetAndGateway, Error> {
951 match (destination, gateway) {
952 (std::net::IpAddr::V4(addr), Some(std::net::IpAddr::V4(g))) => Ok(IpSubnetAndGateway::V4(
953 Subnet::new(Ipv4Addr::from(addr).mask(prefix_len), prefix_len)
954 .map_err(|e| anyhow!("invalid subnet: {:?}", e))?,
955 Some(g.into()),
956 )),
957 (std::net::IpAddr::V4(addr), None) => Ok(IpSubnetAndGateway::V4(
958 Subnet::new(Ipv4Addr::from(addr).mask(prefix_len), prefix_len)
959 .map_err(|e| anyhow!("invalid subnet: {:?}", e))?,
960 None,
961 )),
962 (std::net::IpAddr::V6(addr), Some(std::net::IpAddr::V6(g))) => Ok(IpSubnetAndGateway::V6(
963 Subnet::new(Ipv6Addr::from(addr).mask(prefix_len), prefix_len)
964 .map_err(|e| anyhow!("invalid subnet: {:?}", e))?,
965 Some(g.into()),
966 )),
967 (std::net::IpAddr::V6(addr), None) => Ok(IpSubnetAndGateway::V6(
968 Subnet::new(Ipv6Addr::from(addr).mask(prefix_len), prefix_len)
969 .map_err(|e| anyhow!("invalid subnet: {:?}", e))?,
970 None,
971 )),
972 (std::net::IpAddr::V4(_), Some(std::net::IpAddr::V6(_)))
973 | (std::net::IpAddr::V6(_), Some(std::net::IpAddr::V4(_))) => {
974 Err(anyhow!("Gateway IP version mismatch"))
975 }
976 }
977}
978
979async fn get_interface_authorization_proof<C: NetCliDepsConnector>(
980 interface: &opts::InterfaceIdentifier,
981 connector: &C,
982) -> Result<(NonZeroU64, fnet_resources::ProofOfInterfaceAuthorization), Error> {
983 let nicid = interface.find_nicid(connector).await?;
984
985 let control = get_control(connector, nicid.get()).await?;
986
987 let grant = control
988 .get_authorization_for_interface()
989 .await
990 .context("failed to get authorization for interface")?;
991 let proof = fnet_resources::ProofOfInterfaceAuthorization {
992 interface_id: grant.interface_id,
993 token: grant.token,
994 };
995
996 Ok((nicid, proof))
997}
998
999async fn do_route<C: NetCliDepsConnector>(
1000 out: &mut writer::JsonWriter<serde_json::Value>,
1001 cmd: opts::RouteEnum,
1002 connector: &C,
1003) -> Result<(), Error> {
1004 match cmd {
1005 opts::RouteEnum::List(opts::RouteList {}) => do_route_list(out, connector).await?,
1006 opts::RouteEnum::Add(route) => do_route_add(route, connector).await?,
1007 opts::RouteEnum::Del(route) => do_route_del(route, connector).await?,
1008 }
1009 Ok(())
1010}
1011
1012async fn do_route_add<C: NetCliDepsConnector>(
1013 route: opts::RouteAdd,
1014 connector: &C,
1015) -> Result<(), Error> {
1016 let interface = route.interface;
1017 let (nicid, proof) = get_interface_authorization_proof(&interface, connector).await?;
1018
1019 match try_into_matched_subnet_and_gateway(route.destination, route.prefix_len, route.gateway)? {
1020 IpSubnetAndGateway::V4(dest, g) => {
1021 let gateway = match g {
1022 Some(g) => Some(
1023 SpecifiedAddr::new(g.into())
1024 .ok_or_else(|| anyhow!("gateway must be a specified address"))?,
1025 ),
1026 None => None,
1027 };
1028 do_route_add_inner::<Ipv4, C>(dest, gateway, nicid, route.metric, proof, connector)
1029 .await?;
1030 }
1031 IpSubnetAndGateway::V6(dest, g) => {
1032 let gateway = match g {
1033 Some(g) => Some(
1034 SpecifiedAddr::new(g.into())
1035 .ok_or_else(|| anyhow!("gateway must be a specified address"))?,
1036 ),
1037 None => None,
1038 };
1039 do_route_add_inner::<Ipv6, C>(dest, gateway, nicid, route.metric, proof, connector)
1040 .await?;
1041 }
1042 }
1043 Ok(())
1044}
1045
1046fn new_route<I: Ip>(
1047 destination: net_types::ip::Subnet<I::Addr>,
1048 nicid: u64,
1049 gateway: Option<SpecifiedAddr<I::Addr>>,
1050 metric: Option<u32>,
1051) -> froutes_ext::Route<I> {
1052 match metric {
1053 Some(metric) => froutes_ext::Route::<I>::new_forward_with_explicit_metric(
1054 destination,
1055 nicid,
1056 gateway,
1057 metric,
1058 ),
1059 None => {
1060 froutes_ext::Route::<I>::new_forward_with_inherited_metric(destination, nicid, gateway)
1061 }
1062 }
1063}
1064
1065async fn do_route_add_inner<I, C>(
1066 destination: net_types::ip::Subnet<I::Addr>,
1067 gateway: Option<SpecifiedAddr<I::Addr>>,
1068 nicid: NonZeroU64,
1069 metric: Option<u32>,
1070 proof: fnet_resources::ProofOfInterfaceAuthorization,
1071 connector: &C,
1072) -> Result<(), Error>
1073where
1074 I: Ip + froutes_ext::admin::FidlRouteAdminIpExt + froutes_ext::FidlRouteIpExt,
1075 C: NetCliDepsConnector + ServiceConnector<I::GlobalRouteTableMarker>,
1076{
1077 let routes_proxy = connect_with_context::<I::GlobalRouteTableMarker, _>(connector).await?;
1078 let route_set_proxy = froutes_ext::admin::new_global_route_set::<I>(&routes_proxy)
1079 .context("failed to create global route set")?;
1080
1081 froutes_ext::admin::authenticate_for_interface::<I>(&route_set_proxy, proof)
1082 .await
1083 .context("authenticate_for_interface failed")?
1084 .map_err(|e| anyhow!("authenticate failed: {:?}", e))?;
1085
1086 let route = new_route::<I>(destination, nicid.get(), gateway, metric);
1087 let fidl_route =
1088 route.try_into().map_err(|e| anyhow!("failed to convert route to FIDL: {:?}", e))?;
1089
1090 let did_add = froutes_ext::admin::add_route::<I>(&route_set_proxy, &fidl_route)
1091 .await
1092 .context("add_route failed")?
1093 .map_err(|e| anyhow!("add_route failed: {:?}", e))?;
1094 if !did_add {
1095 warn!("Route already exists, did not add.");
1096 }
1097 Ok(())
1098}
1099
1100async fn do_route_del<C: NetCliDepsConnector>(
1101 route: opts::RouteDel,
1102 connector: &C,
1103) -> Result<(), Error> {
1104 let interface = route.interface;
1105 let (nicid, proof) = get_interface_authorization_proof(&interface, connector).await?;
1106
1107 match try_into_matched_subnet_and_gateway(route.destination, route.prefix_len, route.gateway)? {
1108 IpSubnetAndGateway::V4(dest, g) => {
1109 let gateway = match g {
1110 Some(g) => Some(
1111 SpecifiedAddr::new(g.into())
1112 .ok_or_else(|| anyhow!("gateway must be a specified address"))?,
1113 ),
1114 None => None,
1115 };
1116 do_route_del_inner::<Ipv4, C>(dest, gateway, nicid, route.metric, proof, connector)
1117 .await?;
1118 }
1119 IpSubnetAndGateway::V6(dest, g) => {
1120 let gateway = match g {
1121 Some(g) => Some(
1122 SpecifiedAddr::new(g.into())
1123 .ok_or_else(|| anyhow!("gateway must be a specified address"))?,
1124 ),
1125 None => None,
1126 };
1127 do_route_del_inner::<Ipv6, C>(dest, gateway, nicid, route.metric, proof, connector)
1128 .await?;
1129 }
1130 }
1131 Ok(())
1132}
1133
1134async fn do_route_del_inner<I, C>(
1135 destination: net_types::ip::Subnet<I::Addr>,
1136 gateway: Option<SpecifiedAddr<I::Addr>>,
1137 nicid: NonZeroU64,
1138 metric: Option<u32>,
1139 proof: fnet_resources::ProofOfInterfaceAuthorization,
1140 connector: &C,
1141) -> Result<(), Error>
1142where
1143 I: Ip + froutes_ext::admin::FidlRouteAdminIpExt + froutes_ext::FidlRouteIpExt,
1144 C: NetCliDepsConnector + ServiceConnector<I::GlobalRouteTableMarker>,
1145{
1146 let routes_proxy = connect_with_context::<I::GlobalRouteTableMarker, _>(connector).await?;
1147 let route_set_proxy = froutes_ext::admin::new_global_route_set::<I>(&routes_proxy)
1148 .context("failed to create global route set")?;
1149
1150 froutes_ext::admin::authenticate_for_interface::<I>(&route_set_proxy, proof)
1151 .await
1152 .context("authenticate_for_interface failed")?
1153 .map_err(|e| anyhow!("authenticate failed: {:?}", e))?;
1154
1155 let route = new_route::<I>(destination, nicid.get(), gateway, metric);
1156 let fidl_route =
1157 route.try_into().map_err(|e| anyhow!("failed to convert route to FIDL: {:?}", e))?;
1158
1159 let did_remove = froutes_ext::admin::remove_route::<I>(&route_set_proxy, &fidl_route)
1160 .await
1161 .context("remove_route failed")?
1162 .map_err(|e| anyhow!("remove_route failed: {:?}", e))?;
1163 if !did_remove {
1164 warn!("Route was not found, did not remove.");
1165 }
1166 Ok(())
1167}
1168
1169async fn do_route_list<C: NetCliDepsConnector>(
1170 out: &mut writer::JsonWriter<serde_json::Value>,
1171 connector: &C,
1172) -> Result<(), Error> {
1173 let ipv4_route_event_stream = pin!({
1174 let state_v4 = connect_with_context::<froutes::StateV4Marker, _>(connector)
1175 .await
1176 .context("failed to connect to fuchsia.net.routes/StateV4")?;
1177 froutes_ext::event_stream_from_state::<Ipv4>(&state_v4)
1178 .context("failed to initialize a `WatcherV4` client")?
1179 .fuse()
1180 });
1181 let ipv6_route_event_stream = pin!({
1182 let state_v6 = connect_with_context::<froutes::StateV6Marker, _>(connector)
1183 .await
1184 .context("failed to connect to fuchsia.net.routes/StateV6")?;
1185 froutes_ext::event_stream_from_state::<Ipv6>(&state_v6)
1186 .context("failed to initialize a `WatcherV6` client")?
1187 .fuse()
1188 });
1189 let (v4_routes, v6_routes) = futures::future::join(
1190 froutes_ext::collect_routes_until_idle::<_, Vec<_>>(ipv4_route_event_stream),
1191 froutes_ext::collect_routes_until_idle::<_, Vec<_>>(ipv6_route_event_stream),
1192 )
1193 .await;
1194 let mut v4_routes = v4_routes.context("failed to collect all existing IPv4 routes")?;
1195 let mut v6_routes = v6_routes.context("failed to collect all existing IPv6 routes")?;
1196
1197 fn group_by_table_id_and_sort<I: net_types::ip::Ip>(
1198 routes: &mut Vec<froutes_ext::InstalledRoute<I>>,
1199 ) {
1200 routes.sort_unstable_by_key(|r| r.table_id);
1201 for chunk in routes.chunk_by_mut(|a, b| a.table_id == b.table_id) {
1202 chunk.sort();
1203 }
1204 }
1205 group_by_table_id_and_sort(&mut v4_routes);
1206 group_by_table_id_and_sort(&mut v6_routes);
1207
1208 if out.is_machine() {
1209 fn to_ser<I: net_types::ip::Ip>(
1210 route: froutes_ext::InstalledRoute<I>,
1211 ) -> Option<ser::ForwardingEntry> {
1212 route.try_into().map_err(|e| warn!("failed to convert route: {:?}", e)).ok()
1213 }
1214 let routes = v4_routes
1215 .into_iter()
1216 .filter_map(to_ser)
1217 .chain(v6_routes.into_iter().filter_map(to_ser))
1218 .collect::<Vec<_>>();
1219 out.machine(&serde_json::to_value(routes)?).context("serialize")?;
1220 } else {
1221 let mut t = Table::new();
1222 t.set_format(format::FormatBuilder::new().padding(2, 2).build());
1223
1224 t.set_titles(row!["Destination", "Gateway", "NICID", "Metric", "TableId"]);
1226 fn write_route<I: net_types::ip::Ip>(t: &mut Table, route: froutes_ext::InstalledRoute<I>) {
1227 let froutes_ext::InstalledRoute {
1228 route: froutes_ext::Route { destination, action, properties: _ },
1229 effective_properties: froutes_ext::EffectiveRouteProperties { metric },
1230 table_id,
1231 } = route;
1232 let (device_id, next_hop) = match action {
1233 froutes_ext::RouteAction::Forward(froutes_ext::RouteTarget {
1234 outbound_interface,
1235 next_hop,
1236 }) => (outbound_interface, next_hop),
1237 froutes_ext::RouteAction::Unknown => {
1238 warn!("observed route with unknown RouteAction.");
1239 return;
1240 }
1241 };
1242 let next_hop = next_hop.map(|next_hop| next_hop.to_string());
1243 let next_hop = next_hop.as_ref().map_or("-", |s| s.as_str());
1244 add_row(t, row![destination, next_hop, device_id, metric, table_id]);
1245 }
1246
1247 for route in v4_routes {
1248 write_route(&mut t, route);
1249 }
1250 for route in v6_routes {
1251 write_route(&mut t, route);
1252 }
1253
1254 let _lines_printed: usize = t.print(out)?;
1255 out.line("")?;
1256 }
1257 Ok(())
1258}
1259
1260async fn do_rule<C: NetCliDepsConnector>(
1261 out: &mut writer::JsonWriter<serde_json::Value>,
1262 cmd: opts::RuleEnum,
1263 connector: &C,
1264) -> Result<(), Error> {
1265 match cmd {
1266 opts::RuleEnum::List(opts::RuleList {}) => do_rule_list(out, connector).await,
1267 }
1268}
1269
1270async fn do_rule_list<C: NetCliDepsConnector>(
1271 out: &mut writer::JsonWriter<serde_json::Value>,
1272 connector: &C,
1273) -> Result<(), Error> {
1274 let ipv4_rule_event_stream = pin!({
1275 let state_v4 = connect_with_context::<froutes::StateV4Marker, _>(connector)
1276 .await
1277 .context("failed to connect to fuchsia.net.routes/StateV4")?;
1278 froutes_ext::rules::rule_event_stream_from_state::<Ipv4>(&state_v4)
1279 .context("failed to initialize a `RuleWatcherV4` client")?
1280 .fuse()
1281 });
1282 let ipv6_rule_event_stream = pin!({
1283 let state_v6 = connect_with_context::<froutes::StateV6Marker, _>(connector)
1284 .await
1285 .context("failed to connect to fuchsia.net.routes/StateV6")?;
1286 froutes_ext::rules::rule_event_stream_from_state::<Ipv6>(&state_v6)
1287 .context("failed to initialize a `RuleWatcherV6` client")?
1288 .fuse()
1289 });
1290 let (v4_rules, v6_rules) = futures::future::join(
1291 froutes_ext::rules::collect_rules_until_idle::<Ipv4, Vec<_>>(ipv4_rule_event_stream),
1292 froutes_ext::rules::collect_rules_until_idle::<Ipv6, Vec<_>>(ipv6_rule_event_stream),
1293 )
1294 .await;
1295 let mut v4_rules = v4_rules.context("failed to collect all existing IPv4 rules")?;
1296 let mut v6_rules = v6_rules.context("failed to collect all existing IPv6 rules")?;
1297
1298 v4_rules.sort_by_key(|r| (r.priority, r.index));
1299 v6_rules.sort_by_key(|r| (r.priority, r.index));
1300
1301 fn format_matcher(matcher: fnet_matchers_ext::Mark) -> Cow<'static, str> {
1302 match matcher {
1303 fnet_matchers_ext::Mark::Unmarked => Cow::Borrowed("unmarked"),
1304 fnet_matchers_ext::Mark::Marked { mask, between, invert: _ } => {
1305 format!("{mask:#010x}:{:#010x}..{:#010x}", between.start(), between.end()).into()
1306 }
1307 }
1308 }
1309
1310 struct FormatRule {
1311 rule_set_priority: u32,
1312 index: u32,
1313 from: Option<String>,
1314 locally_generated: Option<String>,
1315 bound_device: Option<String>,
1316 mark_1: Option<Cow<'static, str>>,
1317 mark_2: Option<Cow<'static, str>>,
1318 action: Cow<'static, str>,
1319 }
1320
1321 impl FormatRule {
1322 fn from<I: Ip>(rule: froutes_ext::rules::InstalledRule<I>) -> Self {
1323 let froutes_ext::rules::InstalledRule {
1324 priority: rule_set_priority,
1325 index,
1326 matcher:
1327 froutes_ext::rules::RuleMatcher {
1328 from,
1329 locally_generated,
1330 bound_device,
1331 mark_1,
1332 mark_2,
1333 },
1334 action,
1335 } = rule;
1336
1337 let rule_set_priority = u32::from(rule_set_priority);
1338 let index = u32::from(index);
1339 let from = from.map(|from| from.to_string());
1340 let locally_generated = locally_generated.map(|x| x.to_string());
1341 let bound_device = bound_device.map(|matcher| match matcher {
1342 fnet_matchers_ext::BoundInterface::Bound(fnet_matchers_ext::Interface::Name(
1343 name,
1344 )) => format!("name:{name}"),
1345 fnet_matchers_ext::BoundInterface::Bound(fnet_matchers_ext::Interface::Id(id)) => {
1346 format!("id:{id}")
1347 }
1348 fnet_matchers_ext::BoundInterface::Bound(
1349 fnet_matchers_ext::Interface::PortClass(class),
1350 ) => format!("class:{class:?}"),
1351 fnet_matchers_ext::BoundInterface::Unbound => "unbound".into(),
1352 });
1353 let mark_1 = mark_1.map(format_matcher);
1354 let mark_2 = mark_2.map(format_matcher);
1355 let action = match action {
1356 froutes_ext::rules::RuleAction::Unreachable => Cow::Borrowed("unreachable"),
1357 froutes_ext::rules::RuleAction::Lookup(table_id) => {
1358 format!("lookup {table_id}").into()
1359 }
1360 };
1361
1362 FormatRule {
1363 rule_set_priority,
1364 index,
1365 from,
1366 locally_generated,
1367 bound_device,
1368 mark_1,
1369 mark_2,
1370 action,
1371 }
1372 }
1373 }
1374
1375 if out.is_machine() {
1376 fn rule_to_json<I: Ip>(rule: froutes_ext::rules::InstalledRule<I>) -> serde_json::Value {
1377 let FormatRule {
1378 rule_set_priority,
1379 index,
1380 from,
1381 locally_generated,
1382 bound_device,
1383 mark_1,
1384 mark_2,
1385 action,
1386 } = FormatRule::from(rule);
1387
1388 serde_json::json!({
1389 "rule_set_priority": rule_set_priority,
1390 "index": index,
1391 "from": from,
1392 "locally_generated": locally_generated,
1393 "bound_device": bound_device,
1394 "mark_1": mark_1,
1395 "mark_2": mark_2,
1396 "action": action,
1397 })
1398 }
1399
1400 let rules = v4_rules
1401 .into_iter()
1402 .map(rule_to_json)
1403 .chain(v6_rules.into_iter().map(rule_to_json))
1404 .collect::<Vec<_>>();
1405 out.machine(&serde_json::Value::Array(rules)).context("serialize")?;
1406 } else {
1407 let mut t = Table::new();
1408 t.set_format(format::FormatBuilder::new().padding(2, 2).build());
1409 t.set_titles(row![
1410 "RuleSetPriority",
1411 "RuleIndex",
1412 "From",
1413 "LocallyGenerated",
1414 "BoundDevice",
1415 "Mark1Matcher",
1416 "Mark2Matcher",
1417 "Action"
1418 ]);
1419
1420 fn option<D: Deref<Target = str>>(string: &Option<D>) -> &str {
1421 string.as_ref().map_or("-", |s| s.deref())
1422 }
1423
1424 fn write_rule<I: Ip>(t: &mut Table, rule: froutes_ext::rules::InstalledRule<I>) {
1425 let FormatRule {
1426 rule_set_priority,
1427 index,
1428 from,
1429 locally_generated,
1430 bound_device,
1431 mark_1,
1432 mark_2,
1433 action,
1434 } = FormatRule::from(rule);
1435
1436 add_row(
1437 t,
1438 row![
1439 rule_set_priority,
1440 index,
1441 option(&from),
1442 option(&locally_generated),
1443 option(&bound_device),
1444 option(&mark_1),
1445 option(&mark_2),
1446 action,
1447 ],
1448 );
1449 }
1450
1451 for rule in v4_rules {
1452 write_rule(&mut t, rule);
1453 }
1454
1455 for rule in v6_rules {
1456 write_rule(&mut t, rule);
1457 }
1458
1459 let _lines_printed: usize = t.print(out)?;
1460 out.line("")?;
1461 }
1462 Ok(())
1463}
1464
1465async fn do_log<C: NetCliDepsConnector>(cmd: opts::LogEnum, connector: &C) -> Result<(), Error> {
1466 let log = connect_with_context::<fstack::LogMarker, _>(connector).await?;
1467 match cmd {
1468 opts::LogEnum::SetPackets(opts::LogSetPackets { enabled }) => {
1469 log.set_log_packets(enabled).await.context("error setting log packets")?;
1470 info!("log packets set to {:?}", enabled);
1471 }
1472 }
1473 Ok(())
1474}
1475
1476async fn do_dhcp<C: NetCliDepsConnector>(cmd: opts::DhcpEnum, connector: &C) -> Result<(), Error> {
1477 let stack = connect_with_context::<fstack::StackMarker, _>(connector).await?;
1478 match cmd {
1479 opts::DhcpEnum::Start(opts::DhcpStart { interface }) => {
1480 let id = interface.find_nicid(connector).await?.get();
1481 fstack_ext::exec_fidl!(
1482 stack.set_dhcp_client_enabled(id, true),
1483 "error stopping DHCP client"
1484 )?;
1485 info!("dhcp client started on interface {}", id);
1486 }
1487 opts::DhcpEnum::Stop(opts::DhcpStop { interface }) => {
1488 let id = interface.find_nicid(connector).await?.get();
1489 fstack_ext::exec_fidl!(
1490 stack.set_dhcp_client_enabled(id, false),
1491 "error stopping DHCP client"
1492 )?;
1493 info!("dhcp client stopped on interface {}", id);
1494 }
1495 }
1496 Ok(())
1497}
1498
1499async fn do_dhcpd<C: NetCliDepsConnector>(
1500 cmd: opts::dhcpd::DhcpdEnum,
1501 connector: &C,
1502) -> Result<(), Error> {
1503 let dhcpd_server = connect_with_context::<fdhcp::Server_Marker, _>(connector).await?;
1504 match cmd {
1505 opts::dhcpd::DhcpdEnum::Start(opts::dhcpd::Start {}) => {
1506 Ok(do_dhcpd_start(dhcpd_server).await?)
1507 }
1508 opts::dhcpd::DhcpdEnum::Stop(opts::dhcpd::Stop {}) => {
1509 Ok(do_dhcpd_stop(dhcpd_server).await?)
1510 }
1511 opts::dhcpd::DhcpdEnum::Get(get_arg) => Ok(do_dhcpd_get(get_arg, dhcpd_server).await?),
1512 opts::dhcpd::DhcpdEnum::Set(set_arg) => Ok(do_dhcpd_set(set_arg, dhcpd_server).await?),
1513 opts::dhcpd::DhcpdEnum::List(list_arg) => Ok(do_dhcpd_list(list_arg, dhcpd_server).await?),
1514 opts::dhcpd::DhcpdEnum::Reset(reset_arg) => {
1515 Ok(do_dhcpd_reset(reset_arg, dhcpd_server).await?)
1516 }
1517 opts::dhcpd::DhcpdEnum::ClearLeases(opts::dhcpd::ClearLeases {}) => {
1518 Ok(do_dhcpd_clear_leases(dhcpd_server).await?)
1519 }
1520 }
1521}
1522
1523async fn do_neigh<C: NetCliDepsConnector>(
1524 out: writer::JsonWriter<serde_json::Value>,
1525 cmd: opts::NeighEnum,
1526 connector: &C,
1527) -> Result<(), Error> {
1528 match cmd {
1529 opts::NeighEnum::Add(opts::NeighAdd { interface, ip, mac }) => {
1530 let interface = interface.find_nicid(connector).await?.get();
1531 let controller =
1532 connect_with_context::<fneighbor::ControllerMarker, _>(connector).await?;
1533 do_neigh_add(interface, ip.into(), mac.into(), controller)
1534 .await
1535 .context("failed during neigh add command")?;
1536 info!("Added entry ({}, {}) for interface {}", ip, mac, interface);
1537 }
1538 opts::NeighEnum::Clear(opts::NeighClear { interface, ip_version }) => {
1539 let interface = interface.find_nicid(connector).await?.get();
1540 let controller =
1541 connect_with_context::<fneighbor::ControllerMarker, _>(connector).await?;
1542 do_neigh_clear(interface, ip_version, controller)
1543 .await
1544 .context("failed during neigh clear command")?;
1545 info!("Cleared entries for interface {}", interface);
1546 }
1547 opts::NeighEnum::Del(opts::NeighDel { interface, ip }) => {
1548 let interface = interface.find_nicid(connector).await?.get();
1549 let controller =
1550 connect_with_context::<fneighbor::ControllerMarker, _>(connector).await?;
1551 do_neigh_del(interface, ip.into(), controller)
1552 .await
1553 .context("failed during neigh del command")?;
1554 info!("Deleted entry {} for interface {}", ip, interface);
1555 }
1556 opts::NeighEnum::List(opts::NeighList {}) => {
1557 let view = connect_with_context::<fneighbor::ViewMarker, _>(connector).await?;
1558 print_neigh_entries(out, false , view)
1559 .await
1560 .context("error listing neighbor entries")?;
1561 }
1562 opts::NeighEnum::Watch(opts::NeighWatch {}) => {
1563 let view = connect_with_context::<fneighbor::ViewMarker, _>(connector).await?;
1564 print_neigh_entries(out, true , view)
1565 .await
1566 .context("error watching for changes to the neighbor table")?;
1567 }
1568 opts::NeighEnum::Config(opts::NeighConfig { neigh_config_cmd }) => match neigh_config_cmd {
1569 opts::NeighConfigEnum::Get(opts::NeighGetConfig { interface, ip_version }) => {
1570 let interface = interface.find_nicid(connector).await?.get();
1571 let control = get_control(connector, interface).await.context("get control")?;
1572 let configuration = control
1573 .get_configuration()
1574 .await
1575 .map_err(anyhow::Error::new)
1576 .and_then(|res| {
1577 res.map_err(|e: finterfaces_admin::ControlGetConfigurationError| {
1578 anyhow!("{:?}", e)
1579 })
1580 })
1581 .context("get configuration")?;
1582 let nud = extract_nud_config(configuration, ip_version)?;
1583 println!("{:#?}", nud);
1584 }
1585 opts::NeighConfigEnum::Update(opts::NeighUpdateConfig {
1586 interface,
1587 ip_version,
1588 base_reachable_time,
1589 }) => {
1590 let interface = interface.find_nicid(connector).await?.get();
1591 let control = get_control(connector, interface).await.context("get control")?;
1592 let nud_config = finterfaces_admin::NudConfiguration {
1593 base_reachable_time,
1594 ..Default::default()
1595 };
1596 let config = match ip_version {
1597 fnet::IpVersion::V4 => finterfaces_admin::Configuration {
1598 ipv4: Some(finterfaces_admin::Ipv4Configuration {
1599 arp: Some(finterfaces_admin::ArpConfiguration {
1600 nud: Some(nud_config),
1601 ..Default::default()
1602 }),
1603 ..Default::default()
1604 }),
1605 ..Default::default()
1606 },
1607 fnet::IpVersion::V6 => finterfaces_admin::Configuration {
1608 ipv6: Some(finterfaces_admin::Ipv6Configuration {
1609 ndp: Some(finterfaces_admin::NdpConfiguration {
1610 nud: Some(nud_config),
1611 ..Default::default()
1612 }),
1613 ..Default::default()
1614 }),
1615 ..Default::default()
1616 },
1617 };
1618 let prev_config = control
1619 .set_configuration(&config)
1620 .await
1621 .map_err(anyhow::Error::new)
1622 .and_then(|res| {
1623 res.map_err(|e: finterfaces_admin::ControlSetConfigurationError| {
1624 anyhow!("{:?}", e)
1625 })
1626 })
1627 .context("set configuration")?;
1628 let prev_nud = extract_nud_config(prev_config, ip_version)?;
1629 info!("Updated config for interface {}; previously was: {:?}", interface, prev_nud);
1630 }
1631 },
1632 }
1633 Ok(())
1634}
1635
1636async fn do_neigh_add(
1637 interface: u64,
1638 neighbor: fnet::IpAddress,
1639 mac: fnet::MacAddress,
1640 controller: fneighbor::ControllerProxy,
1641) -> Result<(), Error> {
1642 controller
1643 .add_entry(interface, &neighbor.into(), &mac.into())
1644 .await
1645 .context("FIDL error adding neighbor entry")?
1646 .map_err(|e| anyhow::anyhow!("error adding neighbor entry: {e:?}"))
1647}
1648
1649async fn do_neigh_clear(
1650 interface: u64,
1651 ip_version: fnet::IpVersion,
1652 controller: fneighbor::ControllerProxy,
1653) -> Result<(), Error> {
1654 controller
1655 .clear_entries(interface, ip_version)
1656 .await
1657 .context("FIDL error clearing neighbor table")?
1658 .map_err(|e| anyhow::anyhow!("error clearing neighbor table: {e:?}"))
1659}
1660
1661async fn do_neigh_del(
1662 interface: u64,
1663 neighbor: fnet::IpAddress,
1664 controller: fneighbor::ControllerProxy,
1665) -> Result<(), Error> {
1666 controller
1667 .remove_entry(interface, &neighbor.into())
1668 .await
1669 .context("FIDL error removing neighbor entry")?
1670 .map_err(|e| anyhow::anyhow!("error removing neighbor entry: {e:?}"))
1671}
1672
1673fn unpack_neigh_iter_item(
1674 item: fneighbor::EntryIteratorItem,
1675) -> Result<(&'static str, Option<fneighbor_ext::Entry>), Error> {
1676 let displayed_state_change_status = ser::DISPLAYED_NEIGH_ENTRY_VARIANTS.select(&item);
1677
1678 Ok((
1679 displayed_state_change_status,
1680 match item {
1681 fneighbor::EntryIteratorItem::Existing(entry)
1682 | fneighbor::EntryIteratorItem::Added(entry)
1683 | fneighbor::EntryIteratorItem::Changed(entry)
1684 | fneighbor::EntryIteratorItem::Removed(entry) => {
1685 Some(fneighbor_ext::Entry::try_from(entry)?)
1686 }
1687 fneighbor::EntryIteratorItem::Idle(fneighbor::IdleEvent) => None,
1688 },
1689 ))
1690}
1691
1692fn jsonify_neigh_iter_item(
1693 item: fneighbor::EntryIteratorItem,
1694 include_entry_state: bool,
1695) -> Result<Value, Error> {
1696 let (state_change_status, entry) = unpack_neigh_iter_item(item)?;
1697 let entry_json = entry
1698 .map(ser::NeighborTableEntry::from)
1699 .map(serde_json::to_value)
1700 .map(|res| res.map_err(Error::new))
1701 .unwrap_or_else(|| Err(anyhow!("failed to jsonify NeighborTableEntry")))?;
1702 if include_entry_state {
1703 Ok(json!({
1704 "state_change_status": state_change_status,
1705 "entry": entry_json,
1706 }))
1707 } else {
1708 Ok(entry_json)
1709 }
1710}
1711
1712async fn print_neigh_entries(
1713 mut out: writer::JsonWriter<serde_json::Value>,
1714 watch_for_changes: bool,
1715 view: fneighbor::ViewProxy,
1716) -> Result<(), Error> {
1717 let (it, it_server) = view.domain().create_proxy::<fneighbor::EntryIteratorMarker>();
1718
1719 view.open_entry_iterator(it_server, &fneighbor::EntryIteratorOptions::default())
1720 .context("error opening a connection to the entry iterator")?;
1721
1722 let out_ref = &mut out;
1723 if watch_for_changes {
1724 neigh_entry_stream(it, watch_for_changes)
1725 .map_ok(|item| {
1726 write_neigh_entry(out_ref, item, watch_for_changes)
1727 .context("error writing entry")
1728 })
1729 .try_fold((), |(), r| futures::future::ready(r))
1730 .await?;
1731 } else {
1732 let results: Vec<Result<fneighbor::EntryIteratorItem, _>> =
1733 neigh_entry_stream(it, watch_for_changes).collect().await;
1734 if out.is_machine() {
1735 let jsonified_items: Value =
1736 itertools::process_results(results.into_iter(), |items| {
1737 itertools::process_results(
1738 items.map(|item| {
1739 jsonify_neigh_iter_item(
1740 item,
1741 watch_for_changes,
1742 )
1743 }),
1744 |json_values| Value::from_iter(json_values),
1745 )
1746 })??;
1747 out.machine(&jsonified_items)?;
1748 } else {
1749 itertools::process_results(results.into_iter(), |mut items| {
1750 items.try_for_each(|item| {
1751 write_tabular_neigh_entry(
1752 &mut out,
1753 item,
1754 watch_for_changes,
1755 )
1756 })
1757 })??;
1758 }
1759 }
1760
1761 Ok(())
1762}
1763
1764fn neigh_entry_stream(
1765 iterator: fneighbor::EntryIteratorProxy,
1766 watch_for_changes: bool,
1767) -> impl futures::Stream<Item = Result<fneighbor::EntryIteratorItem, Error>> {
1768 futures::stream::try_unfold(iterator, |iterator| {
1769 iterator
1770 .get_next()
1771 .map_ok(|items| Some((items, iterator)))
1772 .map(|r| r.context("error getting items from iterator"))
1773 })
1774 .map_ok(|items| futures::stream::iter(items.into_iter().map(Ok)))
1775 .try_flatten()
1776 .take_while(move |item| {
1777 futures::future::ready(item.as_ref().is_ok_and(|item| {
1778 if let fneighbor::EntryIteratorItem::Idle(fneighbor::IdleEvent {}) = item {
1779 watch_for_changes
1780 } else {
1781 true
1782 }
1783 }))
1784 })
1785}
1786
1787fn write_tabular_neigh_entry<W: std::io::Write>(
1788 mut f: W,
1789 item: fneighbor::EntryIteratorItem,
1790 include_entry_state: bool,
1791) -> Result<(), Error> {
1792 let (state_change_status, entry) = unpack_neigh_iter_item(item)?;
1793 match entry {
1794 Some(entry) => {
1795 if include_entry_state {
1796 writeln!(
1797 &mut f,
1798 "{:width$} | {}",
1799 state_change_status,
1800 entry,
1801 width = ser::DISPLAYED_NEIGH_ENTRY_VARIANTS
1802 .into_iter()
1803 .map(|s| s.len())
1804 .max()
1805 .unwrap_or(0),
1806 )?
1807 } else {
1808 writeln!(&mut f, "{}", entry)?
1809 }
1810 }
1811 None => writeln!(&mut f, "{}", state_change_status)?,
1812 }
1813 Ok(())
1814}
1815
1816fn write_neigh_entry(
1817 f: &mut writer::JsonWriter<serde_json::Value>,
1818 item: fneighbor::EntryIteratorItem,
1819 include_entry_state: bool,
1820) -> Result<(), Error> {
1821 if f.is_machine() {
1822 let entry = jsonify_neigh_iter_item(item, include_entry_state)?;
1823 f.machine(&entry)?;
1824 } else {
1825 write_tabular_neigh_entry(f, item, include_entry_state)?
1826 }
1827 Ok(())
1828}
1829
1830async fn do_dhcpd_start(server: fdhcp::Server_Proxy) -> Result<(), Error> {
1831 server
1832 .start_serving()
1833 .await?
1834 .map_err(zx::Status::err_from_raw)
1835 .context("failed to start server")
1836}
1837
1838async fn do_dhcpd_stop(server: fdhcp::Server_Proxy) -> Result<(), Error> {
1839 server.stop_serving().await.context("failed to stop server")
1840}
1841
1842async fn do_dhcpd_get(get_arg: opts::dhcpd::Get, server: fdhcp::Server_Proxy) -> Result<(), Error> {
1843 match get_arg.arg {
1844 opts::dhcpd::GetArg::Option(opts::dhcpd::OptionArg { name }) => {
1845 let res = server
1846 .get_option(name.clone().into())
1847 .await?
1848 .map_err(zx::Status::err_from_raw)
1849 .with_context(|| format!("get_option({:?}) failed", name))?;
1850 println!("{:#?}", res);
1851 }
1852 opts::dhcpd::GetArg::Parameter(opts::dhcpd::ParameterArg { name }) => {
1853 let res = server
1854 .get_parameter(name.clone().into())
1855 .await?
1856 .map_err(zx::Status::err_from_raw)
1857 .with_context(|| format!("get_parameter({:?}) failed", name))?;
1858 println!("{:#?}", res);
1859 }
1860 };
1861 Ok(())
1862}
1863
1864async fn do_dhcpd_set(set_arg: opts::dhcpd::Set, server: fdhcp::Server_Proxy) -> Result<(), Error> {
1865 match set_arg.arg {
1866 opts::dhcpd::SetArg::Option(opts::dhcpd::OptionArg { name }) => {
1867 server
1868 .set_option(&name.clone().into())
1869 .await?
1870 .map_err(zx::Status::err_from_raw)
1871 .with_context(|| format!("set_option({:?}) failed", name))?;
1872 }
1873 opts::dhcpd::SetArg::Parameter(opts::dhcpd::ParameterArg { name }) => {
1874 server
1875 .set_parameter(&name.clone().into())
1876 .await?
1877 .map_err(zx::Status::err_from_raw)
1878 .with_context(|| format!("set_parameter({:?}) failed", name))?;
1879 }
1880 };
1881 Ok(())
1882}
1883
1884async fn do_dhcpd_list(
1885 list_arg: opts::dhcpd::List,
1886 server: fdhcp::Server_Proxy,
1887) -> Result<(), Error> {
1888 match list_arg.arg {
1889 opts::dhcpd::ListArg::Option(opts::dhcpd::OptionToken {}) => {
1890 let res = server
1891 .list_options()
1892 .await?
1893 .map_err(zx::Status::err_from_raw)
1894 .context("list_options() failed")?;
1895
1896 println!("{:#?}", res);
1897 }
1898 opts::dhcpd::ListArg::Parameter(opts::dhcpd::ParameterToken {}) => {
1899 let res = server
1900 .list_parameters()
1901 .await?
1902 .map_err(zx::Status::err_from_raw)
1903 .context("list_parameters() failed")?;
1904 println!("{:#?}", res);
1905 }
1906 };
1907 Ok(())
1908}
1909
1910async fn do_dhcpd_reset(
1911 reset_arg: opts::dhcpd::Reset,
1912 server: fdhcp::Server_Proxy,
1913) -> Result<(), Error> {
1914 match reset_arg.arg {
1915 opts::dhcpd::ResetArg::Option(opts::dhcpd::OptionToken {}) => {
1916 server
1917 .reset_options()
1918 .await?
1919 .map_err(zx::Status::err_from_raw)
1920 .context("reset_options() failed")?;
1921 }
1922 opts::dhcpd::ResetArg::Parameter(opts::dhcpd::ParameterToken {}) => {
1923 server
1924 .reset_parameters()
1925 .await?
1926 .map_err(zx::Status::err_from_raw)
1927 .context("reset_parameters() failed")?;
1928 }
1929 };
1930 Ok(())
1931}
1932
1933async fn do_dhcpd_clear_leases(server: fdhcp::Server_Proxy) -> Result<(), Error> {
1934 server.clear_leases().await?.map_err(zx::Status::err_from_raw).context("clear_leases() failed")
1935}
1936
1937async fn do_dns<W: std::io::Write, C: NetCliDepsConnector>(
1938 mut out: W,
1939 cmd: opts::dns::DnsEnum,
1940 connector: &C,
1941) -> Result<(), Error> {
1942 let lookup = connect_with_context::<fname::LookupMarker, _>(connector).await?;
1943 let opts::dns::DnsEnum::Lookup(opts::dns::Lookup { hostname, ipv4, ipv6, sort }) = cmd;
1944 let result = lookup
1945 .lookup_ip(
1946 &hostname,
1947 &fname::LookupIpOptions {
1948 ipv4_lookup: Some(ipv4),
1949 ipv6_lookup: Some(ipv6),
1950 sort_addresses: Some(sort),
1951 ..Default::default()
1952 },
1953 )
1954 .await?
1955 .map_err(|e| anyhow!("DNS lookup failed: {:?}", e))?;
1956 let fname::LookupResult { addresses, .. } = result;
1957 let addrs = addresses.context("`addresses` not set in response from DNS resolver")?;
1958 for addr in addrs {
1959 writeln!(out, "{}", fnet_ext::IpAddress::from(addr))?;
1960 }
1961 Ok(())
1962}
1963
1964#[cfg(test)]
1965mod testutil {
1966 use flex_client::fidl::ProtocolMarker;
1967
1968 use super::*;
1969
1970 #[derive(Default)]
1971 pub(crate) struct TestConnector {
1972 pub debug_interfaces: Option<fdebug::InterfacesProxy>,
1973 pub dhcpd: Option<fdhcp::Server_Proxy>,
1974 pub interfaces_state: Option<finterfaces::StateProxy>,
1975 pub stack: Option<fstack::StackProxy>,
1976 pub root_interfaces: Option<froot::InterfacesProxy>,
1977 pub root_filter: Option<froot::FilterProxy>,
1978 pub root_routes_v4: Option<froot::RoutesV4Proxy>,
1979 pub root_routes_v6: Option<froot::RoutesV6Proxy>,
1980 pub routes_v4: Option<froutes::StateV4Proxy>,
1981 pub routes_v6: Option<froutes::StateV6Proxy>,
1982 pub name_lookup: Option<fname::LookupProxy>,
1983 pub filter: Option<fnet_filter::StateProxy>,
1984 pub installer: Option<finterfaces_admin::InstallerProxy>,
1985 pub packet_capture_provider: Option<fdebug::PacketCaptureProviderProxy>,
1986 }
1987
1988 impl CaptureDeps for TestConnector {
1989 type OutputWriter = std::io::Sink;
1990 fn create_output_writer(
1991 &self,
1992 _path: &std::path::Path,
1993 ) -> Result<Self::OutputWriter, anyhow::Error> {
1994 Ok(std::io::sink())
1995 }
1996 }
1997
1998 #[async_trait::async_trait]
1999 impl ServiceConnector<fdebug::InterfacesMarker> for TestConnector {
2000 async fn connect(
2001 &self,
2002 ) -> Result<<fdebug::InterfacesMarker as ProtocolMarker>::Proxy, Error> {
2003 self.debug_interfaces
2004 .as_ref()
2005 .cloned()
2006 .ok_or_else(|| anyhow!("connector has no dhcp server instance"))
2007 }
2008 }
2009
2010 #[async_trait::async_trait]
2011 impl ServiceConnector<froot::InterfacesMarker> for TestConnector {
2012 async fn connect(
2013 &self,
2014 ) -> Result<<froot::InterfacesMarker as ProtocolMarker>::Proxy, Error> {
2015 self.root_interfaces
2016 .as_ref()
2017 .cloned()
2018 .ok_or_else(|| anyhow!("connector has no root interfaces instance"))
2019 }
2020 }
2021
2022 #[async_trait::async_trait]
2023 impl ServiceConnector<froot::FilterMarker> for TestConnector {
2024 async fn connect(&self) -> Result<<froot::FilterMarker as ProtocolMarker>::Proxy, Error> {
2025 self.root_filter
2026 .as_ref()
2027 .cloned()
2028 .ok_or_else(|| anyhow!("connector has no root filter instance"))
2029 }
2030 }
2031
2032 #[async_trait::async_trait]
2033 impl ServiceConnector<fdhcp::Server_Marker> for TestConnector {
2034 async fn connect(&self) -> Result<<fdhcp::Server_Marker as ProtocolMarker>::Proxy, Error> {
2035 self.dhcpd
2036 .as_ref()
2037 .cloned()
2038 .ok_or_else(|| anyhow!("connector has no dhcp server instance"))
2039 }
2040 }
2041
2042 #[async_trait::async_trait]
2043 impl ServiceConnector<finterfaces::StateMarker> for TestConnector {
2044 async fn connect(
2045 &self,
2046 ) -> Result<<finterfaces::StateMarker as ProtocolMarker>::Proxy, Error> {
2047 self.interfaces_state
2048 .as_ref()
2049 .cloned()
2050 .ok_or_else(|| anyhow!("connector has no interfaces state instance"))
2051 }
2052 }
2053
2054 #[async_trait::async_trait]
2055 impl ServiceConnector<finterfaces_admin::InstallerMarker> for TestConnector {
2056 async fn connect(
2057 &self,
2058 ) -> Result<<finterfaces_admin::InstallerMarker as ProtocolMarker>::Proxy, Error> {
2059 self.installer
2060 .as_ref()
2061 .cloned()
2062 .ok_or_else(|| anyhow!("connector has no fuchsia.net.interfaces.admin.Installer"))
2063 }
2064 }
2065
2066 #[async_trait::async_trait]
2067 impl ServiceConnector<fdebug::PacketCaptureProviderMarker> for TestConnector {
2068 async fn connect(
2069 &self,
2070 ) -> Result<<fdebug::PacketCaptureProviderMarker as ProtocolMarker>::Proxy, Error> {
2071 self.packet_capture_provider
2072 .as_ref()
2073 .cloned()
2074 .ok_or_else(|| anyhow!("connector has no packet capture provider instance"))
2075 }
2076 }
2077
2078 #[async_trait::async_trait]
2079 impl ServiceConnector<fneighbor::ControllerMarker> for TestConnector {
2080 async fn connect(
2081 &self,
2082 ) -> Result<<fneighbor::ControllerMarker as ProtocolMarker>::Proxy, Error> {
2083 Err(anyhow!("connect neighbor controller unimplemented for test connector"))
2084 }
2085 }
2086
2087 #[async_trait::async_trait]
2088 impl ServiceConnector<fneighbor::ViewMarker> for TestConnector {
2089 async fn connect(&self) -> Result<<fneighbor::ViewMarker as ProtocolMarker>::Proxy, Error> {
2090 Err(anyhow!("connect neighbor view unimplemented for test connector"))
2091 }
2092 }
2093
2094 #[async_trait::async_trait]
2095 impl ServiceConnector<fstack::LogMarker> for TestConnector {
2096 async fn connect(&self) -> Result<<fstack::LogMarker as ProtocolMarker>::Proxy, Error> {
2097 Err(anyhow!("connect log unimplemented for test connector"))
2098 }
2099 }
2100
2101 #[async_trait::async_trait]
2102 impl ServiceConnector<fstack::StackMarker> for TestConnector {
2103 async fn connect(&self) -> Result<<fstack::StackMarker as ProtocolMarker>::Proxy, Error> {
2104 self.stack.as_ref().cloned().ok_or_else(|| anyhow!("connector has no stack instance"))
2105 }
2106 }
2107
2108 #[async_trait::async_trait]
2109 impl ServiceConnector<froutes::StateV4Marker> for TestConnector {
2110 async fn connect(
2111 &self,
2112 ) -> Result<<froutes::StateV4Marker as ProtocolMarker>::Proxy, Error> {
2113 self.routes_v4
2114 .as_ref()
2115 .cloned()
2116 .ok_or_else(|| anyhow!("connector has no routes_v4 instance"))
2117 }
2118 }
2119
2120 #[async_trait::async_trait]
2121 impl ServiceConnector<froutes::StateV6Marker> for TestConnector {
2122 async fn connect(
2123 &self,
2124 ) -> Result<<froutes::StateV6Marker as ProtocolMarker>::Proxy, Error> {
2125 self.routes_v6
2126 .as_ref()
2127 .cloned()
2128 .ok_or_else(|| anyhow!("connector has no routes_v6 instance"))
2129 }
2130 }
2131
2132 #[async_trait::async_trait]
2133 impl ServiceConnector<froot::RoutesV4Marker> for TestConnector {
2134 async fn connect(&self) -> Result<<froot::RoutesV4Marker as ProtocolMarker>::Proxy, Error> {
2135 self.root_routes_v4
2136 .as_ref()
2137 .cloned()
2138 .ok_or_else(|| anyhow!("connector has no root_routes_v4 instance"))
2139 }
2140 }
2141
2142 #[async_trait::async_trait]
2143 impl ServiceConnector<froot::RoutesV6Marker> for TestConnector {
2144 async fn connect(&self) -> Result<<froot::RoutesV6Marker as ProtocolMarker>::Proxy, Error> {
2145 self.root_routes_v6
2146 .as_ref()
2147 .cloned()
2148 .ok_or_else(|| anyhow!("connector has no root_routes_v6 instance"))
2149 }
2150 }
2151
2152 #[async_trait::async_trait]
2153 impl ServiceConnector<fname::LookupMarker> for TestConnector {
2154 async fn connect(&self) -> Result<<fname::LookupMarker as ProtocolMarker>::Proxy, Error> {
2155 self.name_lookup
2156 .as_ref()
2157 .cloned()
2158 .ok_or_else(|| anyhow!("connector has no name lookup instance"))
2159 }
2160 }
2161
2162 #[async_trait::async_trait]
2163 impl ServiceConnector<fnet_filter::StateMarker> for TestConnector {
2164 async fn connect(
2165 &self,
2166 ) -> Result<<fnet_filter::StateMarker as ProtocolMarker>::Proxy, Error> {
2167 self.filter.as_ref().cloned().ok_or_else(|| anyhow!("connector has no filter instance"))
2168 }
2169 }
2170}
2171
2172#[cfg(test)]
2173mod tests {
2174 use std::convert::TryInto as _;
2175 use std::fmt::Debug;
2176 use std::num::NonZeroU64;
2177
2178 use assert_matches::assert_matches;
2179 #[cfg(not(feature = "fdomain"))]
2180 use fidl_fuchsia_net_routes_ext as froutes_ext;
2181 #[cfg(feature = "fdomain")]
2182 use fidl_fuchsia_net_routes_ext_fdomain as froutes_ext;
2183 use flex_fuchsia_net_routes as froutes;
2184 use fuchsia_async::TimeoutExt as _;
2185 use net_declare::{fidl_ip, fidl_ip_v4, fidl_mac, fidl_subnet};
2186 use test_case::test_case;
2187
2188 use super::testutil::TestConnector;
2189 use super::*;
2190
2191 const IF_ADDR_V4: fnet::Subnet = fidl_subnet!("192.168.0.1/32");
2192 const IF_ADDR_V6: fnet::Subnet = fidl_subnet!("fd00::1/64");
2193
2194 const MAC_1: fnet::MacAddress = fidl_mac!("01:02:03:04:05:06");
2195 const MAC_2: fnet::MacAddress = fidl_mac!("02:03:04:05:06:07");
2196
2197 fn trim_whitespace_for_comparison(s: &str) -> String {
2198 s.trim().lines().map(|s| s.trim()).collect::<Vec<&str>>().join("\n")
2199 }
2200
2201 fn get_fake_interface(
2202 id: u64,
2203 name: &'static str,
2204 port_class: finterfaces_ext::PortClass,
2205 octets: Option<[u8; 6]>,
2206 ) -> (finterfaces_ext::Properties<finterfaces_ext::AllInterest>, Option<fnet::MacAddress>) {
2207 let port_identity_koid = match port_class {
2208 finterfaces_ext::PortClass::Loopback => None,
2209 finterfaces_ext::PortClass::Virtual
2210 | finterfaces_ext::PortClass::Ethernet
2211 | finterfaces_ext::PortClass::WlanClient
2212 | finterfaces_ext::PortClass::WlanAp
2213 | finterfaces_ext::PortClass::Ppp
2214 | finterfaces_ext::PortClass::Bridge
2215 | finterfaces_ext::PortClass::Lowpan
2216 | finterfaces_ext::PortClass::Blackhole => {
2217 Some(finterfaces_ext::PortIdentityKoid::from_raw(id))
2218 }
2219 };
2220 (
2221 finterfaces_ext::Properties {
2222 id: id.try_into().unwrap(),
2223 name: name.to_string(),
2224 port_class,
2225 online: true,
2226 addresses: Vec::new(),
2227 has_default_ipv4_route: false,
2228 has_default_ipv6_route: false,
2229 port_identity_koid,
2230 },
2231 octets.map(|octets| fnet::MacAddress { octets }),
2232 )
2233 }
2234
2235 fn shortlist_interfaces_by_nicid(name_pattern: &str) -> Vec<u64> {
2236 let mut interfaces = [
2237 get_fake_interface(1, "lo", finterfaces_ext::PortClass::Loopback, None),
2238 get_fake_interface(
2239 10,
2240 "eth001",
2241 finterfaces_ext::PortClass::Ethernet,
2242 Some([1, 2, 3, 4, 5, 6]),
2243 ),
2244 get_fake_interface(
2245 20,
2246 "eth002",
2247 finterfaces_ext::PortClass::Ethernet,
2248 Some([1, 2, 3, 4, 5, 7]),
2249 ),
2250 get_fake_interface(
2251 30,
2252 "eth003",
2253 finterfaces_ext::PortClass::Ethernet,
2254 Some([1, 2, 3, 4, 5, 8]),
2255 ),
2256 get_fake_interface(
2257 100,
2258 "wlan001",
2259 finterfaces_ext::PortClass::WlanClient,
2260 Some([2, 2, 3, 4, 5, 6]),
2261 ),
2262 get_fake_interface(
2263 200,
2264 "wlan002",
2265 finterfaces_ext::PortClass::WlanClient,
2266 Some([2, 2, 3, 4, 5, 7]),
2267 ),
2268 get_fake_interface(
2269 300,
2270 "wlan003",
2271 finterfaces_ext::PortClass::WlanClient,
2272 Some([2, 2, 3, 4, 5, 8]),
2273 ),
2274 ]
2275 .into_iter()
2276 .map(|(properties, _): (_, Option<fnet::MacAddress>)| {
2277 let finterfaces_ext::Properties { id, .. } = &properties;
2278 (id.get(), finterfaces_ext::PropertiesAndState { properties, state: () })
2279 })
2280 .collect();
2281 shortlist_interfaces(name_pattern, &mut interfaces);
2282 let mut interfaces: Vec<_> = interfaces.into_keys().collect();
2283 interfaces.sort();
2284 interfaces
2285 }
2286
2287 #[test]
2288 fn test_shortlist_interfaces() {
2289 assert_eq!(vec![1, 10, 20, 30, 100, 200, 300], shortlist_interfaces_by_nicid(""));
2290 assert_eq!(vec![0_u64; 0], shortlist_interfaces_by_nicid("no such thing"));
2291
2292 assert_eq!(vec![1], shortlist_interfaces_by_nicid("lo"));
2293 assert_eq!(vec![10, 20, 30], shortlist_interfaces_by_nicid("eth"));
2294 assert_eq!(vec![10, 20, 30], shortlist_interfaces_by_nicid("th"));
2295 assert_eq!(vec![100, 200, 300], shortlist_interfaces_by_nicid("wlan"));
2296 assert_eq!(vec![10, 100], shortlist_interfaces_by_nicid("001"));
2297 }
2298
2299 #[test_case(fnet::IpVersion::V4, true ; "IPv4 enable routing")]
2300 #[test_case(fnet::IpVersion::V4, false ; "IPv4 disable routing")]
2301 #[test_case(fnet::IpVersion::V6, true ; "IPv6 enable routing")]
2302 #[test_case(fnet::IpVersion::V6, false ; "IPv6 disable routing")]
2303 #[fuchsia::test(logging = false)]
2304 async fn if_ip_forward(ip_version: fnet::IpVersion, enable: bool) {
2305 let client = flex_local::local_client_empty();
2306
2307 let interface1 = TestInterface { nicid: 1, name: "interface1" };
2308 let (root_interfaces, mut requests) =
2309 client.create_proxy_and_stream::<froot::InterfacesMarker>();
2310 let connector =
2311 TestConnector { root_interfaces: Some(root_interfaces), ..Default::default() };
2312
2313 let requests_fut = set_configuration_request(
2314 &mut requests,
2315 interface1.nicid,
2316 |c| extract_ip_forwarding(c, ip_version).expect("extract IP forwarding configuration"),
2317 enable,
2318 );
2319 let buf = writer::TestBuffers::default();
2320 let mut out = writer::JsonWriter::new_test(None, &buf);
2321 let do_if_fut = do_if(
2322 &mut out,
2323 opts::IfEnum::IpForward(opts::IfIpForward {
2324 cmd: opts::IfIpForwardEnum::Set(opts::IfIpForwardSet {
2325 interface: interface1.identifier(false ),
2326 ip_version,
2327 enable,
2328 }),
2329 }),
2330 &connector,
2331 );
2332 let ((), ()) = futures::future::try_join(do_if_fut, requests_fut.map(Ok))
2333 .await
2334 .expect("setting interface ip forwarding should succeed");
2335
2336 let requests_fut = get_configuration_request(
2337 &mut requests,
2338 interface1.nicid,
2339 configuration_with_ip_forwarding_set(ip_version, enable),
2340 );
2341 let buf = writer::TestBuffers::default();
2342 let mut out = writer::JsonWriter::new_test(None, &buf);
2343 let do_if_fut = do_if(
2344 &mut out,
2345 opts::IfEnum::IpForward(opts::IfIpForward {
2346 cmd: opts::IfIpForwardEnum::Get(opts::IfIpForwardGet {
2347 interface: interface1.identifier(false ),
2348 ip_version,
2349 }),
2350 }),
2351 &connector,
2352 );
2353 let ((), ()) = futures::future::try_join(do_if_fut, requests_fut.map(Ok))
2354 .await
2355 .expect("getting interface ip forwarding should succeed");
2356 let got_output = buf.into_stdout_str();
2357 pretty_assertions::assert_eq!(
2358 trim_whitespace_for_comparison(&got_output),
2359 trim_whitespace_for_comparison(&format!(
2360 "IP forwarding for {:?} is {} on interface {}",
2361 ip_version, enable, interface1.nicid
2362 )),
2363 )
2364 }
2365
2366 async fn set_configuration_request<
2367 O: Debug + PartialEq,
2368 F: FnOnce(finterfaces_admin::Configuration) -> O,
2369 >(
2370 requests: &mut froot::InterfacesRequestStream,
2371 expected_nicid: u64,
2372 extract_config: F,
2373 expected_config: O,
2374 ) {
2375 let (id, control, _control_handle) = requests
2376 .next()
2377 .await
2378 .expect("root request stream not ended")
2379 .expect("root request stream not error")
2380 .into_get_admin()
2381 .expect("get admin request");
2382 assert_eq!(id, expected_nicid);
2383
2384 let mut control: finterfaces_admin::ControlRequestStream = control.into_stream();
2385 let (configuration, responder) = control
2386 .next()
2387 .await
2388 .expect("control request stream not ended")
2389 .expect("control request stream not error")
2390 .into_set_configuration()
2391 .expect("set configuration request");
2392 assert_eq!(extract_config(configuration), expected_config);
2393 responder.send(Ok(&Default::default())).expect("responder.send should succeed");
2396 }
2397
2398 async fn get_configuration_request(
2399 requests: &mut froot::InterfacesRequestStream,
2400 expected_nicid: u64,
2401 config: finterfaces_admin::Configuration,
2402 ) {
2403 let (id, control, _control_handle) = requests
2404 .next()
2405 .await
2406 .expect("root request stream not ended")
2407 .expect("root request stream not error")
2408 .into_get_admin()
2409 .expect("get admin request");
2410 assert_eq!(id, expected_nicid);
2411
2412 let mut control: finterfaces_admin::ControlRequestStream = control.into_stream();
2413 let responder = control
2414 .next()
2415 .await
2416 .expect("control request stream not ended")
2417 .expect("control request stream not error")
2418 .into_get_configuration()
2419 .expect("get configuration request");
2420 responder.send(Ok(&config)).expect("responder.send should succeed");
2421 }
2422
2423 #[test_case(finterfaces_admin::IgmpVersion::V1)]
2424 #[test_case(finterfaces_admin::IgmpVersion::V2)]
2425 #[test_case(finterfaces_admin::IgmpVersion::V3)]
2426 #[fuchsia::test(logging = false)]
2427 async fn if_igmp(igmp_version: finterfaces_admin::IgmpVersion) {
2428 let client = flex_local::local_client_empty();
2429
2430 let interface1 = TestInterface { nicid: 1, name: "interface1" };
2431 let (root_interfaces, mut requests) =
2432 client.create_proxy_and_stream::<froot::InterfacesMarker>();
2433 let connector =
2434 TestConnector { root_interfaces: Some(root_interfaces), ..Default::default() };
2435
2436 let requests_fut = set_configuration_request(
2437 &mut requests,
2438 interface1.nicid,
2439 |c| extract_igmp_version(c).unwrap(),
2440 Some(igmp_version),
2441 );
2442 let buffers = writer::TestBuffers::default();
2443 let mut out = writer::JsonWriter::new_test(None, &buffers);
2444 let do_if_fut = do_if(
2445 &mut out,
2446 opts::IfEnum::Igmp(opts::IfIgmp {
2447 cmd: opts::IfIgmpEnum::Set(opts::IfIgmpSet {
2448 interface: interface1.identifier(false ),
2449 version: Some(igmp_version),
2450 }),
2451 }),
2452 &connector,
2453 );
2454 let ((), ()) = futures::future::try_join(do_if_fut, requests_fut.map(Ok))
2455 .await
2456 .expect("setting interface IGMP configuration should succeed");
2457
2458 let requests_fut = get_configuration_request(
2459 &mut requests,
2460 interface1.nicid,
2461 finterfaces_admin::Configuration {
2462 ipv4: Some(finterfaces_admin::Ipv4Configuration {
2463 igmp: Some(finterfaces_admin::IgmpConfiguration {
2464 version: Some(igmp_version),
2465 ..Default::default()
2466 }),
2467 ..Default::default()
2468 }),
2469 ..Default::default()
2470 },
2471 );
2472 let buffers = writer::TestBuffers::default();
2473 let mut output_buf = writer::JsonWriter::new_test(None, &buffers);
2474 let do_if_fut = do_if(
2475 &mut output_buf,
2476 opts::IfEnum::Igmp(opts::IfIgmp {
2477 cmd: opts::IfIgmpEnum::Get(opts::IfIgmpGet {
2478 interface: interface1.identifier(false ),
2479 }),
2480 }),
2481 &connector,
2482 );
2483 let ((), ()) = futures::future::try_join(do_if_fut, requests_fut.map(Ok))
2484 .await
2485 .expect("getting interface IGMP configuration should succeed");
2486 let got_output = buffers.into_stdout_str();
2487 pretty_assertions::assert_eq!(
2488 trim_whitespace_for_comparison(&got_output),
2489 trim_whitespace_for_comparison(&format!(
2490 "IGMP configuration on interface {}:\n Version: {:?}",
2491 interface1.nicid,
2492 Some(igmp_version),
2493 )),
2494 )
2495 }
2496
2497 #[test_case(finterfaces_admin::MldVersion::V1)]
2498 #[test_case(finterfaces_admin::MldVersion::V2)]
2499 #[fuchsia::test(logging = false)]
2500 async fn if_mld(mld_version: finterfaces_admin::MldVersion) {
2501 let client = flex_local::local_client_empty();
2502
2503 let interface1 = TestInterface { nicid: 1, name: "interface1" };
2504 let (root_interfaces, mut requests) =
2505 client.create_proxy_and_stream::<froot::InterfacesMarker>();
2506 let connector =
2507 TestConnector { root_interfaces: Some(root_interfaces), ..Default::default() };
2508
2509 let requests_fut = set_configuration_request(
2510 &mut requests,
2511 interface1.nicid,
2512 |c| extract_mld_version(c).unwrap(),
2513 Some(mld_version),
2514 );
2515 let buffers = writer::TestBuffers::default();
2516 let mut out = writer::JsonWriter::new_test(None, &buffers);
2517 let do_if_fut = do_if(
2518 &mut out,
2519 opts::IfEnum::Mld(opts::IfMld {
2520 cmd: opts::IfMldEnum::Set(opts::IfMldSet {
2521 interface: interface1.identifier(false ),
2522 version: Some(mld_version),
2523 }),
2524 }),
2525 &connector,
2526 );
2527 let ((), ()) = futures::future::try_join(do_if_fut, requests_fut.map(Ok))
2528 .await
2529 .expect("setting interface MLD configuration should succeed");
2530
2531 let requests_fut = get_configuration_request(
2532 &mut requests,
2533 interface1.nicid,
2534 finterfaces_admin::Configuration {
2535 ipv6: Some(finterfaces_admin::Ipv6Configuration {
2536 mld: Some(finterfaces_admin::MldConfiguration {
2537 version: Some(mld_version),
2538 ..Default::default()
2539 }),
2540 ..Default::default()
2541 }),
2542 ..Default::default()
2543 },
2544 );
2545 let buffers = writer::TestBuffers::default();
2546 let mut output_buf = writer::JsonWriter::new_test(None, &buffers);
2547 let do_if_fut = do_if(
2548 &mut output_buf,
2549 opts::IfEnum::Mld(opts::IfMld {
2550 cmd: opts::IfMldEnum::Get(opts::IfMldGet {
2551 interface: interface1.identifier(false ),
2552 }),
2553 }),
2554 &connector,
2555 );
2556 let ((), ()) = futures::future::try_join(do_if_fut, requests_fut.map(Ok))
2557 .await
2558 .expect("getting interface MLD configuration should succeed");
2559 let got_output = buffers.into_stdout_str();
2560 pretty_assertions::assert_eq!(
2561 trim_whitespace_for_comparison(&got_output),
2562 trim_whitespace_for_comparison(&format!(
2563 "MLD configuration on interface {}:\n Version: {:?}",
2564 interface1.nicid,
2565 Some(mld_version),
2566 )),
2567 )
2568 }
2569
2570 async fn always_answer_with_interfaces(
2571 interfaces_state_requests: finterfaces::StateRequestStream,
2572 interfaces: Vec<finterfaces::Properties>,
2573 ) {
2574 interfaces_state_requests
2575 .try_for_each(|request| {
2576 let interfaces = interfaces.clone();
2577 async move {
2578 let (finterfaces::WatcherOptions { .. }, server_end, _): (
2579 _,
2580 _,
2581 finterfaces::StateControlHandle,
2582 ) = request.into_get_watcher().expect("request type should be GetWatcher");
2583
2584 let mut watcher_request_stream: finterfaces::WatcherRequestStream =
2585 server_end.into_stream();
2586
2587 for event in interfaces
2588 .into_iter()
2589 .map(finterfaces::Event::Existing)
2590 .chain(std::iter::once(finterfaces::Event::Idle(finterfaces::Empty)))
2591 {
2592 watcher_request_stream
2593 .try_next()
2594 .await
2595 .expect("watcher watch FIDL error")
2596 .expect("watcher request stream should not have ended")
2597 .into_watch()
2598 .expect("request should be of type Watch")
2599 .send(&event)
2600 .expect("responder.send should succeed");
2601 }
2602
2603 assert_matches!(
2604 watcher_request_stream.try_next().await.expect("watcher watch FIDL error"),
2605 None,
2606 "remaining watcher request stream should be empty"
2607 );
2608 Ok(())
2609 }
2610 })
2611 .await
2612 .expect("interfaces state FIDL error")
2613 }
2614
2615 #[derive(Clone)]
2616 struct TestInterface {
2617 nicid: u64,
2618 name: &'static str,
2619 }
2620
2621 impl TestInterface {
2622 fn identifier(&self, use_ifname: bool) -> opts::InterfaceIdentifier {
2623 let Self { nicid, name } = self;
2624 if use_ifname {
2625 opts::InterfaceIdentifier::Name(name.to_string())
2626 } else {
2627 opts::InterfaceIdentifier::Id(NonZeroU64::new(*nicid).unwrap())
2628 }
2629 }
2630 }
2631
2632 #[test_case(true, false ; "when interface is up, and adding subnet route")]
2633 #[test_case(true, true ; "when interface is up, and not adding subnet route")]
2634 #[test_case(false, false ; "when interface is down, and adding subnet route")]
2635 #[test_case(false, true ; "when interface is down, and not adding subnet route")]
2636 #[fuchsia::test]
2637 async fn if_addr_add(interface_is_up: bool, no_subnet_route: bool) {
2638 let client = flex_local::local_client_empty();
2639
2640 const TEST_PREFIX_LENGTH: u8 = 64;
2641
2642 let interface1 = TestInterface { nicid: 1, name: "interface1" };
2643 let (root_interfaces, mut requests) =
2644 client.create_proxy_and_stream::<froot::InterfacesMarker>();
2645
2646 let connector =
2647 TestConnector { root_interfaces: Some(root_interfaces), ..Default::default() };
2648 let buffers = writer::TestBuffers::default();
2649 let mut out = writer::JsonWriter::new_test(None, &buffers);
2650 let do_if_fut = do_if(
2651 &mut out,
2652 opts::IfEnum::Addr(opts::IfAddr {
2653 addr_cmd: opts::IfAddrEnum::Add(opts::IfAddrAdd {
2654 interface: interface1.identifier(false ),
2655 addr: fnet_ext::IpAddress::from(IF_ADDR_V6.addr).to_string(),
2656 prefix: TEST_PREFIX_LENGTH,
2657 no_subnet_route,
2658 }),
2659 }),
2660 &connector,
2661 )
2662 .map(|res| res.expect("success"));
2663
2664 let admin_fut = async {
2665 let (id, control, _control_handle) = requests
2666 .next()
2667 .await
2668 .expect("root request stream not ended")
2669 .expect("root request stream not error")
2670 .into_get_admin()
2671 .expect("get admin request");
2672 assert_eq!(id, interface1.nicid);
2673
2674 let mut control: finterfaces_admin::ControlRequestStream = control.into_stream();
2675 let (
2676 addr,
2677 addr_params,
2678 address_state_provider_server_end,
2679 _admin_control_control_handle,
2680 ) = control
2681 .next()
2682 .await
2683 .expect("control request stream not ended")
2684 .expect("control request stream not error")
2685 .into_add_address()
2686 .expect("add address request");
2687 assert_eq!(addr, IF_ADDR_V6);
2688 assert_eq!(
2689 addr_params,
2690 finterfaces_admin::AddressParameters {
2691 add_subnet_route: Some(!no_subnet_route),
2692 ..Default::default()
2693 }
2694 );
2695
2696 let mut address_state_provider_request_stream =
2697 address_state_provider_server_end.into_stream();
2698 async fn next_request(
2699 stream: &mut finterfaces_admin::AddressStateProviderRequestStream,
2700 ) -> finterfaces_admin::AddressStateProviderRequest {
2701 stream
2702 .next()
2703 .await
2704 .expect("address state provider request stream not ended")
2705 .expect("address state provider request stream not error")
2706 }
2707
2708 let _address_state_provider_control_handle =
2709 next_request(&mut address_state_provider_request_stream)
2710 .await
2711 .into_detach()
2712 .expect("detach request");
2713
2714 for _ in 0..3 {
2715 next_request(&mut address_state_provider_request_stream)
2716 .await
2717 .into_watch_address_assignment_state()
2718 .expect("watch address assignment state request")
2719 .send(finterfaces::AddressAssignmentState::Tentative)
2720 .expect("send address assignment state succeeds");
2721 }
2722
2723 next_request(&mut address_state_provider_request_stream)
2724 .await
2725 .into_watch_address_assignment_state()
2726 .expect("watch address assignment state request")
2727 .send(if interface_is_up {
2728 finterfaces::AddressAssignmentState::Assigned
2729 } else {
2730 finterfaces::AddressAssignmentState::Unavailable
2731 })
2732 .expect("send address assignment state succeeds");
2733 };
2734
2735 let ((), ()) = futures::join!(admin_fut, do_if_fut);
2736 }
2737
2738 #[test_case(false ; "providing nicids")]
2739 #[test_case(true ; "providing interface names")]
2740 #[fuchsia::test]
2741 async fn if_del_addr(use_ifname: bool) {
2742 let client = flex_local::local_client_empty();
2743
2744 let interface1 = TestInterface { nicid: 1, name: "interface1" };
2745 let interface2 = TestInterface { nicid: 2, name: "interface2" };
2746
2747 let (root_interfaces, mut requests) =
2748 client.create_proxy_and_stream::<froot::InterfacesMarker>();
2749 let (interfaces_state, interfaces_requests) =
2750 client.create_proxy_and_stream::<finterfaces::StateMarker>();
2751
2752 let (interface1_properties, _mac) = get_fake_interface(
2753 interface1.nicid,
2754 interface1.name,
2755 finterfaces_ext::PortClass::Ethernet,
2756 None,
2757 );
2758
2759 let interfaces_fut =
2760 always_answer_with_interfaces(interfaces_requests, vec![interface1_properties.into()])
2761 .fuse();
2762 let mut interfaces_fut = pin!(interfaces_fut);
2763
2764 let connector = TestConnector {
2765 root_interfaces: Some(root_interfaces),
2766 interfaces_state: Some(interfaces_state),
2767 ..Default::default()
2768 };
2769
2770 let buffers = writer::TestBuffers::default();
2771 let mut out = writer::JsonWriter::new_test(None, &buffers);
2772 let succeeds = do_if(
2774 &mut out,
2775 opts::IfEnum::Addr(opts::IfAddr {
2776 addr_cmd: opts::IfAddrEnum::Del(opts::IfAddrDel {
2777 interface: interface1.identifier(use_ifname),
2778 addr: fnet_ext::IpAddress::from(IF_ADDR_V4.addr).to_string(),
2779 prefix: None, }),
2781 }),
2782 &connector,
2783 )
2784 .map(|res| res.expect("success"));
2785 let handler_fut = async {
2786 let (id, control, _control_handle) = requests
2787 .next()
2788 .await
2789 .expect("root request stream not ended")
2790 .expect("root request stream not error")
2791 .into_get_admin()
2792 .expect("get admin request");
2793 assert_eq!(id, interface1.nicid);
2794 let mut control = control.into_stream();
2795 let (addr, responder) = control
2796 .next()
2797 .await
2798 .expect("control request stream not ended")
2799 .expect("control request stream not error")
2800 .into_remove_address()
2801 .expect("del address request");
2802 assert_eq!(addr, IF_ADDR_V4);
2803 responder.send(Ok(true)).expect("responder send");
2804 };
2805
2806 futures::select! {
2807 () = interfaces_fut => panic!("interfaces_fut should never complete"),
2808 ((), ()) = futures::future::join(handler_fut, succeeds).fuse() => {},
2809 }
2810
2811 let buffers = writer::TestBuffers::default();
2812 let mut out = writer::JsonWriter::new_test(None, &buffers);
2813 let fails = do_if(
2815 &mut out,
2816 opts::IfEnum::Addr(opts::IfAddr {
2817 addr_cmd: opts::IfAddrEnum::Del(opts::IfAddrDel {
2818 interface: interface2.identifier(use_ifname),
2819 addr: fnet_ext::IpAddress::from(IF_ADDR_V6.addr).to_string(),
2820 prefix: Some(IF_ADDR_V6.prefix_len),
2821 }),
2822 }),
2823 &connector,
2824 )
2825 .map(|res| res.expect_err("failure"));
2826
2827 if use_ifname {
2828 futures::select! {
2831 () = interfaces_fut => panic!("interfaces_fut should never complete"),
2832 e = fails.fuse() => {
2833 assert_eq!(e.to_string(), format!("No interface with name {}", interface2.name));
2834 },
2835 }
2836 } else {
2837 let handler_fut = async {
2838 let (id, control, _control_handle) = requests
2839 .next()
2840 .await
2841 .expect("root request stream not ended")
2842 .expect("root request stream not error")
2843 .into_get_admin()
2844 .expect("get admin request");
2845 assert_eq!(id, interface2.nicid);
2846 let mut control = control.into_stream();
2847 let (addr, responder) = control
2848 .next()
2849 .await
2850 .expect("control request stream not ended")
2851 .expect("control request stream not error")
2852 .into_remove_address()
2853 .expect("del address request");
2854 assert_eq!(addr, IF_ADDR_V6);
2855 responder.send(Ok(false)).expect("responder send");
2856 };
2857 futures::select! {
2858 () = interfaces_fut => panic!("interfaces_fut should never complete"),
2859 ((), e) = futures::future::join(handler_fut, fails).fuse() => {
2860 let fnet_ext::IpAddress(addr) = IF_ADDR_V6.addr.into();
2861 assert_eq!(e.to_string(), format!("Address {} not found on interface {}", addr, interface2.nicid));
2862 },
2863 }
2864 }
2865 }
2866
2867 const INTERFACE_NAME: &str = "if1";
2868
2869 fn interface_properties(
2870 addrs: Vec<(fnet::Subnet, finterfaces::AddressAssignmentState)>,
2871 ) -> finterfaces::Properties {
2872 finterfaces_ext::Properties {
2873 id: INTERFACE_ID.try_into().unwrap(),
2874 name: INTERFACE_NAME.to_string(),
2875 port_class: finterfaces_ext::PortClass::Ethernet,
2876 online: true,
2877 addresses: addrs
2878 .into_iter()
2879 .map(|(addr, assignment_state)| finterfaces_ext::Address::<
2880 finterfaces_ext::AllInterest,
2881 > {
2882 addr,
2883 assignment_state,
2884 valid_until: finterfaces_ext::PositiveMonotonicInstant::INFINITE_FUTURE,
2885 preferred_lifetime_info:
2886 finterfaces_ext::PreferredLifetimeInfo::preferred_forever(),
2887 })
2888 .collect(),
2889 has_default_ipv4_route: false,
2890 has_default_ipv6_route: false,
2891 port_identity_koid: None,
2892 }
2893 .into()
2894 }
2895
2896 #[test_case(
2897 false,
2898 vec![
2899 finterfaces::Event::Existing(interface_properties(vec![])),
2900 finterfaces::Event::Idle(finterfaces::Empty),
2901 finterfaces::Event::Changed(interface_properties(vec![
2902 (fidl_subnet!("192.168.0.1/32"), finterfaces::AddressAssignmentState::Assigned)
2903 ])),
2904 ],
2905 "192.168.0.1";
2906 "wait for an address to be assigned"
2907 )]
2908 #[test_case(
2909 false,
2910 vec![
2911 finterfaces::Event::Existing(interface_properties(vec![
2912 (fidl_subnet!("192.168.0.1/32"), finterfaces::AddressAssignmentState::Assigned),
2913 (fidl_subnet!("fd00::1/64"), finterfaces::AddressAssignmentState::Assigned),
2914 ])),
2915 ],
2916 "192.168.0.1";
2917 "prefer first when any address requested"
2918 )]
2919 #[test_case(
2920 true,
2921 vec![
2922 finterfaces::Event::Existing(interface_properties(vec![
2923 (fidl_subnet!("192.168.0.1/32"), finterfaces::AddressAssignmentState::Assigned)
2924 ])),
2925 finterfaces::Event::Idle(finterfaces::Empty),
2926 finterfaces::Event::Changed(interface_properties(vec![
2927 (fidl_subnet!("fd00::1/64"), finterfaces::AddressAssignmentState::Assigned)
2928 ])),
2929 ],
2930 "fd00::1";
2931 "wait for IPv6 when IPv6 address requested"
2932 )]
2933 #[fuchsia::test]
2934 async fn if_addr_wait(ipv6: bool, events: Vec<finterfaces::Event>, expected_output: &str) {
2935 let client = flex_local::local_client_empty();
2936
2937 let interface = TestInterface { nicid: INTERFACE_ID, name: INTERFACE_NAME };
2938
2939 let (interfaces_state, mut request_stream) =
2940 client.create_proxy_and_stream::<finterfaces::StateMarker>();
2941
2942 let interfaces_handler = async move {
2943 let (finterfaces::WatcherOptions { include_non_assigned_addresses, .. }, server_end, _) =
2944 request_stream
2945 .next()
2946 .await
2947 .expect("should call state")
2948 .expect("should succeed")
2949 .into_get_watcher()
2950 .expect("request should be GetWatcher");
2951 assert_eq!(include_non_assigned_addresses, Some(false));
2952 let mut request_stream: finterfaces::WatcherRequestStream = server_end.into_stream();
2953 for event in events {
2954 request_stream
2955 .next()
2956 .await
2957 .expect("should call watcher")
2958 .expect("should succeed")
2959 .into_watch()
2960 .expect("request should be Watch")
2961 .send(&event)
2962 .expect("send response");
2963 }
2964 };
2965
2966 let connector =
2967 TestConnector { interfaces_state: Some(interfaces_state), ..Default::default() };
2968 let buffers = writer::TestBuffers::default();
2969 let mut out = writer::JsonWriter::new_test(None, &buffers);
2970 let run_command = do_if(
2971 &mut out,
2972 opts::IfEnum::Addr(opts::IfAddr {
2973 addr_cmd: opts::IfAddrEnum::Wait(opts::IfAddrWait {
2974 interface: interface.identifier(false),
2975 ipv6,
2976 }),
2977 }),
2978 &connector,
2979 )
2980 .map(|r| r.expect("command should succeed"));
2981
2982 let ((), ()) = futures::future::join(interfaces_handler, run_command).await;
2983
2984 let output = buffers.into_stdout_str();
2985 pretty_assertions::assert_eq!(
2986 trim_whitespace_for_comparison(&output),
2987 trim_whitespace_for_comparison(expected_output),
2988 );
2989 }
2990
2991 fn wanted_net_if_list_json() -> String {
2992 json!([
2993 {
2994 "addresses": {
2995 "ipv4": [],
2996 "ipv6": [],
2997 },
2998 "device_class": "Loopback",
2999 "mac": "00:00:00:00:00:00",
3000 "name": "lo",
3001 "nicid": 1,
3002 "online": true,
3003 "has_default_ipv4_route": false,
3004 "has_default_ipv6_route": false,
3005 "port_identity_koid": null,
3006 },
3007 {
3008 "addresses": {
3009 "ipv4": [],
3010 "ipv6": [],
3011 },
3012 "device_class": "Ethernet",
3013 "mac": "01:02:03:04:05:06",
3014 "name": "eth001",
3015 "nicid": 10,
3016 "online": true,
3017 "has_default_ipv4_route": false,
3018 "has_default_ipv6_route": false,
3019 "port_identity_koid": 10,
3020 },
3021 {
3022 "addresses": {
3023 "ipv4": [],
3024 "ipv6": [],
3025 },
3026 "device_class": "Virtual",
3027 "mac": null,
3028 "name": "virt001",
3029 "nicid": 20,
3030 "online": true,
3031 "has_default_ipv4_route": false,
3032 "has_default_ipv6_route": false,
3033 "port_identity_koid": 20,
3034 },
3035 {
3036 "addresses": {
3037 "ipv4": [
3038 {
3039 "addr": "192.168.0.1",
3040 "assignment_state": "Tentative",
3041 "prefix_len": 24,
3042 "valid_until": 2500000000_u64,
3043 }
3044 ],
3045 "ipv6": [],
3046 },
3047 "device_class": "Ethernet",
3048 "mac": null,
3049 "name": "eth002",
3050 "nicid": 30,
3051 "online": true,
3052 "has_default_ipv4_route": false,
3053 "has_default_ipv6_route": true,
3054 "port_identity_koid": null,
3055 },
3056 {
3057 "addresses": {
3058 "ipv4": [],
3059 "ipv6": [{
3060 "addr": "2001:db8::1",
3061 "assignment_state": "Unavailable",
3062 "prefix_len": 64,
3063 "valid_until": null,
3064 }],
3065 },
3066 "device_class": "Ethernet",
3067 "mac": null,
3068 "name": "eth003",
3069 "nicid": 40,
3070 "online": true,
3071 "has_default_ipv4_route": true,
3072 "has_default_ipv6_route": true,
3073 "port_identity_koid": null,
3074 },
3075 ])
3076 .to_string()
3077 }
3078
3079 fn wanted_net_if_list_tabular() -> String {
3080 String::from(
3081 r#"
3082nicid 1
3083name lo
3084device class loopback
3085online true
3086default routes -
3087mac 00:00:00:00:00:00
3088port_identity_koid -
3089
3090nicid 10
3091name eth001
3092device class ethernet
3093online true
3094default routes -
3095mac 01:02:03:04:05:06
3096port_identity_koid 10
3097
3098nicid 20
3099name virt001
3100device class virtual
3101online true
3102default routes -
3103mac -
3104port_identity_koid 20
3105
3106nicid 30
3107name eth002
3108device class ethernet
3109online true
3110default routes IPv6
3111addr 192.168.0.1/24 TENTATIVE valid until [2.5s]
3112mac -
3113port_identity_koid -
3114
3115nicid 40
3116name eth003
3117device class ethernet
3118online true
3119default routes IPv4,IPv6
3120addr 2001:db8::1/64 UNAVAILABLE
3121mac -
3122port_identity_koid -
3123"#,
3124 )
3125 }
3126
3127 #[test_case(true, wanted_net_if_list_json() ; "in json format")]
3128 #[test_case(false, wanted_net_if_list_tabular() ; "in tabular format")]
3129 #[fuchsia::test]
3130 async fn if_list(json: bool, wanted_output: String) {
3131 let client = flex_local::local_client_empty();
3132
3133 let (root_interfaces, root_interfaces_stream) =
3134 client.create_proxy_and_stream::<froot::InterfacesMarker>();
3135 let (interfaces_state, interfaces_state_stream) =
3136 client.create_proxy_and_stream::<finterfaces::StateMarker>();
3137
3138 let buffers = writer::TestBuffers::default();
3139 let mut output = if json {
3140 writer::JsonWriter::new_test(Some(writer::Format::Json), &buffers)
3141 } else {
3142 writer::JsonWriter::new_test(None, &buffers)
3143 };
3144 let output_ref = &mut output;
3145
3146 let do_if_fut = async {
3147 let connector = TestConnector {
3148 root_interfaces: Some(root_interfaces),
3149 interfaces_state: Some(interfaces_state),
3150 ..Default::default()
3151 };
3152 do_if(output_ref, opts::IfEnum::List(opts::IfList { name_pattern: None }), &connector)
3153 .map(|res| res.expect("if list"))
3154 .await
3155 };
3156 let watcher_stream = interfaces_state_stream
3157 .and_then(|req| match req {
3158 finterfaces::StateRequest::GetWatcher {
3159 options: _,
3160 watcher,
3161 control_handle: _,
3162 } => futures::future::ready(Ok(watcher.into_stream())),
3163 })
3164 .try_flatten()
3165 .map(|res| res.expect("watcher stream error"));
3166 let (interfaces, mac_addresses): (Vec<_>, HashMap<_, _>) = [
3167 get_fake_interface(
3168 1,
3169 "lo",
3170 finterfaces_ext::PortClass::Loopback,
3171 Some([0, 0, 0, 0, 0, 0]),
3172 ),
3173 get_fake_interface(
3174 10,
3175 "eth001",
3176 finterfaces_ext::PortClass::Ethernet,
3177 Some([1, 2, 3, 4, 5, 6]),
3178 ),
3179 get_fake_interface(20, "virt001", finterfaces_ext::PortClass::Virtual, None),
3180 (
3181 finterfaces_ext::Properties {
3182 id: 30.try_into().unwrap(),
3183 name: "eth002".to_string(),
3184 port_class: finterfaces_ext::PortClass::Ethernet,
3185 online: true,
3186 addresses: vec![finterfaces_ext::Address {
3187 addr: fidl_subnet!("192.168.0.1/24"),
3188 valid_until: i64::try_from(
3189 std::time::Duration::from_millis(2500).as_nanos(),
3190 )
3191 .unwrap()
3192 .try_into()
3193 .unwrap(),
3194 assignment_state: finterfaces::AddressAssignmentState::Tentative,
3195 preferred_lifetime_info:
3196 finterfaces_ext::PreferredLifetimeInfo::preferred_forever(),
3197 }],
3198 has_default_ipv4_route: false,
3199 has_default_ipv6_route: true,
3200 port_identity_koid: None,
3201 },
3202 None,
3203 ),
3204 (
3205 finterfaces_ext::Properties {
3206 id: 40.try_into().unwrap(),
3207 name: "eth003".to_string(),
3208 port_class: finterfaces_ext::PortClass::Ethernet,
3209 online: true,
3210 addresses: vec![finterfaces_ext::Address {
3211 addr: fidl_subnet!("2001:db8::1/64"),
3212 valid_until: finterfaces_ext::PositiveMonotonicInstant::INFINITE_FUTURE,
3213 assignment_state: finterfaces::AddressAssignmentState::Unavailable,
3214 preferred_lifetime_info:
3215 finterfaces_ext::PreferredLifetimeInfo::preferred_forever(),
3216 }],
3217 has_default_ipv4_route: true,
3218 has_default_ipv6_route: true,
3219 port_identity_koid: None,
3220 },
3221 None,
3222 ),
3223 ]
3224 .into_iter()
3225 .map(|(properties, mac)| {
3226 let finterfaces_ext::Properties { id, .. } = &properties;
3227 let id = *id;
3228 (properties, (id, mac))
3229 })
3230 .unzip();
3231 let interfaces =
3232 futures::stream::iter(interfaces.into_iter().map(Some).chain(std::iter::once(None)));
3233 let watcher_fut = watcher_stream.zip(interfaces).for_each(|(req, properties)| match req {
3234 finterfaces::WatcherRequest::Watch { responder } => {
3235 let event = properties.map_or(
3236 finterfaces::Event::Idle(finterfaces::Empty),
3237 |finterfaces_ext::Properties {
3238 id,
3239 name,
3240 port_class,
3241 online,
3242 addresses,
3243 has_default_ipv4_route,
3244 has_default_ipv6_route,
3245 port_identity_koid,
3246 }| {
3247 finterfaces::Event::Existing(finterfaces::Properties {
3248 id: Some(id.get()),
3249 name: Some(name),
3250 port_class: Some(port_class.into()),
3251 online: Some(online),
3252 addresses: Some(
3253 addresses.into_iter().map(finterfaces::Address::from).collect(),
3254 ),
3255 has_default_ipv4_route: Some(has_default_ipv4_route),
3256 has_default_ipv6_route: Some(has_default_ipv6_route),
3257 port_identity_koid: port_identity_koid.map(|p| p.raw_koid()),
3258 ..Default::default()
3259 })
3260 },
3261 );
3262 responder.send(&event).expect("send watcher event");
3263 futures::future::ready(())
3264 }
3265 });
3266 let root_fut = root_interfaces_stream
3267 .map(|res| res.expect("root interfaces stream error"))
3268 .for_each_concurrent(None, |req| {
3269 let (id, responder) = req.into_get_mac().expect("get_mac request");
3270 responder
3271 .send(
3272 mac_addresses
3273 .get(&id.try_into().unwrap())
3274 .map(Option::as_ref)
3275 .ok_or(froot::InterfacesGetMacError::NotFound),
3276 )
3277 .expect("send get_mac response");
3278 futures::future::ready(())
3279 });
3280 let ((), (), ()) = futures::future::join3(do_if_fut, watcher_fut, root_fut).await;
3281
3282 let got_output = buffers.into_stdout_str();
3283
3284 if json {
3285 let got: Value = serde_json::from_str(&got_output).unwrap();
3286 let want: Value = serde_json::from_str(&wanted_output).unwrap();
3287 pretty_assertions::assert_eq!(got, want);
3288 } else {
3289 pretty_assertions::assert_eq!(
3290 trim_whitespace_for_comparison(&got_output),
3291 trim_whitespace_for_comparison(&wanted_output),
3292 );
3293 }
3294 }
3295
3296 async fn test_do_dhcp(client: flex_client::ClientArg, cmd: opts::DhcpEnum) {
3297 let (stack, mut requests) = client.create_proxy_and_stream::<fstack::StackMarker>();
3298 let connector = TestConnector { stack: Some(stack), ..Default::default() };
3299 let op = do_dhcp(cmd.clone(), &connector);
3300 let op_succeeds = async move {
3301 let (expected_id, expected_enable) = match cmd {
3302 opts::DhcpEnum::Start(opts::DhcpStart { interface }) => (interface, true),
3303 opts::DhcpEnum::Stop(opts::DhcpStop { interface }) => (interface, false),
3304 };
3305 let request = requests
3306 .try_next()
3307 .await
3308 .expect("start FIDL error")
3309 .expect("request stream should not have ended");
3310 let (received_id, enable, responder) = request
3311 .into_set_dhcp_client_enabled()
3312 .expect("request should be of type StopDhcpClient");
3313 assert_eq!(
3314 opts::InterfaceIdentifier::Id(NonZeroU64::new(u64::from(received_id)).unwrap()),
3315 expected_id
3316 );
3317 assert_eq!(enable, expected_enable);
3318 responder.send(Ok(())).map_err(anyhow::Error::new)
3319 };
3320 let ((), ()) =
3321 futures::future::try_join(op, op_succeeds).await.expect("dhcp command should succeed");
3322 }
3323
3324 #[fuchsia::test]
3325 async fn dhcp_start() {
3326 let client = flex_local::local_client_empty();
3327
3328 test_do_dhcp(
3329 client,
3330 opts::DhcpEnum::Start(opts::DhcpStart {
3331 interface: NonZeroU64::new(1).unwrap().into(),
3332 }),
3333 )
3334 .await;
3335 }
3336
3337 #[fuchsia::test]
3338 async fn dhcp_stop() {
3339 let client = flex_local::local_client_empty();
3340
3341 test_do_dhcp(
3342 client,
3343 opts::DhcpEnum::Stop(opts::DhcpStop { interface: NonZeroU64::new(1).unwrap().into() }),
3344 )
3345 .await;
3346 }
3347
3348 async fn test_modify_route(client: flex_client::ClientArg, cmd: opts::RouteEnum) {
3349 let event_token = client.create_event();
3350 let expected_interface = match &cmd {
3351 opts::RouteEnum::List(_) => panic!("test_modify_route should not take a List command"),
3352 opts::RouteEnum::Add(opts::RouteAdd { interface, .. }) => interface,
3353 opts::RouteEnum::Del(opts::RouteDel { interface, .. }) => interface,
3354 }
3355 .clone();
3356 let expected_id = match expected_interface {
3357 opts::InterfaceIdentifier::Id(ref id) => *id,
3358 opts::InterfaceIdentifier::Name(_) => {
3359 panic!("expected test to work only with ids")
3360 }
3361 };
3362
3363 let (root_interfaces, mut interfaces_requests) =
3364 client.create_proxy_and_stream::<froot::InterfacesMarker>();
3365 let (routes_v4, mut routes_v4_requests) =
3366 client.create_proxy_and_stream::<froot::RoutesV4Marker>();
3367 let (routes_v6, _routes_v6_requests) =
3368 client.create_proxy_and_stream::<froot::RoutesV6Marker>();
3369
3370 let connector = TestConnector {
3371 root_interfaces: Some(root_interfaces),
3372 root_routes_v4: Some(routes_v4),
3373 root_routes_v6: Some(routes_v6),
3374 ..Default::default()
3375 };
3376 let buffers = writer::TestBuffers::default();
3377 let mut out = writer::JsonWriter::new_test(None, &buffers);
3378 let op = do_route(&mut out, cmd.clone(), &connector);
3379
3380 let op_succeeds = async move {
3381 let (nic_id, control_server, _get_admin_responder) = interfaces_requests
3383 .try_next()
3384 .await
3385 .expect("get_admin FIDL error")
3386 .expect("request stream should not have ended")
3387 .into_get_admin()
3388 .expect("request should be get_admin");
3389 assert_eq!(nic_id, expected_id.get());
3390
3391 let mut control_stream = control_server.into_stream();
3392 let auth_responder = control_stream
3393 .try_next()
3394 .await
3395 .expect("get_authorization_for_interface FIDL error")
3396 .expect("request stream should not have ended")
3397 .into_get_authorization_for_interface()
3398 .expect("request should be get_authorization_for_interface");
3399
3400 let grant = fnet_resources::GrantForInterfaceAuthorization {
3401 interface_id: nic_id,
3402 token: event_token,
3403 };
3404 auth_responder.send(grant).expect("failed to send grant");
3405
3406 match cmd {
3408 opts::RouteEnum::List(_) => {
3409 panic!("test_modify_route should not take a List command")
3410 }
3411 opts::RouteEnum::Add(route) => match route.destination {
3412 std::net::IpAddr::V4(addr) => {
3413 let (route_set_server, _routes_responder) = routes_v4_requests
3414 .try_next()
3415 .await
3416 .expect("global_route_set FIDL error")
3417 .expect("request stream should not have ended")
3418 .into_global_route_set()
3419 .expect("request should be global_route_set");
3420
3421 let mut route_set_stream = route_set_server.into_stream();
3422
3423 let (proof, auth_responder) = route_set_stream
3424 .try_next()
3425 .await
3426 .expect("authenticate FIDL error")
3427 .expect("request stream should not have ended")
3428 .into_authenticate_for_interface()
3429 .expect("request should be authenticate");
3430 assert_eq!(proof.interface_id, nic_id);
3431 auth_responder.send(Ok(())).expect("failed to send authenticate response");
3432
3433 let (route_v4, add_responder) = route_set_stream
3434 .try_next()
3435 .await
3436 .expect("add_route FIDL error")
3437 .expect("request stream should not have ended")
3438 .into_add_route()
3439 .expect("request should be add_route");
3440 assert_eq!(route_v4.destination.addr.addr, addr.octets());
3441 assert_eq!(route_v4.destination.prefix_len, route.prefix_len);
3442 assert_eq!(
3443 route_v4.action,
3444 froutes::RouteActionV4::Forward(froutes::RouteTargetV4 {
3445 outbound_interface: nic_id,
3446 next_hop: None,
3447 })
3448 );
3449 add_responder.send(Ok(true)).expect("failed to send add_route response");
3450 }
3451 _ => panic!("test only mocks IPv4 currently"),
3452 },
3453 opts::RouteEnum::Del(route) => match route.destination {
3454 std::net::IpAddr::V4(addr) => {
3455 let (route_set_server, _routes_responder) = routes_v4_requests
3456 .try_next()
3457 .await
3458 .expect("global_route_set FIDL error")
3459 .expect("request stream should not have ended")
3460 .into_global_route_set()
3461 .expect("request should be global_route_set");
3462
3463 let mut route_set_stream = route_set_server.into_stream();
3464
3465 let (proof, auth_responder) = route_set_stream
3466 .try_next()
3467 .await
3468 .expect("authenticate FIDL error")
3469 .expect("request stream should not have ended")
3470 .into_authenticate_for_interface()
3471 .expect("request should be authenticate");
3472 assert_eq!(proof.interface_id, nic_id);
3473 auth_responder.send(Ok(())).expect("failed to send authenticate response");
3474
3475 let (route_v4, del_responder) = route_set_stream
3476 .try_next()
3477 .await
3478 .expect("remove_route FIDL error")
3479 .expect("request stream should not have ended")
3480 .into_remove_route()
3481 .expect("request should be remove_route");
3482 assert_eq!(route_v4.destination.addr.addr, addr.octets());
3483 assert_eq!(route_v4.destination.prefix_len, route.prefix_len);
3484 assert_eq!(
3485 route_v4.action,
3486 froutes::RouteActionV4::Forward(froutes::RouteTargetV4 {
3487 outbound_interface: nic_id,
3488 next_hop: None,
3489 })
3490 );
3491 del_responder.send(Ok(true)).expect("failed to send remove_route response");
3492 }
3493 _ => panic!("test only mocks IPv4 currently"),
3494 },
3495 }
3496 Ok(())
3497 };
3498 let ((), ()) =
3499 futures::future::try_join(op, op_succeeds).await.expect("dhcp command should succeed");
3500 }
3501
3502 #[fuchsia::test]
3503 async fn route_add() {
3504 let client = flex_local::local_client_empty();
3505
3506 test_modify_route(
3508 client,
3509 opts::RouteEnum::Add(opts::RouteAdd {
3510 destination: std::net::IpAddr::V4(std::net::Ipv4Addr::new(192, 168, 1, 0)),
3511 prefix_len: 24,
3512 gateway: None,
3513 interface: opts::InterfaceIdentifier::Id(NonZeroU64::new(2).unwrap()),
3514 metric: Some(100),
3515 }),
3516 )
3517 .await;
3518 }
3519
3520 #[fuchsia::test]
3521 async fn route_del() {
3522 let client = flex_local::local_client_empty();
3523
3524 test_modify_route(
3526 client,
3527 opts::RouteEnum::Del(opts::RouteDel {
3528 destination: std::net::IpAddr::V4(std::net::Ipv4Addr::new(192, 168, 1, 0)),
3529 prefix_len: 24,
3530 gateway: None,
3531 interface: opts::InterfaceIdentifier::Id(NonZeroU64::new(2).unwrap()),
3532 metric: Some(100),
3533 }),
3534 )
3535 .await;
3536 }
3537
3538 fn wanted_route_list_json() -> String {
3539 json!([
3540 {
3541 "destination":{"addr":"0.0.0.0","prefix_len":0},
3542 "gateway":"127.0.0.1",
3543 "metric":4,
3544 "nicid":3,
3545 "table_id":0,
3546 },
3547 {
3548 "destination":{"addr":"1.1.1.0","prefix_len":24},
3549 "gateway":"1.1.1.2",
3550 "metric":4,
3551 "nicid":3,
3552 "table_id":0,
3553 },
3554 {
3555 "destination":{"addr":"10.10.10.0","prefix_len":24},
3556 "gateway":"10.10.10.20",
3557 "metric":40,
3558 "nicid":30,
3559 "table_id":1,
3560 },
3561 {
3562 "destination":{"addr":"11.11.11.0","prefix_len":28},
3563 "gateway":null,
3564 "metric":40,
3565 "nicid":30,
3566 "table_id":1,
3567 },
3568 {
3569 "destination":{"addr":"ff00::","prefix_len":8},
3570 "gateway":null,
3571 "metric":400,
3572 "nicid":300,
3573 "table_id":2,
3574 },
3575 {
3576 "destination":{"addr":"fe80::","prefix_len":64},
3577 "gateway":null,
3578 "metric":400,
3579 "nicid":300,
3580 "table_id":2,
3581 },
3582 ])
3583 .to_string()
3584 }
3585
3586 fn wanted_route_list_tabular() -> String {
3587 "Destination Gateway NICID Metric TableId
3588 0.0.0.0/0 127.0.0.1 3 4 0
3589 1.1.1.0/24 1.1.1.2 3 4 0
3590 10.10.10.0/24 10.10.10.20 30 40 1
3591 11.11.11.0/28 - 30 40 1
3592 ff00::/8 - 300 400 2
3593 fe80::/64 - 300 400 2
3594 "
3595 .to_string()
3596 }
3597
3598 #[test_case(true, wanted_route_list_json() ; "in json format")]
3599 #[test_case(false, wanted_route_list_tabular() ; "in tabular format")]
3600 #[fuchsia::test]
3601 async fn route_list(json: bool, wanted_output: String) {
3602 let client = flex_local::local_client_empty();
3603
3604 let (routes_v4_controller, mut routes_v4_state_stream) =
3605 client.create_proxy_and_stream::<froutes::StateV4Marker>();
3606 let (routes_v6_controller, mut routes_v6_state_stream) =
3607 client.create_proxy_and_stream::<froutes::StateV6Marker>();
3608 let connector = TestConnector {
3609 routes_v4: Some(routes_v4_controller),
3610 routes_v6: Some(routes_v6_controller),
3611 ..Default::default()
3612 };
3613
3614 let buffers = writer::TestBuffers::default();
3615 let mut output = if json {
3616 writer::JsonWriter::new_test(Some(writer::Format::Json), &buffers)
3617 } else {
3618 writer::JsonWriter::new_test(None, &buffers)
3619 };
3620
3621 let do_route_fut =
3622 do_route(&mut output, opts::RouteEnum::List(opts::RouteList {}), &connector);
3623
3624 let v4_route_events = vec![
3625 froutes::EventV4::Existing(froutes::InstalledRouteV4 {
3626 route: Some(froutes::RouteV4 {
3627 destination: net_declare::fidl_ip_v4_with_prefix!("1.1.1.0/24"),
3628 action: froutes::RouteActionV4::Forward(froutes::RouteTargetV4 {
3629 outbound_interface: 3,
3630 next_hop: Some(Box::new(net_declare::fidl_ip_v4!("1.1.1.2"))),
3631 }),
3632 properties: froutes::RoutePropertiesV4 {
3633 specified_properties: Some(froutes::SpecifiedRouteProperties {
3634 metric: Some(froutes::SpecifiedMetric::ExplicitMetric(4)),
3635 ..Default::default()
3636 }),
3637 ..Default::default()
3638 },
3639 }),
3640 effective_properties: Some(froutes::EffectiveRouteProperties {
3641 metric: Some(4),
3642 ..Default::default()
3643 }),
3644 table_id: Some(0),
3645 ..Default::default()
3646 }),
3647 froutes::EventV4::Existing(froutes::InstalledRouteV4 {
3648 route: Some(froutes::RouteV4 {
3649 destination: net_declare::fidl_ip_v4_with_prefix!("10.10.10.0/24"),
3650 action: froutes::RouteActionV4::Forward(froutes::RouteTargetV4 {
3651 outbound_interface: 30,
3652 next_hop: Some(Box::new(net_declare::fidl_ip_v4!("10.10.10.20"))),
3653 }),
3654 properties: froutes::RoutePropertiesV4 {
3655 specified_properties: Some(froutes::SpecifiedRouteProperties {
3656 metric: Some(froutes::SpecifiedMetric::ExplicitMetric(40)),
3657 ..Default::default()
3658 }),
3659 ..Default::default()
3660 },
3661 }),
3662 effective_properties: Some(froutes::EffectiveRouteProperties {
3663 metric: Some(40),
3664 ..Default::default()
3665 }),
3666 table_id: Some(1),
3667 ..Default::default()
3668 }),
3669 froutes::EventV4::Existing(froutes::InstalledRouteV4 {
3670 route: Some(froutes::RouteV4 {
3671 destination: net_declare::fidl_ip_v4_with_prefix!("0.0.0.0/0"),
3672 action: froutes::RouteActionV4::Forward(froutes::RouteTargetV4 {
3673 outbound_interface: 3,
3674 next_hop: Some(Box::new(net_declare::fidl_ip_v4!("127.0.0.1"))),
3675 }),
3676 properties: froutes::RoutePropertiesV4 {
3677 specified_properties: Some(froutes::SpecifiedRouteProperties {
3678 metric: Some(froutes::SpecifiedMetric::ExplicitMetric(4)),
3679 ..Default::default()
3680 }),
3681 ..Default::default()
3682 },
3683 }),
3684 effective_properties: Some(froutes::EffectiveRouteProperties {
3685 metric: Some(4),
3686 ..Default::default()
3687 }),
3688 table_id: Some(0),
3689 ..Default::default()
3690 }),
3691 froutes::EventV4::Existing(froutes::InstalledRouteV4 {
3692 route: Some(froutes::RouteV4 {
3693 destination: net_declare::fidl_ip_v4_with_prefix!("11.11.11.0/28"),
3694 action: froutes::RouteActionV4::Forward(froutes::RouteTargetV4 {
3695 outbound_interface: 30,
3696 next_hop: None,
3697 }),
3698 properties: froutes::RoutePropertiesV4 {
3699 specified_properties: Some(froutes::SpecifiedRouteProperties {
3700 metric: Some(froutes::SpecifiedMetric::ExplicitMetric(40)),
3701 ..Default::default()
3702 }),
3703 ..Default::default()
3704 },
3705 }),
3706 effective_properties: Some(froutes::EffectiveRouteProperties {
3707 metric: Some(40),
3708 ..Default::default()
3709 }),
3710 table_id: Some(1),
3711 ..Default::default()
3712 }),
3713 froutes::EventV4::Idle(froutes::Empty),
3714 ];
3715 let v6_route_events = vec![
3716 froutes::EventV6::Existing(froutes::InstalledRouteV6 {
3717 route: Some(froutes::RouteV6 {
3718 destination: net_declare::fidl_ip_v6_with_prefix!("fe80::/64"),
3719 action: froutes::RouteActionV6::Forward(froutes::RouteTargetV6 {
3720 outbound_interface: 300,
3721 next_hop: None,
3722 }),
3723 properties: froutes::RoutePropertiesV6 {
3724 specified_properties: Some(froutes::SpecifiedRouteProperties {
3725 metric: Some(froutes::SpecifiedMetric::ExplicitMetric(400)),
3726 ..Default::default()
3727 }),
3728 ..Default::default()
3729 },
3730 }),
3731 effective_properties: Some(froutes::EffectiveRouteProperties {
3732 metric: Some(400),
3733 ..Default::default()
3734 }),
3735 table_id: Some(2),
3736 ..Default::default()
3737 }),
3738 froutes::EventV6::Existing(froutes::InstalledRouteV6 {
3739 route: Some(froutes::RouteV6 {
3740 destination: net_declare::fidl_ip_v6_with_prefix!("ff00::/8"),
3741 action: froutes::RouteActionV6::Forward(froutes::RouteTargetV6 {
3742 outbound_interface: 300,
3743 next_hop: None,
3744 }),
3745 properties: froutes::RoutePropertiesV6 {
3746 specified_properties: Some(froutes::SpecifiedRouteProperties {
3747 metric: Some(froutes::SpecifiedMetric::ExplicitMetric(400)),
3748 ..Default::default()
3749 }),
3750 ..Default::default()
3751 },
3752 }),
3753 effective_properties: Some(froutes::EffectiveRouteProperties {
3754 metric: Some(400),
3755 ..Default::default()
3756 }),
3757 table_id: Some(2),
3758 ..Default::default()
3759 }),
3760 froutes::EventV6::Idle(froutes::Empty),
3761 ];
3762
3763 let route_v4_fut = routes_v4_state_stream.select_next_some().then(|request| {
3764 froutes_ext::testutil::serve_state_request::<Ipv4>(
3765 request,
3766 futures::stream::once(futures::future::ready(v4_route_events)),
3767 )
3768 });
3769 let route_v6_fut = routes_v6_state_stream.select_next_some().then(|request| {
3770 froutes_ext::testutil::serve_state_request::<Ipv6>(
3771 request,
3772 futures::stream::once(futures::future::ready(v6_route_events)),
3773 )
3774 });
3775
3776 let ((), (), ()) =
3777 futures::try_join!(do_route_fut, route_v4_fut.map(Ok), route_v6_fut.map(Ok))
3778 .expect("listing forwarding table entries should succeed");
3779
3780 let got_output = buffers.into_stdout_str();
3781
3782 if json {
3783 let got: Value = serde_json::from_str(&got_output).unwrap();
3784 let want: Value = serde_json::from_str(&wanted_output).unwrap();
3785 pretty_assertions::assert_eq!(got, want);
3786 } else {
3787 pretty_assertions::assert_eq!(
3788 trim_whitespace_for_comparison(&got_output),
3789 trim_whitespace_for_comparison(&wanted_output),
3790 );
3791 }
3792 }
3793
3794 async fn test_get_neigh_entries(
3795 watch_for_changes: bool,
3796 batches: Vec<Vec<fneighbor::EntryIteratorItem>>,
3797 want: String,
3798 ) {
3799 let client = flex_local::local_client_empty();
3800
3801 let (it, mut requests) = client.create_proxy_and_stream::<fneighbor::EntryIteratorMarker>();
3802
3803 let server = async {
3804 for items in batches {
3805 let responder = requests
3806 .try_next()
3807 .await
3808 .expect("neigh FIDL error")
3809 .expect("request stream should not have ended")
3810 .into_get_next()
3811 .expect("request should be of type GetNext");
3812 responder.send(&items).expect("responder.send should succeed");
3813 }
3814 }
3815 .on_timeout(std::time::Duration::from_secs(60), || panic!("server responder timed out"));
3816
3817 let client = async {
3818 let mut stream = neigh_entry_stream(it, watch_for_changes);
3819
3820 let item_to_string = |item| {
3821 let buffers = writer::TestBuffers::default();
3822 let mut buf = writer::JsonWriter::new_test(None, &buffers);
3823 write_neigh_entry(&mut buf, item, watch_for_changes)
3824 .expect("write_neigh_entry should succeed");
3825 buffers.into_stdout_str()
3826 };
3827
3828 for want_line in want.lines() {
3830 let got = stream
3831 .next()
3832 .await
3833 .map(|item| item_to_string(item.expect("neigh_entry_stream should succeed")));
3834 assert_eq!(got, Some(format!("{}\n", want_line)));
3835 }
3836
3837 if !watch_for_changes {
3839 match stream.next().await {
3840 Some(Ok(item)) => {
3841 panic!("unexpected item from stream: {}", item_to_string(item))
3842 }
3843 Some(Err(err)) => panic!("unexpected error from stream: {}", err),
3844 None => {}
3845 }
3846 }
3847 };
3848
3849 let ((), ()) = futures::future::join(client, server).await;
3850 }
3851
3852 async fn test_neigh_none(watch_for_changes: bool, want: String) {
3853 test_get_neigh_entries(
3854 watch_for_changes,
3855 vec![vec![fneighbor::EntryIteratorItem::Idle(fneighbor::IdleEvent {})]],
3856 want,
3857 )
3858 .await
3859 }
3860
3861 #[fuchsia::test]
3862 async fn neigh_list_none() {
3863 test_neigh_none(false , "".to_string()).await
3864 }
3865
3866 #[fuchsia::test]
3867 async fn neigh_watch_none() {
3868 test_neigh_none(true , "IDLE".to_string()).await
3869 }
3870
3871 fn timestamp_60s_ago() -> i64 {
3872 let now = std::time::SystemTime::now()
3873 .duration_since(std::time::SystemTime::UNIX_EPOCH)
3874 .expect("failed to get duration since epoch");
3875 let past = now - std::time::Duration::from_secs(60);
3876 i64::try_from(past.as_nanos()).expect("failed to convert duration to i64")
3877 }
3878
3879 async fn test_neigh_one(watch_for_changes: bool, want: fn(fneighbor_ext::Entry) -> String) {
3880 fn new_entry(updated_at: i64) -> fneighbor::Entry {
3881 fneighbor::Entry {
3882 interface: Some(1),
3883 neighbor: Some(IF_ADDR_V4.addr),
3884 state: Some(fneighbor::EntryState::Reachable),
3885 mac: Some(MAC_1),
3886 updated_at: Some(updated_at),
3887 ..Default::default()
3888 }
3889 }
3890
3891 let updated_at = timestamp_60s_ago();
3892
3893 test_get_neigh_entries(
3894 watch_for_changes,
3895 vec![vec![
3896 fneighbor::EntryIteratorItem::Existing(new_entry(updated_at)),
3897 fneighbor::EntryIteratorItem::Idle(fneighbor::IdleEvent {}),
3898 ]],
3899 want(fneighbor_ext::Entry::try_from(new_entry(updated_at)).unwrap()),
3900 )
3901 .await
3902 }
3903
3904 #[fuchsia::test]
3905 async fn neigh_list_one() {
3906 test_neigh_one(false , |entry| format!("{}\n", entry)).await
3907 }
3908
3909 #[fuchsia::test]
3910 async fn neigh_watch_one() {
3911 test_neigh_one(true , |entry| {
3912 format!(
3913 "EXISTING | {}\n\
3914 IDLE\n",
3915 entry
3916 )
3917 })
3918 .await
3919 }
3920
3921 async fn test_neigh_many(
3922 watch_for_changes: bool,
3923 want: fn(fneighbor_ext::Entry, fneighbor_ext::Entry) -> String,
3924 ) {
3925 fn new_entry(
3926 ip: fnet::IpAddress,
3927 mac: fnet::MacAddress,
3928 updated_at: i64,
3929 ) -> fneighbor::Entry {
3930 fneighbor::Entry {
3931 interface: Some(1),
3932 neighbor: Some(ip),
3933 state: Some(fneighbor::EntryState::Reachable),
3934 mac: Some(mac),
3935 updated_at: Some(updated_at),
3936 ..Default::default()
3937 }
3938 }
3939
3940 let updated_at = timestamp_60s_ago();
3941 let offset = i64::try_from(std::time::Duration::from_secs(60).as_nanos())
3942 .expect("failed to convert duration to i64");
3943
3944 test_get_neigh_entries(
3945 watch_for_changes,
3946 vec![vec![
3947 fneighbor::EntryIteratorItem::Existing(new_entry(
3948 IF_ADDR_V4.addr,
3949 MAC_1,
3950 updated_at,
3951 )),
3952 fneighbor::EntryIteratorItem::Existing(new_entry(
3953 IF_ADDR_V6.addr,
3954 MAC_2,
3955 updated_at - offset,
3956 )),
3957 fneighbor::EntryIteratorItem::Idle(fneighbor::IdleEvent {}),
3958 ]],
3959 want(
3960 fneighbor_ext::Entry::try_from(new_entry(IF_ADDR_V4.addr, MAC_1, updated_at))
3961 .unwrap(),
3962 fneighbor_ext::Entry::try_from(new_entry(
3963 IF_ADDR_V6.addr,
3964 MAC_2,
3965 updated_at - offset,
3966 ))
3967 .unwrap(),
3968 ),
3969 )
3970 .await
3971 }
3972
3973 #[fuchsia::test]
3974 async fn neigh_list_many() {
3975 test_neigh_many(false , |a, b| format!("{}\n{}\n", a, b)).await
3976 }
3977
3978 #[fuchsia::test]
3979 async fn neigh_watch_many() {
3980 test_neigh_many(true , |a, b| {
3981 format!(
3982 "EXISTING | {}\n\
3983 EXISTING | {}\n\
3984 IDLE\n",
3985 a, b
3986 )
3987 })
3988 .await
3989 }
3990
3991 fn wanted_neigh_list_json() -> String {
3992 json!({
3993 "interface": 1,
3994 "neighbor": "192.168.0.1",
3995 "state": "REACHABLE",
3996 "mac": "01:02:03:04:05:06",
3997 })
3998 .to_string()
3999 }
4000
4001 fn wanted_neigh_watch_json() -> String {
4002 json!({
4003 "state_change_status": "EXISTING",
4004 "entry": {
4005 "interface": 1,
4006 "neighbor": "192.168.0.1",
4007 "state": "REACHABLE",
4008 "mac": "01:02:03:04:05:06",
4009 },
4010 })
4011 .to_string()
4012 }
4013
4014 #[test_case(true, false, &wanted_neigh_list_json() ; "in json format, not including entry state")]
4015 #[test_case(false, false, "Interface 1 | IP 192.168.0.1 | MAC 01:02:03:04:05:06 | REACHABLE" ; "in tabular format, not including entry state")]
4016 #[test_case(true, true, &wanted_neigh_watch_json() ; "in json format, including entry state")]
4017 #[test_case(false, true, "EXISTING | Interface 1 | IP 192.168.0.1 | MAC 01:02:03:04:05:06 | REACHABLE" ; "in tabular format, including entry state")]
4018 fn neigh_write_entry(json: bool, include_entry_state: bool, wanted_output: &str) {
4019 let entry = fneighbor::EntryIteratorItem::Existing(fneighbor::Entry {
4020 interface: Some(1),
4021 neighbor: Some(IF_ADDR_V4.addr),
4022 state: Some(fneighbor::EntryState::Reachable),
4023 mac: Some(MAC_1),
4024 updated_at: Some(timestamp_60s_ago()),
4025 ..Default::default()
4026 });
4027
4028 let buffers = writer::TestBuffers::default();
4029 let mut output = if json {
4030 writer::JsonWriter::new_test(Some(writer::Format::Json), &buffers)
4031 } else {
4032 writer::JsonWriter::new_test(None, &buffers)
4033 };
4034 write_neigh_entry(&mut output, entry, include_entry_state)
4035 .expect("write_neigh_entry should succeed");
4036 let got_output = buffers.into_stdout_str();
4037 pretty_assertions::assert_eq!(
4038 trim_whitespace_for_comparison(&got_output),
4039 trim_whitespace_for_comparison(wanted_output),
4040 );
4041 }
4042
4043 const INTERFACE_ID: u64 = 1;
4044 const IP_VERSION: fnet::IpVersion = fnet::IpVersion::V4;
4045
4046 #[fuchsia::test]
4047 async fn neigh_add() {
4048 let client = flex_local::local_client_empty();
4049
4050 let (controller, mut requests) =
4051 client.create_proxy_and_stream::<fneighbor::ControllerMarker>();
4052 let neigh = do_neigh_add(INTERFACE_ID, IF_ADDR_V4.addr, MAC_1, controller);
4053 let neigh_succeeds = async {
4054 let (got_interface_id, got_ip_address, got_mac, responder) = requests
4055 .try_next()
4056 .await
4057 .expect("neigh FIDL error")
4058 .expect("request stream should not have ended")
4059 .into_add_entry()
4060 .expect("request should be of type AddEntry");
4061 assert_eq!(got_interface_id, INTERFACE_ID);
4062 assert_eq!(got_ip_address, IF_ADDR_V4.addr);
4063 assert_eq!(got_mac, MAC_1);
4064 responder.send(Ok(())).expect("responder.send should succeed");
4065 Ok(())
4066 };
4067 let ((), ()) = futures::future::try_join(neigh, neigh_succeeds)
4068 .await
4069 .expect("neigh add should succeed");
4070 }
4071
4072 #[fuchsia::test]
4073 async fn neigh_clear() {
4074 let client = flex_local::local_client_empty();
4075
4076 let (controller, mut requests) =
4077 client.create_proxy_and_stream::<fneighbor::ControllerMarker>();
4078 let neigh = do_neigh_clear(INTERFACE_ID, IP_VERSION, controller);
4079 let neigh_succeeds = async {
4080 let (got_interface_id, got_ip_version, responder) = requests
4081 .try_next()
4082 .await
4083 .expect("neigh FIDL error")
4084 .expect("request stream should not have ended")
4085 .into_clear_entries()
4086 .expect("request should be of type ClearEntries");
4087 assert_eq!(got_interface_id, INTERFACE_ID);
4088 assert_eq!(got_ip_version, IP_VERSION);
4089 responder.send(Ok(())).expect("responder.send should succeed");
4090 Ok(())
4091 };
4092 let ((), ()) = futures::future::try_join(neigh, neigh_succeeds)
4093 .await
4094 .expect("neigh clear should succeed");
4095 }
4096
4097 #[fuchsia::test]
4098 async fn neigh_del() {
4099 let client = flex_local::local_client_empty();
4100
4101 let (controller, mut requests) =
4102 client.create_proxy_and_stream::<fneighbor::ControllerMarker>();
4103 let neigh = do_neigh_del(INTERFACE_ID, IF_ADDR_V4.addr, controller);
4104 let neigh_succeeds = async {
4105 let (got_interface_id, got_ip_address, responder) = requests
4106 .try_next()
4107 .await
4108 .expect("neigh FIDL error")
4109 .expect("request stream should not have ended")
4110 .into_remove_entry()
4111 .expect("request should be of type RemoveEntry");
4112 assert_eq!(got_interface_id, INTERFACE_ID);
4113 assert_eq!(got_ip_address, IF_ADDR_V4.addr);
4114 responder.send(Ok(())).expect("responder.send should succeed");
4115 Ok(())
4116 };
4117 let ((), ()) = futures::future::try_join(neigh, neigh_succeeds)
4118 .await
4119 .expect("neigh remove should succeed");
4120 }
4121
4122 #[test_case(opts::dhcpd::DhcpdEnum::Get(opts::dhcpd::Get {
4123 arg: opts::dhcpd::GetArg::Option(
4124 opts::dhcpd::OptionArg {
4125 name: opts::dhcpd::Option_::SubnetMask(
4126 opts::dhcpd::SubnetMask { mask: None }) }),
4127 }); "get option")]
4128 #[test_case(opts::dhcpd::DhcpdEnum::Get(opts::dhcpd::Get {
4129 arg: opts::dhcpd::GetArg::Parameter(opts::dhcpd::ParameterArg {
4130 name: opts::dhcpd::Parameter::LeaseLength(
4131 opts::dhcpd::LeaseLength { default: None, max: None }),
4132 }),
4133 }); "get parameter")]
4134 #[test_case(opts::dhcpd::DhcpdEnum::Set(opts::dhcpd::Set {
4135 arg: opts::dhcpd::SetArg::Option(opts::dhcpd::OptionArg {
4136 name: opts::dhcpd::Option_::SubnetMask(opts::dhcpd::SubnetMask {
4137 mask: Some(net_declare::std_ip_v4!("255.255.255.0")),
4138 }),
4139 }),
4140 }); "set option")]
4141 #[test_case(opts::dhcpd::DhcpdEnum::Set(opts::dhcpd::Set {
4142 arg: opts::dhcpd::SetArg::Parameter(opts::dhcpd::ParameterArg {
4143 name: opts::dhcpd::Parameter::LeaseLength(
4144 opts::dhcpd::LeaseLength { max: Some(42), default: Some(42) }),
4145 }),
4146 }); "set parameter")]
4147 #[test_case(opts::dhcpd::DhcpdEnum::List(opts::dhcpd::List { arg:
4148 opts::dhcpd::ListArg::Option(opts::dhcpd::OptionToken {}) }); "list option")]
4149 #[test_case(opts::dhcpd::DhcpdEnum::List(
4150 opts::dhcpd::List { arg: opts::dhcpd::ListArg::Parameter(opts::dhcpd::ParameterToken {}) });
4151 "list parameter")]
4152 #[test_case(opts::dhcpd::DhcpdEnum::Reset(opts::dhcpd::Reset {
4153 arg: opts::dhcpd::ResetArg::Option(opts::dhcpd::OptionToken {}) }); "reset option")]
4154 #[test_case(opts::dhcpd::DhcpdEnum::Reset(
4155 opts::dhcpd::Reset {
4156 arg: opts::dhcpd::ResetArg::Parameter(opts::dhcpd::ParameterToken {}) });
4157 "reset parameter")]
4158 #[test_case(opts::dhcpd::DhcpdEnum::ClearLeases(opts::dhcpd::ClearLeases {}); "clear leases")]
4159 #[test_case(opts::dhcpd::DhcpdEnum::Start(opts::dhcpd::Start {}); "start")]
4160 #[test_case(opts::dhcpd::DhcpdEnum::Stop(opts::dhcpd::Stop {}); "stop")]
4161 #[fuchsia::test]
4162 async fn test_do_dhcpd(cmd: opts::dhcpd::DhcpdEnum) {
4163 let client = flex_local::local_client_empty();
4164
4165 let (dhcpd, mut requests) = client.create_proxy_and_stream::<fdhcp::Server_Marker>();
4166
4167 let connector = TestConnector { dhcpd: Some(dhcpd), ..Default::default() };
4168 let op = do_dhcpd(cmd.clone(), &connector);
4169 let op_succeeds = async move {
4170 let req = requests
4171 .try_next()
4172 .await
4173 .expect("receiving request")
4174 .expect("request stream should not have ended");
4175 match cmd {
4176 opts::dhcpd::DhcpdEnum::Get(opts::dhcpd::Get { arg }) => match arg {
4177 opts::dhcpd::GetArg::Option(opts::dhcpd::OptionArg { name }) => {
4178 let (code, responder) =
4179 req.into_get_option().expect("request should be of type get option");
4180 assert_eq!(
4181 <opts::dhcpd::Option_ as Into<fdhcp::OptionCode>>::into(name),
4182 code
4183 );
4184 let dummy_result = fdhcp::Option_::SubnetMask(fidl_ip_v4!("255.255.255.0"));
4187 responder.send(Ok(&dummy_result)).expect("responder.send should succeed");
4188 Ok(())
4189 }
4190 opts::dhcpd::GetArg::Parameter(opts::dhcpd::ParameterArg { name }) => {
4191 let (param, responder) = req
4192 .into_get_parameter()
4193 .expect("request should be of type get parameter");
4194 assert_eq!(
4195 <opts::dhcpd::Parameter as Into<fdhcp::ParameterName>>::into(name),
4196 param
4197 );
4198 let dummy_result = fdhcp::Parameter::Lease(fdhcp::LeaseLength::default());
4201 responder.send(Ok(&dummy_result)).expect("responder.send should succeed");
4202 Ok(())
4203 }
4204 },
4205 opts::dhcpd::DhcpdEnum::Set(opts::dhcpd::Set { arg }) => match arg {
4206 opts::dhcpd::SetArg::Option(opts::dhcpd::OptionArg { name }) => {
4207 let (opt, responder) =
4208 req.into_set_option().expect("request should be of type set option");
4209 assert_eq!(<opts::dhcpd::Option_ as Into<fdhcp::Option_>>::into(name), opt);
4210 responder.send(Ok(())).expect("responder.send should succeed");
4211 Ok(())
4212 }
4213 opts::dhcpd::SetArg::Parameter(opts::dhcpd::ParameterArg { name }) => {
4214 let (opt, responder) = req
4215 .into_set_parameter()
4216 .expect("request should be of type set parameter");
4217 assert_eq!(
4218 <opts::dhcpd::Parameter as Into<fdhcp::Parameter>>::into(name),
4219 opt
4220 );
4221 responder.send(Ok(())).expect("responder.send should succeed");
4222 Ok(())
4223 }
4224 },
4225 opts::dhcpd::DhcpdEnum::List(opts::dhcpd::List { arg }) => match arg {
4226 opts::dhcpd::ListArg::Option(opts::dhcpd::OptionToken {}) => {
4227 let responder = req
4228 .into_list_options()
4229 .expect("request should be of type list options");
4230 responder.send(Ok(&[])).expect("responder.send should succeed");
4231 Ok(())
4232 }
4233 opts::dhcpd::ListArg::Parameter(opts::dhcpd::ParameterToken {}) => {
4234 let responder = req
4235 .into_list_parameters()
4236 .expect("request should be of type list options");
4237 responder.send(Ok(&[])).expect("responder.send should succeed");
4238 Ok(())
4239 }
4240 },
4241 opts::dhcpd::DhcpdEnum::Reset(opts::dhcpd::Reset { arg }) => match arg {
4242 opts::dhcpd::ResetArg::Option(opts::dhcpd::OptionToken {}) => {
4243 let responder = req
4244 .into_reset_options()
4245 .expect("request should be of type reset options");
4246 responder.send(Ok(())).expect("responder.send should succeed");
4247 Ok(())
4248 }
4249 opts::dhcpd::ResetArg::Parameter(opts::dhcpd::ParameterToken {}) => {
4250 let responder = req
4251 .into_reset_parameters()
4252 .expect("request should be of type reset parameters");
4253 responder.send(Ok(())).expect("responder.send should succeed");
4254 Ok(())
4255 }
4256 },
4257 opts::dhcpd::DhcpdEnum::ClearLeases(opts::dhcpd::ClearLeases {}) => {
4258 let responder =
4259 req.into_clear_leases().expect("request should be of type clear leases");
4260 responder.send(Ok(())).expect("responder.send should succeed");
4261 Ok(())
4262 }
4263 opts::dhcpd::DhcpdEnum::Start(opts::dhcpd::Start {}) => {
4264 let responder =
4265 req.into_start_serving().expect("request should be of type start serving");
4266 responder.send(Ok(())).expect("responder.send should succeed");
4267 Ok(())
4268 }
4269 opts::dhcpd::DhcpdEnum::Stop(opts::dhcpd::Stop {}) => {
4270 let responder =
4271 req.into_stop_serving().expect("request should be of type stop serving");
4272 responder.send().expect("responder.send should succeed");
4273 Ok(())
4274 }
4275 }
4276 };
4277 let ((), ()) = futures::future::try_join(op, op_succeeds)
4278 .await
4279 .expect("dhcp server command should succeed");
4280 }
4281
4282 #[fuchsia::test]
4283 async fn dns_lookup() {
4284 let client = flex_local::local_client_empty();
4285
4286 let (lookup, mut requests) = client.create_proxy_and_stream::<fname::LookupMarker>();
4287 let connector = TestConnector { name_lookup: Some(lookup), ..Default::default() };
4288
4289 let cmd = opts::dns::DnsEnum::Lookup(opts::dns::Lookup {
4290 hostname: "example.com".to_string(),
4291 ipv4: true,
4292 ipv6: true,
4293 sort: true,
4294 });
4295 let mut output = Vec::new();
4296 let dns_command = do_dns(&mut output, cmd.clone(), &connector)
4297 .map(|result| result.expect("dns command should succeed"));
4298
4299 let handle_request = async move {
4300 let (hostname, options, responder) = requests
4301 .try_next()
4302 .await
4303 .expect("FIDL error")
4304 .expect("request stream should not have ended")
4305 .into_lookup_ip()
4306 .expect("request should be of type LookupIp");
4307 let opts::dns::DnsEnum::Lookup(opts::dns::Lookup {
4308 hostname: want_hostname,
4309 ipv4,
4310 ipv6,
4311 sort,
4312 }) = cmd;
4313 let want_options = fname::LookupIpOptions {
4314 ipv4_lookup: Some(ipv4),
4315 ipv6_lookup: Some(ipv6),
4316 sort_addresses: Some(sort),
4317 ..Default::default()
4318 };
4319 assert_eq!(
4320 hostname, want_hostname,
4321 "received IP lookup request for unexpected hostname"
4322 );
4323 assert_eq!(options, want_options, "received unexpected IP lookup options");
4324
4325 responder
4326 .send(Ok(&fname::LookupResult {
4327 addresses: Some(vec![fidl_ip!("203.0.113.1"), fidl_ip!("2001:db8::1")]),
4328 ..Default::default()
4329 }))
4330 .expect("send response");
4331 };
4332 let ((), ()) = futures::future::join(dns_command, handle_request).await;
4333
4334 const WANT_OUTPUT: &str = "
4335203.0.113.1
43362001:db8::1
4337";
4338 let got_output = std::str::from_utf8(&output).unwrap();
4339 pretty_assertions::assert_eq!(
4340 trim_whitespace_for_comparison(got_output),
4341 trim_whitespace_for_comparison(WANT_OUTPUT),
4342 );
4343 }
4344}