Skip to main content

net_cli/
lib.rs

1// Copyright 2018 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5mod 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/// An interface for acquiring a proxy to a FIDL service.
72#[async_trait::async_trait]
73pub trait ServiceConnector<S: flex_client::fidl::ProtocolMarker> {
74    /// Acquires a proxy to the parameterized FIDL interface.
75    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
86/// An interface for acquiring all system dependencies required by net-cli.
87///
88/// FIDL dependencies are specified as supertraits. These supertraits are a complete enumeration of
89/// all FIDL dependencies required by net-cli.
90pub 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                    // TODO(https://fxbug.dev/368806554): Print these with the same names used when
826                    // setting each property.
827                    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    // TODO(https://fxbug.dev/368806554): Print the returned configuration
926    // struct to give feedback to user about which parameters changed.
927    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        // TODO(https://fxbug.dev/342413894): Populate the table name.
1225        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 /* watch_for_changes */, 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 /* watch_for_changes */, 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, /* include_entry_state= */ 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                                /* include_entry_state= */ 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                        /* include_entry_state= */ 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 /* use_ifname */),
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 /* use_ifname */),
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        // net-cli does not check the returned configuration so we do not
2394        // return a populated one.
2395        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 /* use_ifname */),
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 /* use_ifname */),
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 /* use_ifname */),
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 /* use_ifname */),
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 /* use_ifname */),
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        // Make the first request.
2773        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, // The prefix should be set to the default of 32 for IPv4.
2780                }),
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        // Make the second request.
2814        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            // The caller will have failed to find an interface matching the name,
2829            // so we don't expect any requests to make it to us.
2830            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            // Handle get_interface_authorization_proof.
3382            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            // Handle RouteSet API calls.
3407            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 arguments have been arbitrarily selected.
3507        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 arguments have been arbitrarily selected.
3525        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            // Check each string sent by get_neigh_entries
3829            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            // When listing entries, the sender should close after sending all existing entries.
3838            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 /* watch_for_changes */, "".to_string()).await
3864    }
3865
3866    #[fuchsia::test]
3867    async fn neigh_watch_none() {
3868        test_neigh_none(true /* watch_for_changes */, "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 /* watch_for_changes */, |entry| format!("{}\n", entry)).await
3907    }
3908
3909    #[fuchsia::test]
3910    async fn neigh_watch_one() {
3911        test_neigh_one(true /* watch_for_changes */, |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 /* watch_for_changes */, |a, b| format!("{}\n{}\n", a, b)).await
3976    }
3977
3978    #[fuchsia::test]
3979    async fn neigh_watch_many() {
3980        test_neigh_many(true /* watch_for_changes */, |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                        // We don't care what the value is here, we just need something to give as
4185                        // an argument to responder.send().
4186                        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                        // We don't care what the value is here, we just need something to give as
4199                        // an argument to responder.send().
4200                        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}