1use std::num::{NonZeroU16, NonZeroU64};
6use std::ops::RangeInclusive;
7use std::str::FromStr;
8
9use anyhow::{anyhow, Context as _};
10use argh::{ArgsInfo, FromArgs};
11use {
12 fidl_fuchsia_net as fnet, fidl_fuchsia_net_ext as fnet_ext,
13 fidl_fuchsia_net_filter as fnet_filter, fidl_fuchsia_net_filter_ext as fnet_filter_ext,
14 fidl_fuchsia_net_interfaces_ext as fnet_interfaces_ext,
15};
16
17#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
18#[argh(subcommand, name = "filter")]
19pub struct Filter {
21 #[argh(subcommand)]
22 pub filter_cmd: FilterEnum,
23}
24
25#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
26#[argh(subcommand)]
27pub enum FilterEnum {
28 List(List),
29 Create(Create),
30 Remove(Remove),
31}
32
33#[derive(Clone, Debug, ArgsInfo, FromArgs, PartialEq)]
35#[argh(subcommand, name = "list")]
36pub struct List {}
37
38#[derive(Clone, Debug, ArgsInfo, FromArgs, PartialEq)]
40#[argh(subcommand, name = "create")]
41pub struct Create {
42 #[argh(option)]
44 pub controller: String,
45 #[argh(switch)]
48 pub idempotent: Option<bool>,
49 #[argh(subcommand)]
51 pub resource: Resource,
52}
53
54#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
55#[argh(subcommand)]
56pub enum Resource {
57 Namespace(Namespace),
58 Routine(Routine),
59 Rule(Rule),
60}
61
62#[derive(Clone, Debug, ArgsInfo, FromArgs, PartialEq)]
64#[argh(subcommand, name = "namespace")]
65pub struct Namespace {
66 #[argh(option)]
68 pub name: String,
69 #[argh(option, default = "Domain::All")]
71 pub domain: Domain,
72}
73
74#[derive(Clone, Debug, PartialEq)]
75pub enum Domain {
76 All,
77 Ipv4,
78 Ipv6,
79}
80
81impl std::str::FromStr for Domain {
82 type Err = anyhow::Error;
83
84 fn from_str(s: &str) -> Result<Self, Self::Err> {
85 match s.to_lowercase().as_str() {
86 "all" => Ok(Self::All),
87 "ipv4" => Ok(Self::Ipv4),
88 "ipv6" => Ok(Self::Ipv6),
89 s => Err(anyhow!("unknown IP domain {s}")),
90 }
91 }
92}
93
94#[derive(Clone, Debug, ArgsInfo, FromArgs, PartialEq)]
96#[argh(subcommand, name = "routine")]
97pub struct Routine {
98 #[argh(option)]
100 pub namespace: String,
101 #[argh(option)]
103 pub name: String,
104 #[argh(option, arg_name = "type")]
106 pub type_: RoutineType,
107 #[argh(option)]
109 pub hook: Option<Hook>,
110 #[argh(option)]
112 pub priority: Option<i32>,
113}
114
115#[derive(Clone, Debug, PartialEq)]
116pub enum RoutineType {
117 Ip,
118 Nat,
119}
120
121impl std::str::FromStr for RoutineType {
122 type Err = anyhow::Error;
123
124 fn from_str(s: &str) -> Result<Self, Self::Err> {
125 match s.to_lowercase().as_str() {
126 "ip" => Ok(Self::Ip),
127 "nat" => Ok(Self::Nat),
128 s => Err(anyhow!("unknown routine type {s}")),
129 }
130 }
131}
132
133#[derive(Clone, Debug, PartialEq)]
134pub enum Hook {
135 Ingress,
136 LocalIngress,
137 Forwarding,
138 LocalEgress,
139 Egress,
140}
141
142impl std::str::FromStr for Hook {
143 type Err = anyhow::Error;
144
145 fn from_str(s: &str) -> Result<Self, Self::Err> {
146 match s.to_lowercase().as_str() {
147 "ingress" => Ok(Self::Ingress),
148 "local_ingress" | "local-ingress" => Ok(Self::LocalIngress),
149 "forwarding" => Ok(Self::Forwarding),
150 "local_egress" | "local-egress" => Ok(Self::LocalEgress),
151 "egress" => Ok(Self::Egress),
152 s => Err(anyhow!("unknown hook {s}")),
153 }
154 }
155}
156
157#[derive(Clone, Debug, ArgsInfo, FromArgs, PartialEq)]
164#[argh(subcommand, name = "rule")]
165pub struct Rule {
166 #[argh(option)]
168 pub namespace: String,
169 #[argh(option)]
171 pub routine: String,
172 #[argh(option)]
174 pub index: u32,
175 #[argh(option)]
179 pub in_interface: Option<InterfaceMatcher>,
180 #[argh(option)]
184 pub out_interface: Option<InterfaceMatcher>,
185 #[argh(option)]
190 pub src_addr: Option<AddressMatcher>,
191 #[argh(option)]
196 pub dst_addr: Option<AddressMatcher>,
197 #[argh(option)]
201 pub transport_protocol: Option<TransportProtocolMatcher>,
202 #[argh(option)]
208 pub src_port: Option<PortMatcher>,
209 #[argh(option)]
215 pub dst_port: Option<PortMatcher>,
216 #[argh(subcommand)]
218 pub action: Action,
219}
220
221#[derive(Clone, Debug, PartialEq)]
223pub enum InterfaceMatcher {
224 Id(NonZeroU64),
225 Name(String),
226 PortClass(fnet_interfaces_ext::PortClass),
227}
228
229impl FromStr for InterfaceMatcher {
230 type Err = anyhow::Error;
231
232 fn from_str(s: &str) -> Result<Self, Self::Err> {
233 let (property, value) = s.split_once(":").ok_or_else(|| {
234 anyhow!("expected $property:$value where property is one of id, name, class")
235 })?;
236 match property {
237 "id" => {
238 let id = value.parse::<NonZeroU64>()?;
239 Ok(Self::Id(id))
240 }
241 "name" => Ok(Self::Name(value.to_owned())),
242 "class" => {
243 let class = match value.to_lowercase().as_str() {
244 "loopback" => fnet_interfaces_ext::PortClass::Loopback,
245 "virtual" => fnet_interfaces_ext::PortClass::Virtual,
246 "ethernet" => fnet_interfaces_ext::PortClass::Ethernet,
247 "wlan_client" | "wlan-client" | "wlanclient" => {
248 fnet_interfaces_ext::PortClass::WlanClient
249 }
250 "wlan_ap" | "wlan-ap" | "wlanap" => fnet_interfaces_ext::PortClass::WlanAp,
251 "ppp" => fnet_interfaces_ext::PortClass::Ppp,
252 "bridge" => fnet_interfaces_ext::PortClass::Bridge,
253 "lowpan" => fnet_interfaces_ext::PortClass::Lowpan,
254 other => return Err(anyhow!("unrecognized port class {other}")),
255 };
256 Ok(Self::PortClass(class))
257 }
258 other => Err(anyhow!("unrecognized interface property {other}")),
259 }
260 }
261}
262
263impl From<InterfaceMatcher> for fnet_filter_ext::InterfaceMatcher {
264 fn from(matcher: InterfaceMatcher) -> Self {
265 match matcher {
266 InterfaceMatcher::Id(id) => Self::Id(id),
267 InterfaceMatcher::Name(name) => Self::Name(name),
268 InterfaceMatcher::PortClass(class) => Self::PortClass(class),
269 }
270 }
271}
272
273#[derive(Clone, Debug, PartialEq)]
275pub struct AddressMatcher {
276 pub matcher: AddressMatcherType,
277 pub invert: bool,
278}
279
280impl FromStr for AddressMatcher {
281 type Err = anyhow::Error;
282
283 fn from_str(s: &str) -> Result<Self, Self::Err> {
284 let (invert, s) = s.strip_prefix("!").map(|s| (true, s)).unwrap_or((false, s));
285 let matcher = s.parse::<AddressMatcherType>()?;
286 Ok(Self { matcher, invert })
287 }
288}
289
290impl From<AddressMatcher> for fnet_filter_ext::AddressMatcher {
291 fn from(matcher: AddressMatcher) -> Self {
292 let AddressMatcher { matcher, invert } = matcher;
293 Self { matcher: matcher.into(), invert }
294 }
295}
296
297#[derive(Clone, Debug, PartialEq)]
299pub enum AddressMatcherType {
300 Subnet(fnet_filter_ext::Subnet),
301 Range(fnet_filter_ext::AddressRange),
302}
303
304impl FromStr for AddressMatcherType {
305 type Err = anyhow::Error;
306
307 fn from_str(s: &str) -> Result<Self, Self::Err> {
308 let (property, value) = s.split_once(":").ok_or_else(|| {
309 anyhow!("expected $property:$value where property is one of subnet, range")
310 })?;
311 match property {
312 "subnet" => {
313 let subnet: fnet::Subnet = value.parse::<fnet_ext::Subnet>()?.into();
314 let subnet = subnet.try_into()?;
315 Ok(Self::Subnet(subnet))
316 }
317 "range" => {
318 let (start, end) = value.split_once("..=").ok_or_else(|| {
319 anyhow!(
320 "expected inclusive address range to be specified in the form $start..=$end"
321 )
322 })?;
323 let start: fnet::IpAddress = start.parse::<fnet_ext::IpAddress>()?.into();
324 let end: fnet::IpAddress = end.parse::<fnet_ext::IpAddress>()?.into();
325 let range = fnet_filter::AddressRange { start, end }.try_into()?;
326 Ok(Self::Range(range))
327 }
328 other => Err(anyhow!("unrecognized address property {other}")),
329 }
330 }
331}
332
333impl From<AddressMatcherType> for fnet_filter_ext::AddressMatcherType {
334 fn from(matcher: AddressMatcherType) -> Self {
335 match matcher {
336 AddressMatcherType::Subnet(subnet) => Self::Subnet(subnet),
337 AddressMatcherType::Range(range) => Self::Range(range),
338 }
339 }
340}
341
342#[derive(Clone, Debug, PartialEq)]
344pub enum TransportProtocolMatcher {
345 Tcp,
346 Udp,
347 Icmp,
348 Icmpv6,
349}
350
351impl FromStr for TransportProtocolMatcher {
352 type Err = anyhow::Error;
353
354 fn from_str(s: &str) -> Result<Self, Self::Err> {
355 let matcher = match s.to_lowercase().as_str() {
356 "tcp" => Self::Tcp,
357 "udp" => Self::Udp,
358 "icmp" | "icmpv4" => Self::Icmp,
359 "icmpv6" => Self::Icmpv6,
360 other => return Err(anyhow!("unrecognized transport protocol {other}")),
361 };
362 Ok(matcher)
363 }
364}
365
366#[derive(Clone, Debug, PartialEq)]
368pub struct PortMatcher(fnet_filter_ext::PortMatcher);
369
370impl FromStr for PortMatcher {
371 type Err = anyhow::Error;
372
373 fn from_str(s: &str) -> Result<Self, Self::Err> {
374 let (invert, s) = s.strip_prefix("!").map(|s| (true, s)).unwrap_or((false, s));
375 let (start, end) = s.split_once("..=").ok_or_else(|| {
376 anyhow!("expected inclusive port range to be specified in the form $start..=$end")
377 })?;
378 let start = start.parse::<u16>()?;
379 let end = end.parse::<u16>()?;
380 let matcher = fnet_filter_ext::PortMatcher::new(start, end, invert)?;
381 Ok(Self(matcher))
382 }
383}
384
385impl From<PortMatcher> for fnet_filter_ext::PortMatcher {
386 fn from(matcher: PortMatcher) -> Self {
387 matcher.0
388 }
389}
390
391#[derive(Clone, Debug, ArgsInfo, FromArgs, PartialEq)]
393#[argh(subcommand)]
394pub enum Action {
395 Accept(Accept),
396 Drop(Drop),
397 Jump(Jump),
398 Return(Return),
399 TransparentProxy(TransparentProxy),
400 Redirect(Redirect),
401 Masquerade(Masquerade),
402}
403
404#[derive(Clone, Debug, ArgsInfo, FromArgs, PartialEq)]
406#[argh(subcommand, name = "accept")]
407pub struct Accept {}
408
409#[derive(Clone, Debug, ArgsInfo, FromArgs, PartialEq)]
411#[argh(subcommand, name = "drop")]
412pub struct Drop {}
413
414#[derive(Clone, Debug, ArgsInfo, FromArgs, PartialEq)]
416#[argh(subcommand, name = "jump")]
417pub struct Jump {
418 #[argh(positional)]
420 pub target: String,
421}
422
423#[derive(Clone, Debug, ArgsInfo, FromArgs, PartialEq)]
425#[argh(subcommand, name = "return")]
426pub struct Return {}
427
428#[derive(Clone, Debug, ArgsInfo, FromArgs, PartialEq)]
432#[argh(subcommand, name = "tproxy")]
433pub struct TransparentProxy {
434 #[argh(option)]
436 pub addr: Option<String>,
437 #[argh(option)]
439 pub port: Option<NonZeroU16>,
440}
441
442#[derive(Clone, Debug, PartialEq)]
444pub struct PortRange(pub RangeInclusive<NonZeroU16>);
445
446impl FromStr for PortRange {
447 type Err = anyhow::Error;
448
449 fn from_str(s: &str) -> Result<Self, Self::Err> {
450 let (start, end) = s.split_once("..=").ok_or_else(|| {
451 anyhow!("expected inclusive port range to be specified in the form $start..=$end")
452 })?;
453 let start = NonZeroU16::new(start.parse::<u16>()?).context("port must be nonzero")?;
454 let end = NonZeroU16::new(end.parse::<u16>()?).context("port must be nonzero")?;
455 Ok(Self(start..=end))
456 }
457}
458
459#[derive(Clone, Debug, ArgsInfo, FromArgs, PartialEq)]
465#[argh(subcommand, name = "redirect")]
466pub struct Redirect {
467 #[argh(option)]
469 pub dst_port: Option<PortRange>,
470}
471
472#[derive(Clone, Debug, ArgsInfo, FromArgs, PartialEq)]
478#[argh(subcommand, name = "masquerade")]
479pub struct Masquerade {
480 #[argh(option)]
482 pub src_port: Option<PortRange>,
483}
484
485#[derive(Clone, Debug, ArgsInfo, FromArgs, PartialEq)]
487#[argh(subcommand, name = "remove")]
488pub struct Remove {
489 #[argh(option)]
491 pub controller: String,
492 #[argh(subcommand)]
494 pub resource: ResourceId,
495 #[argh(switch)]
498 pub idempotent: Option<bool>,
499}
500
501#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
502#[argh(subcommand)]
503pub enum ResourceId {
504 Namespace(NamespaceId),
505 Routine(RoutineId),
506 Rule(RuleId),
507}
508
509#[derive(Clone, Debug, ArgsInfo, FromArgs, PartialEq)]
511#[argh(subcommand, name = "namespace")]
512pub struct NamespaceId {
513 #[argh(option)]
515 pub name: String,
516}
517
518#[derive(Clone, Debug, ArgsInfo, FromArgs, PartialEq)]
520#[argh(subcommand, name = "routine")]
521pub struct RoutineId {
522 #[argh(option)]
524 pub namespace: String,
525 #[argh(option)]
527 pub name: String,
528}
529
530#[derive(Clone, Debug, ArgsInfo, FromArgs, PartialEq)]
532#[argh(subcommand, name = "rule")]
533pub struct RuleId {
534 #[argh(option)]
536 pub namespace: String,
537 #[argh(option)]
539 pub routine: String,
540 #[argh(option)]
542 pub index: u32,
543}
544
545#[cfg(test)]
546mod tests {
547 use net_declare::{fidl_ip, fidl_subnet};
548 use test_case::test_case;
549
550 use super::*;
551
552 const THREE: NonZeroU64 = NonZeroU64::new(3).unwrap();
553
554 #[test_case("id:0" => Err(()); "id must be nonzero")]
555 #[test_case("id:-1" => Err(()); "id must be nonnegative")]
556 #[test_case("id:a" => Err(()); "id must be numeric")]
557 #[test_case("wlan" => Err(()); "must have both property and value")]
558 #[test_case("class:not-a-class" => Err(()); "class must be valid")]
559 #[test_case("unknown-property:value" => Err(()); "property must be id, name, or class")]
560 #[test_case("id:3" => Ok(InterfaceMatcher::Id(THREE)); "valid ID matcher")]
561 #[test_case(
562 "name:wlan" =>
563 Ok(InterfaceMatcher::Name(String::from("wlan")));
564 "valid name matcher"
565 )]
566 #[test_case(
567 "class:ethernet" =>
568 Ok(InterfaceMatcher::PortClass(fnet_interfaces_ext::PortClass::Ethernet));
569 "valid ethernet matcher"
570 )]
571 #[test_case(
572 "class:wlan-client" =>
573 Ok(InterfaceMatcher::PortClass(fnet_interfaces_ext::PortClass::WlanClient));
574 "valid wlan-client matcher"
575 )]
576 fn interface_matcher(s: &str) -> Result<InterfaceMatcher, ()> {
577 s.parse::<InterfaceMatcher>().map_err(|_| ())
578 }
579
580 #[test_case("192.168.0.1" => Err(()); "must have both property and value")]
581 #[test_case("unknown-property:value" => Err(()); "property must be range or subnet")]
582 #[test_case("subnet:192.0.2.1/" => Err(()); "not a subnet")]
583 #[test_case("subnet:192.0.2.1/24" => Err(()); "host bits are set")]
584 #[test_case("range:192.0.2.1" => Err(()); "not a range")]
585 #[test_case("range:192.0.2.1..192.0.2.255" => Err(()); "range must be specified with ..=")]
586 #[test_case("range:192.0.2.255..=192.0.2.1" => Err(()); "invalid range")]
587 #[test_case("range:A..=B" => Err(()); "not an IP address")]
588 #[test_case("!!subnet:192.0.2.0/24" => Err(()); "double invert")]
589 #[test_case(
590 "subnet:192.0.2.0/24" =>
591 Ok(AddressMatcher {
592 matcher: AddressMatcherType::Subnet(fidl_subnet!("192.0.2.0/24").try_into().unwrap()),
593 invert: false,
594 });
595 "valid IPv4 subnet matcher"
596 )]
597 #[test_case(
598 "subnet:2001:db8::/32" =>
599 Ok(AddressMatcher {
600 matcher: AddressMatcherType::Subnet(fidl_subnet!("2001:db8::/32").try_into().unwrap()),
601 invert: false,
602 });
603 "valid IPv6 subnet matcher"
604 )]
605 #[test_case(
606 "!subnet:192.0.2.0/24" =>
607 Ok(AddressMatcher {
608 matcher: AddressMatcherType::Subnet(fidl_subnet!("192.0.2.0/24").try_into().unwrap()),
609 invert: true,
610 });
611 "valid inverse IPv4 subnet matcher"
612 )]
613 #[test_case(
614 "range:192.0.2.1..=192.0.2.255" =>
615 Ok(AddressMatcher {
616 matcher: AddressMatcherType::Range(
617 fnet_filter::AddressRange {
618 start: fidl_ip!("192.0.2.1"),
619 end: fidl_ip!("192.0.2.255"),
620 }.try_into().unwrap()
621 ),
622 invert: false,
623 });
624 "valid IPv4 range matcher"
625 )]
626 #[test_case(
627 "range:2001:db8::1..=2001:db8::2" =>
628 Ok(AddressMatcher {
629 matcher: AddressMatcherType::Range(
630 fnet_filter::AddressRange {
631 start: fidl_ip!("2001:db8::1"),
632 end: fidl_ip!("2001:db8::2"),
633 }.try_into().unwrap()
634 ),
635 invert: false,
636 });
637 "valid IPv6 range matcher"
638 )]
639 #[test_case(
640 "!range:192.0.2.1..=192.0.2.255" =>
641 Ok(AddressMatcher {
642 matcher: AddressMatcherType::Range(
643 fnet_filter::AddressRange {
644 start: fidl_ip!("192.0.2.1"),
645 end: fidl_ip!("192.0.2.255"),
646 }.try_into().unwrap()
647 ),
648 invert: true,
649 });
650 "valid inverse IPv6 range matcher"
651 )]
652 fn address_matcher(s: &str) -> Result<AddressMatcher, ()> {
653 s.parse::<AddressMatcher>().map_err(|_| ())
654 }
655
656 #[test_case("22" => Err(()); "must be specified as range")]
657 #[test_case("22..22" => Err(()); "must be written as ..=")]
658 #[test_case("65536..=65536" => Err(()); "invalid u16")]
659 #[test_case("33333..=22222" => Err(()); "invalid range")]
660 #[test_case("!!33333..=22222" => Err(()); "double invert")]
661 #[test_case(
662 "22..=22" =>
663 Ok(PortMatcher(fnet_filter_ext::PortMatcher::new(22, 22, false).unwrap()));
664 "valid single port matcher"
665 )]
666 #[test_case(
667 "0..=65535" =>
668 Ok(PortMatcher(fnet_filter_ext::PortMatcher::new(0, 65535, false).unwrap()));
669 "valid port range matcher"
670 )]
671 #[test_case(
672 "!443..=443" =>
673 Ok(PortMatcher(fnet_filter_ext::PortMatcher::new(443, 443, true).unwrap()));
674 "valid inverse port matcher"
675 )]
676 fn port_matcher(s: &str) -> Result<PortMatcher, ()> {
677 s.parse::<PortMatcher>().map_err(|_| ())
678 }
679
680 #[test_case("22" => Err(()); "must be specified as range")]
681 #[test_case("22..22" => Err(()); "must be written as ..=")]
682 #[test_case("0..=1" => Err(()); "must be nonzero")]
683 #[test_case("65536..=65536" => Err(()); "invalid u16")]
684 #[test_case(
685 "22..=22" => Ok(PortRange(NonZeroU16::new(22).unwrap()..=NonZeroU16::new(22).unwrap()));
686 "valid single port matcher"
687 )]
688 #[test_case(
689 "1..=65535" => Ok(PortRange(NonZeroU16::new(1).unwrap()..=NonZeroU16::MAX));
690 "valid port range matcher"
691 )]
692 fn port_range(s: &str) -> Result<PortRange, ()> {
693 s.parse::<PortRange>().map_err(|_| ())
694 }
695}