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