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