Skip to main content

net_cli/opts/
mod.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
5use argh::{ArgsInfo, FromArgs};
6use fidl_fuchsia_net_ext as fnet_ext;
7#[cfg(not(feature = "fdomain"))]
8use fidl_fuchsia_net_interfaces_ext as finterfaces_ext;
9#[cfg(feature = "fdomain")]
10use fidl_fuchsia_net_interfaces_ext_fdomain as finterfaces_ext;
11use flex_fuchsia_net as fnet;
12use flex_fuchsia_net_interfaces as finterfaces;
13use flex_fuchsia_net_interfaces_admin as finterfaces_admin;
14use std::collections::HashMap;
15use std::convert::TryInto as _;
16use std::num::NonZeroU64;
17
18pub(crate) mod capture;
19pub(crate) mod dhcpd;
20pub(crate) mod dns;
21pub(crate) mod filter;
22
23#[derive(thiserror::Error, Clone, Debug)]
24#[error("{msg}")]
25pub struct UserFacingError {
26    pub msg: String,
27}
28
29pub fn user_facing_error(cause: impl Into<String>) -> anyhow::Error {
30    UserFacingError { msg: cause.into() }.into()
31}
32
33pub fn underlying_user_facing_error(error: &anyhow::Error) -> Option<UserFacingError> {
34    error.root_cause().downcast_ref::<UserFacingError>().cloned()
35}
36
37fn parse_ip_version_str(value: &str) -> Result<fnet::IpVersion, String> {
38    match &value.to_lowercase()[..] {
39        "ipv4" => Ok(fnet::IpVersion::V4),
40        "ipv6" => Ok(fnet::IpVersion::V6),
41        _ => Err("invalid IP version".to_string()),
42    }
43}
44
45#[derive(ArgsInfo, FromArgs, Debug)]
46/// commands for net-cli
47pub struct Command {
48    #[argh(subcommand)]
49    pub cmd: CommandEnum,
50}
51
52#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
53#[argh(subcommand)]
54pub enum CommandEnum {
55    Capture(capture::Capture),
56    Filter(filter::Filter),
57    If(If),
58    Log(Log),
59    Neigh(Neigh),
60    Route(Route),
61    Rule(Rule),
62    Dhcp(Dhcp),
63    Dhcpd(dhcpd::Dhcpd),
64    Dns(dns::Dns),
65}
66
67#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
68#[argh(subcommand, name = "if")]
69/// commands for network interfaces
70pub struct If {
71    #[argh(subcommand)]
72    pub if_cmd: IfEnum,
73}
74
75#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
76#[argh(subcommand)]
77pub enum IfEnum {
78    Addr(IfAddr),
79    Config(IfConfig),
80    Disable(IfDisable),
81    Enable(IfEnable),
82    Get(IfGet),
83    Igmp(IfIgmp),
84    IpForward(IfIpForward),
85    List(IfList),
86    Mld(IfMld),
87    Add(IfAdd),
88    Remove(IfRemove),
89}
90
91#[derive(Clone, Debug, PartialEq)]
92pub enum InterfaceIdentifier {
93    Id(NonZeroU64),
94    Name(String),
95}
96
97impl InterfaceIdentifier {
98    pub async fn find_nicid<C>(&self, connector: &C) -> Result<NonZeroU64, anyhow::Error>
99    where
100        C: crate::ServiceConnector<finterfaces::StateMarker>,
101    {
102        match self {
103            Self::Id(id) => Ok(*id),
104            Self::Name(name) => {
105                let interfaces_state = crate::connect_with_context(connector).await?;
106                let stream = finterfaces_ext::event_stream_from_state::<
107                    finterfaces_ext::AllInterest,
108                >(&interfaces_state, Default::default())?;
109                let response = finterfaces_ext::existing(
110                    stream,
111                    HashMap::<NonZeroU64, finterfaces_ext::PropertiesAndState<(), _>>::new(),
112                )
113                .await?;
114                response
115                    .values()
116                    .find_map(|properties_and_state| {
117                        (&properties_and_state.properties.name == name)
118                            .then_some(properties_and_state.properties.id)
119                    })
120                    .ok_or_else(|| user_facing_error(format!("No interface with name {}", name)))
121            }
122        }
123    }
124}
125
126impl core::str::FromStr for InterfaceIdentifier {
127    type Err = anyhow::Error;
128
129    fn from_str(s: &str) -> Result<Self, Self::Err> {
130        let nicid_parse_result = s.parse::<NonZeroU64>();
131        nicid_parse_result.map_or_else(
132            |nicid_parse_error| {
133                if !s.starts_with("name:") {
134                    Err(user_facing_error(format!(
135                        "Failed to parse as NICID (error: {}) or as interface name \
136                        (error: interface names must be specified as `name:ifname`, where \
137                        ifname is the actual interface name in this example)",
138                        nicid_parse_error
139                    )))
140                } else {
141                    Ok(Self::Name(s["name:".len()..].to_string()))
142                }
143            },
144            |nicid| Ok(Self::Id(nicid)),
145        )
146    }
147}
148
149impl From<NonZeroU64> for InterfaceIdentifier {
150    fn from(nicid: NonZeroU64) -> InterfaceIdentifier {
151        InterfaceIdentifier::Id(nicid)
152    }
153}
154
155#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
156#[argh(subcommand, name = "addr")]
157/// commands for updating network interface addresses
158pub struct IfAddr {
159    #[argh(subcommand)]
160    pub addr_cmd: IfAddrEnum,
161}
162
163#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
164#[argh(subcommand)]
165pub enum IfAddrEnum {
166    Add(IfAddrAdd),
167    Del(IfAddrDel),
168    Wait(IfAddrWait),
169}
170
171#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
172#[argh(subcommand, name = "add")]
173/// adds an address to the network interface
174pub struct IfAddrAdd {
175    #[argh(positional, arg_name = "nicid or name:ifname")]
176    pub interface: InterfaceIdentifier,
177    #[argh(positional)]
178    pub addr: String,
179    #[argh(positional, from_str_fn(parse_netmask_or_prefix_length))]
180    pub prefix: u8,
181    #[argh(switch)]
182    /// skip adding a local subnet route for this interface and address
183    pub no_subnet_route: bool,
184}
185
186#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
187#[argh(subcommand, name = "del")]
188/// deletes an address from the network interface
189pub struct IfAddrDel {
190    #[argh(positional, arg_name = "nicid or name:ifname")]
191    pub interface: InterfaceIdentifier,
192    #[argh(positional)]
193    pub addr: String,
194    #[argh(positional, from_str_fn(parse_netmask_or_prefix_length))]
195    pub prefix: Option<u8>,
196}
197
198#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
199#[argh(subcommand, name = "wait")]
200/// waits for an address to be assigned on the network interface.
201///
202/// by default waits for any address; if --ipv6 is specified, waits for an IPv6
203/// address.
204pub struct IfAddrWait {
205    #[argh(positional, arg_name = "nicid or name:ifname")]
206    pub interface: InterfaceIdentifier,
207    /// wait for an IPv6 address
208    #[argh(switch)]
209    pub ipv6: bool,
210}
211
212#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
213#[argh(subcommand, name = "disable")]
214/// disables a network interface
215pub struct IfDisable {
216    #[argh(positional, arg_name = "nicid or name:ifname")]
217    pub interface: InterfaceIdentifier,
218}
219
220#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
221#[argh(subcommand, name = "enable")]
222/// enables a network interface
223pub struct IfEnable {
224    #[argh(positional, arg_name = "nicid or name:ifname")]
225    pub interface: InterfaceIdentifier,
226}
227
228#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
229#[argh(subcommand, name = "get")]
230/// queries a network interface
231pub struct IfGet {
232    #[argh(positional, arg_name = "nicid or name:ifname")]
233    pub interface: InterfaceIdentifier,
234}
235
236// TODO(https://fxbug.dev/371584272): Replace this with if config.
237#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
238#[argh(subcommand, name = "igmp")]
239/// get or set IGMP configuration
240pub struct IfIgmp {
241    #[argh(subcommand)]
242    pub cmd: IfIgmpEnum,
243}
244
245// TODO(https://fxbug.dev/371584272): Replace this with if config.
246#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
247#[argh(subcommand)]
248pub enum IfIgmpEnum {
249    Get(IfIgmpGet),
250    Set(IfIgmpSet),
251}
252
253// TODO(https://fxbug.dev/371584272): Replace this with if config get.
254#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
255#[argh(subcommand, name = "get")]
256/// get IGMP configuration for an interface
257pub struct IfIgmpGet {
258    #[argh(positional, arg_name = "nicid or name:ifname")]
259    pub interface: InterfaceIdentifier,
260}
261
262// TODO(https://fxbug.dev/371584272): Replace this with if config set.
263#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
264#[argh(subcommand, name = "set")]
265/// set IGMP configuration for an interface
266pub struct IfIgmpSet {
267    #[argh(positional, arg_name = "nicid or name:ifname")]
268    pub interface: InterfaceIdentifier,
269
270    /// the version of IGMP to perform.
271    #[argh(option, from_str_fn(parse_igmp_version))]
272    pub version: Option<finterfaces_admin::IgmpVersion>,
273}
274
275fn parse_igmp_version(s: &str) -> Result<finterfaces_admin::IgmpVersion, String> {
276    match s.parse::<u8>() {
277        Err(err) => Err(format!("Failed to parse IGMP version (error: {})", err)),
278        Ok(v) => match v {
279            1 => Ok(finterfaces_admin::IgmpVersion::V1),
280            2 => Ok(finterfaces_admin::IgmpVersion::V2),
281            3 => Ok(finterfaces_admin::IgmpVersion::V3),
282            v => Err(format!("Invalid IGMP version ({}). Valid values: 1, 2, 3", v)),
283        },
284    }
285}
286
287// TODO(https://fxbug.dev/371584272): Replace this with if config.
288#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
289#[argh(subcommand, name = "ip-forward")]
290/// get or set IP forwarding for an interface
291pub struct IfIpForward {
292    #[argh(subcommand)]
293    pub cmd: IfIpForwardEnum,
294}
295
296// TODO(https://fxbug.dev/371584272): Replace this with if config.
297#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
298#[argh(subcommand)]
299pub enum IfIpForwardEnum {
300    Get(IfIpForwardGet),
301    Set(IfIpForwardSet),
302}
303
304// TODO(https://fxbug.dev/371584272): Replace this with if config get.
305#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
306#[argh(subcommand, name = "get")]
307/// get IP forwarding for an interface
308pub struct IfIpForwardGet {
309    #[argh(positional, arg_name = "nicid or name:ifname")]
310    pub interface: InterfaceIdentifier,
311
312    #[argh(positional, from_str_fn(parse_ip_version_str))]
313    pub ip_version: fnet::IpVersion,
314}
315
316// TODO(https://fxbug.dev/371584272): Replace this with if config set.
317#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
318#[argh(subcommand, name = "set")]
319/// set IP forwarding for an interface
320pub struct IfIpForwardSet {
321    #[argh(positional, arg_name = "nicid or name:ifname")]
322    pub interface: InterfaceIdentifier,
323
324    #[argh(positional, from_str_fn(parse_ip_version_str))]
325    pub ip_version: fnet::IpVersion,
326
327    #[argh(positional)]
328    pub enable: bool,
329}
330
331#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
332#[argh(subcommand, name = "list")]
333/// lists network interfaces (supports ffx machine output)
334pub struct IfList {
335    #[argh(positional)]
336    pub name_pattern: Option<String>,
337}
338
339#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
340#[argh(subcommand, name = "add")]
341/// add interfaces
342pub struct IfAdd {
343    #[argh(subcommand)]
344    pub cmd: IfAddEnum,
345}
346
347#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
348#[argh(subcommand)]
349pub enum IfAddEnum {
350    Blackhole(IfBlackholeAdd),
351}
352
353#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
354#[argh(subcommand, name = "blackhole")]
355/// add a blackhole interface
356pub struct IfBlackholeAdd {
357    #[argh(positional)]
358    pub name: String,
359}
360
361#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
362#[argh(subcommand, name = "remove")]
363/// remove interfaces
364pub struct IfRemove {
365    #[argh(positional, arg_name = "nicid or name:ifname")]
366    pub interface: InterfaceIdentifier,
367}
368
369// TODO(https://fxbug.dev/371584272): Replace this with if config.
370#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
371#[argh(subcommand, name = "mld")]
372/// get or set MLD configuration
373pub struct IfMld {
374    #[argh(subcommand)]
375    pub cmd: IfMldEnum,
376}
377
378// TODO(https://fxbug.dev/371584272): Replace this with if config.
379#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
380#[argh(subcommand)]
381pub enum IfMldEnum {
382    Get(IfMldGet),
383    Set(IfMldSet),
384}
385
386// TODO(https://fxbug.dev/371584272): Replace this with if config get.
387#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
388#[argh(subcommand, name = "get")]
389/// get MLD configuration for an interface
390pub struct IfMldGet {
391    #[argh(positional, arg_name = "nicid or name:ifname")]
392    pub interface: InterfaceIdentifier,
393}
394
395// TODO(https://fxbug.dev/371584272): Replace this with if config set.
396#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
397#[argh(subcommand, name = "set")]
398/// set MLD configuration for an interface
399pub struct IfMldSet {
400    #[argh(positional, arg_name = "nicid or name:ifname")]
401    pub interface: InterfaceIdentifier,
402
403    /// the version of MLD to perform.
404    #[argh(option, from_str_fn(parse_mld_version))]
405    pub version: Option<finterfaces_admin::MldVersion>,
406}
407
408fn parse_mld_version(s: &str) -> Result<finterfaces_admin::MldVersion, String> {
409    match s.parse::<u8>() {
410        Err(err) => Err(format!("Failed to parse MLD version (error: {})", err)),
411        Ok(v) => match v {
412            1 => Ok(finterfaces_admin::MldVersion::V1),
413            2 => Ok(finterfaces_admin::MldVersion::V2),
414            v => Err(format!("Invalid MLD version ({}). Valid values: 1, 2", v)),
415        },
416    }
417}
418
419#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
420#[argh(subcommand, name = "config")]
421/// get or set interface configuration
422pub struct IfConfig {
423    #[argh(positional, arg_name = "nicid or name:ifname")]
424    pub interface: InterfaceIdentifier,
425
426    #[argh(subcommand)]
427    pub cmd: IfConfigEnum,
428}
429
430#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
431#[argh(subcommand)]
432/// get configuration for an interface
433pub enum IfConfigEnum {
434    Set(IfConfigSet),
435    Get(IfConfigGet),
436}
437
438#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
439#[argh(subcommand, name = "set")]
440// TODO(https://fxbug.dev/370850762): Generate the help string programmatically
441// so that it can live next to the parsing logic.
442/** set interface configuration
443
444Configuration parameters and the values to be set to should be passed
445in pairs. The names of the configuration parameters are taken from
446the structure of fuchsia.net.interfaces.admin/Configuration.
447
448The list of supported parameters are:
449  ipv6.ndp.slaac.temporary_address_enabled
450    bool
451    Whether temporary addresses should be generated.
452  ipv6.ndp.dad.transmits
453    u16
454    Number of DAD transmissions before an address is considered available for use.
455  ipv6.ndp.router_solicitations.max
456    u8
457    Maximum number of Router Solicitations to send.
458*/
459pub struct IfConfigSet {
460    /// the config parameter names and the
461    #[argh(positional)]
462    pub options: Vec<String>,
463}
464
465#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
466#[argh(subcommand, name = "get")]
467/// get interface configuration
468pub struct IfConfigGet {}
469
470#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
471#[argh(subcommand, name = "log")]
472/// commands for logging
473pub struct Log {
474    #[argh(subcommand)]
475    pub log_cmd: LogEnum,
476}
477
478#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
479#[argh(subcommand)]
480pub enum LogEnum {
481    SetPackets(LogSetPackets),
482}
483
484#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
485#[argh(subcommand, name = "set-packets")]
486/// log packets to stdout
487pub struct LogSetPackets {
488    #[argh(positional)]
489    pub enabled: bool,
490}
491
492#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
493#[argh(subcommand, name = "neigh")]
494/// commands for neighbor tables
495pub struct Neigh {
496    #[argh(subcommand)]
497    pub neigh_cmd: NeighEnum,
498}
499
500#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
501#[argh(subcommand)]
502pub enum NeighEnum {
503    Add(NeighAdd),
504    Clear(NeighClear),
505    Del(NeighDel),
506    List(NeighList),
507    Watch(NeighWatch),
508    Config(NeighConfig),
509}
510
511#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
512#[argh(subcommand, name = "add")]
513/// adds an entry to the neighbor table
514pub struct NeighAdd {
515    #[argh(positional, arg_name = "nicid or name:ifname")]
516    pub interface: InterfaceIdentifier,
517    #[argh(positional)]
518    pub ip: fnet_ext::IpAddress,
519    #[argh(positional)]
520    pub mac: fnet_ext::MacAddress,
521}
522
523#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
524#[argh(subcommand, name = "clear")]
525/// removes all entries associated with a network interface from the neighbor table
526pub struct NeighClear {
527    #[argh(positional, arg_name = "nicid or name:ifname")]
528    pub interface: InterfaceIdentifier,
529
530    #[argh(positional, from_str_fn(parse_ip_version_str))]
531    pub ip_version: fnet::IpVersion,
532}
533
534#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
535#[argh(subcommand, name = "list")]
536/// lists neighbor table entries (supports ffx machine output)
537pub struct NeighList {}
538
539#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
540#[argh(subcommand, name = "del")]
541/// removes an entry from the neighbor table
542pub struct NeighDel {
543    #[argh(positional, arg_name = "nicid or name:ifname")]
544    pub interface: InterfaceIdentifier,
545    #[argh(positional)]
546    pub ip: fnet_ext::IpAddress,
547}
548
549#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
550#[argh(subcommand, name = "watch")]
551/// watches neighbor table entries for state changes (supports ffx machine output)
552pub struct NeighWatch {}
553
554#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
555#[argh(subcommand, name = "config")]
556/// commands for the Neighbor Unreachability Detection configuration
557pub struct NeighConfig {
558    #[argh(subcommand)]
559    pub neigh_config_cmd: NeighConfigEnum,
560}
561
562#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
563#[argh(subcommand)]
564pub enum NeighConfigEnum {
565    Get(NeighGetConfig),
566    Update(NeighUpdateConfig),
567}
568
569#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
570#[argh(subcommand, name = "get")]
571/// returns the current NUD configuration options for the provided interface
572pub struct NeighGetConfig {
573    #[argh(positional, arg_name = "nicid or name:ifname")]
574    pub interface: InterfaceIdentifier,
575
576    #[argh(positional, from_str_fn(parse_ip_version_str))]
577    pub ip_version: fnet::IpVersion,
578}
579
580#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
581#[argh(subcommand, name = "update")]
582/// updates the current NUD configuration options for the provided interface
583pub struct NeighUpdateConfig {
584    #[argh(positional, arg_name = "nicid or name:ifname")]
585    pub interface: InterfaceIdentifier,
586
587    #[argh(positional, from_str_fn(parse_ip_version_str))]
588    pub ip_version: fnet::IpVersion,
589
590    /// a base duration, in nanoseconds, for computing the random reachable
591    /// time
592    #[argh(option)]
593    pub base_reachable_time: Option<i64>,
594}
595
596#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
597#[argh(subcommand, name = "route")]
598/// commands for routing tables
599pub struct Route {
600    #[argh(subcommand)]
601    pub route_cmd: RouteEnum,
602}
603
604#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
605#[argh(subcommand)]
606pub enum RouteEnum {
607    List(RouteList),
608    Add(RouteAdd),
609    Del(RouteDel),
610}
611
612fn parse_netmask_or_prefix_length(s: &str) -> Result<u8, String> {
613    let netmask_parse_result = s.parse::<std::net::IpAddr>();
614    let prefix_len_parse_result = s.parse::<u8>();
615    match (netmask_parse_result, prefix_len_parse_result) {
616        (Err(netmask_parse_error), Err(prefix_len_parse_error)) => Err(format!(
617            "Failed to parse as netmask (error: {}) or prefix length (error: {})",
618            netmask_parse_error, prefix_len_parse_error,
619        )),
620        (Ok(_), Ok(_)) => Err(format!(
621            "Input parses both as netmask and as prefix length. This should never happen."
622        )),
623        (Ok(netmask), Err(_)) => Ok(subnet_mask_to_prefix_length(netmask)),
624        (Err(_), Ok(prefix_len)) => Ok(prefix_len),
625    }
626}
627
628fn subnet_mask_to_prefix_length(addr: std::net::IpAddr) -> u8 {
629    match addr {
630        std::net::IpAddr::V4(addr) => (!u32::from_be_bytes(addr.octets())).leading_zeros(),
631        std::net::IpAddr::V6(addr) => (!u128::from_be_bytes(addr.octets())).leading_zeros(),
632    }
633    .try_into()
634    .unwrap()
635}
636
637#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
638#[argh(subcommand, name = "list")]
639/// lists routes (supports ffx machine output)
640pub struct RouteList {}
641
642macro_rules! route_struct {
643    ($ty_name:ident, $name:literal, $comment:expr) => {
644        #[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
645        #[argh(subcommand, name = $name)]
646        #[doc = $comment]
647        pub struct $ty_name {
648            #[argh(option)]
649            /// the network id of the destination network
650            pub destination: std::net::IpAddr,
651            #[argh(
652                option,
653                arg_name = "netmask or prefix length",
654                from_str_fn(parse_netmask_or_prefix_length),
655                long = "netmask"
656            )]
657            /// the netmask or prefix length corresponding to destination
658            pub prefix_len: u8,
659            #[argh(option)]
660            /// the ip address of the first hop router
661            pub gateway: Option<std::net::IpAddr>,
662            #[argh(option, arg_name = "nicid or name:ifname", long = "nicid")]
663            /// the outgoing network interface of the route
664            pub interface: InterfaceIdentifier,
665            #[argh(option)]
666            /// the metric for the route
667            pub metric: Option<u32>,
668        }
669    };
670}
671
672// TODO(https://github.com/google/argh/issues/48): do this more sanely.
673route_struct!(RouteAdd, "add", "adds a route to the route table");
674route_struct!(RouteDel, "del", "deletes a route from the route table");
675
676#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
677#[argh(subcommand, name = "rule")]
678/// commands for policy-based-routing rules
679pub struct Rule {
680    #[argh(subcommand)]
681    pub rule_cmd: RuleEnum,
682}
683
684#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
685#[argh(subcommand)]
686pub enum RuleEnum {
687    List(RuleList),
688}
689
690#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
691#[argh(subcommand, name = "list")]
692/// lists rules (supports ffx machine output)
693pub struct RuleList {}
694
695#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
696#[argh(subcommand, name = "dhcp")]
697/// commands for an interfaces dhcp client
698pub struct Dhcp {
699    #[argh(subcommand)]
700    pub dhcp_cmd: DhcpEnum,
701}
702
703#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
704#[argh(subcommand)]
705pub enum DhcpEnum {
706    Start(DhcpStart),
707    Stop(DhcpStop),
708}
709
710#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
711#[argh(subcommand, name = "start")]
712/// starts a dhcp client on the interface
713pub struct DhcpStart {
714    #[argh(positional, arg_name = "nicid or name:ifname")]
715    pub interface: InterfaceIdentifier,
716}
717
718#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
719#[argh(subcommand, name = "stop")]
720/// stops the dhcp client on the interface
721pub struct DhcpStop {
722    #[argh(positional, arg_name = "nicid or name:ifname")]
723    pub interface: InterfaceIdentifier,
724}
725
726#[cfg(test)]
727mod tests {
728    use super::*;
729    use assert_matches::assert_matches;
730    use test_case::test_case;
731
732    #[test_case("24", 24 ; "from prefix length")]
733    #[test_case("255.255.254.0", 23 ; "from ipv4 netmask")]
734    #[test_case("ffff:fff0::", 28 ; "from ipv6 netmask")]
735    fn parse_prefix_len(to_parse: &str, want: u8) {
736        let got = parse_netmask_or_prefix_length(to_parse).unwrap();
737        assert_eq!(got, want)
738    }
739
740    proptest::proptest! {
741        #[test]
742        fn cant_parse_as_both_netmask_and_prefix_len(s: String) {
743            let netmask_parse_result = s.parse::<std::net::IpAddr>();
744            let prefix_len_parse_result = s.parse::<u8>();
745            assert_matches!((netmask_parse_result, prefix_len_parse_result), (Ok(_), Err(_)) | (Err(_), Ok(_)) | (Err(_), Err(_)));
746        }
747    }
748
749    #[test_case("1", InterfaceIdentifier::Id(NonZeroU64::new(1).unwrap()) ; "as nicid")]
750    #[test_case("name:lo", InterfaceIdentifier::Name("lo".to_string()) ; "as ifname")]
751    #[test_case("name:name:lo", InterfaceIdentifier::Name("name:lo".to_string()) ; "as ifname with 'name:' as part of name")]
752    #[test_case("name:1", InterfaceIdentifier::Name("1".to_string()) ; "as numerical ifname")]
753    fn parse_interface(to_parse: &str, want: InterfaceIdentifier) {
754        let got = to_parse.parse::<InterfaceIdentifier>().unwrap();
755        assert_eq!(got, want)
756    }
757
758    #[test]
759    fn parse_interface_without_prefix_fails() {
760        assert_matches!("lo".parse::<InterfaceIdentifier>(), Err(_))
761    }
762
763    #[test]
764    fn parse_interface_zero_fails() {
765        assert_matches!("0".parse::<InterfaceIdentifier>(), Err(_))
766    }
767
768    #[test_case("0", Err("Invalid IGMP version (0). Valid values: 1, 2, 3".to_string()); "input_0")]
769    #[test_case("1", Ok(finterfaces_admin::IgmpVersion::V1); "input_1")]
770    #[test_case("2", Ok(finterfaces_admin::IgmpVersion::V2); "input_2")]
771    #[test_case("3", Ok(finterfaces_admin::IgmpVersion::V3); "input_3")]
772    #[test_case("4", Err("Invalid IGMP version (4). Valid values: 1, 2, 3".to_string()); "input_4")]
773    #[test_case("a", Err("Failed to parse IGMP version (error: invalid digit found in string)".to_string()); "input_a")]
774    fn igmp_version(s: &str, expected: Result<finterfaces_admin::IgmpVersion, String>) {
775        assert_eq!(parse_igmp_version(s), expected)
776    }
777
778    #[test_case("0", Err("Invalid MLD version (0). Valid values: 1, 2".to_string()); "input_0")]
779    #[test_case("1", Ok(finterfaces_admin::MldVersion::V1); "input_1")]
780    #[test_case("2", Ok(finterfaces_admin::MldVersion::V2); "input_2")]
781    #[test_case("3", Err("Invalid MLD version (3). Valid values: 1, 2".to_string()); "input_3")]
782    #[test_case("a", Err("Failed to parse MLD version (error: invalid digit found in string)".to_string()); "input_a")]
783    fn mld_version(s: &str, expected: Result<finterfaces_admin::MldVersion, String>) {
784        assert_eq!(parse_mld_version(s), expected)
785    }
786}