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