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