Skip to main content

netfilter/
parser.rs

1// Copyright 2024 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 pest::Parser;
6use pest::iterators::Pair;
7
8#[cfg(not(feature = "fdomain"))]
9use fidl_fuchsia_net_filter_ext as filter_ext;
10#[cfg(feature = "fdomain")]
11use fidl_fuchsia_net_filter_ext_fdomain as filter_ext;
12#[cfg(not(feature = "fdomain"))]
13use fidl_fuchsia_net_interfaces_ext as fnet_interfaces_ext;
14#[cfg(feature = "fdomain")]
15use fidl_fuchsia_net_interfaces_ext_fdomain as fnet_interfaces_ext;
16#[cfg(not(feature = "fdomain"))]
17use fidl_fuchsia_net_matchers_ext as fnet_matchers_ext;
18#[cfg(feature = "fdomain")]
19use fidl_fuchsia_net_matchers_ext_fdomain as fnet_matchers_ext;
20use flex_fuchsia_net as fnet;
21
22use crate::grammar::{Error, FilterRuleParser, InvalidReason, Rule};
23use crate::util;
24
25fn parse_action(pair: Pair<'_, Rule>) -> filter_ext::Action {
26    assert_eq!(pair.as_rule(), Rule::action);
27    match pair.into_inner().next().unwrap().as_rule() {
28        Rule::pass => filter_ext::Action::Accept,
29        Rule::drop => filter_ext::Action::Drop,
30        // TODO(https://fxbug.dev/329500057): Remove dropreset from the grammar
31        // as it is unimplemented in filter.deprecated and currently unsupported
32        // in the filter2 API
33        Rule::dropreset => todo!("not yet supported in the filter2 API"),
34        _ => unreachable!("action must be one of (pass|drop|dropreset)"),
35    }
36}
37
38// A subset of `filter_ext::IpHook` to handle current
39// parsing capabilities.
40#[derive(Copy, Clone, Debug, PartialEq)]
41pub enum Direction {
42    LocalIngress,
43    LocalEgress,
44}
45
46fn parse_direction(pair: Pair<'_, Rule>) -> Direction {
47    assert_eq!(pair.as_rule(), Rule::direction);
48    match pair.into_inner().next().unwrap().as_rule() {
49        Rule::incoming => Direction::LocalIngress,
50        Rule::outgoing => Direction::LocalEgress,
51        _ => unreachable!("direction must be one of (in|out)"),
52    }
53}
54
55// A subset of `fnet_matchers_ext::TransportProtocol` to handle current
56// parsing capabilities.
57enum TransportProtocol {
58    Tcp,
59    Udp,
60    Icmp,
61}
62
63fn parse_proto(pair: Pair<'_, Rule>) -> Option<TransportProtocol> {
64    assert_eq!(pair.as_rule(), Rule::proto);
65    pair.into_inner().next().map(|pair| match pair.as_rule() {
66        Rule::tcp => TransportProtocol::Tcp,
67        Rule::udp => TransportProtocol::Udp,
68        Rule::icmp => TransportProtocol::Icmp,
69        _ => unreachable!("protocol must be one of (tcp|udp|icmp)"),
70    })
71}
72
73fn parse_devclass(pair: Pair<'_, Rule>) -> Option<fnet_interfaces_ext::PortClass> {
74    assert_eq!(pair.as_rule(), Rule::devclass);
75    pair.into_inner().next().map(|pair| match pair.as_rule() {
76        Rule::virt => fnet_interfaces_ext::PortClass::Virtual,
77        Rule::ethernet => fnet_interfaces_ext::PortClass::Ethernet,
78        Rule::wlan => fnet_interfaces_ext::PortClass::WlanClient,
79        Rule::ppp => fnet_interfaces_ext::PortClass::Ppp,
80        Rule::bridge => fnet_interfaces_ext::PortClass::Bridge,
81        Rule::ap => fnet_interfaces_ext::PortClass::WlanAp,
82        Rule::lowpan => fnet_interfaces_ext::PortClass::Lowpan,
83        _ => unreachable!("devclass must be one of (virt|ethernet|wlan|ppp|bridge|ap|lowpan)"),
84    })
85}
86
87fn parse_src(
88    pair: Pair<'_, Rule>,
89) -> Result<(Option<fnet_matchers_ext::Address>, Option<fnet_matchers_ext::Port>), Error> {
90    assert_eq!(pair.as_rule(), Rule::src);
91    parse_src_or_dst(pair)
92}
93
94fn parse_dst(
95    pair: Pair<'_, Rule>,
96) -> Result<(Option<fnet_matchers_ext::Address>, Option<fnet_matchers_ext::Port>), Error> {
97    assert_eq!(pair.as_rule(), Rule::dst);
98    parse_src_or_dst(pair)
99}
100
101fn parse_src_or_dst(
102    pair: Pair<'_, Rule>,
103) -> Result<(Option<fnet_matchers_ext::Address>, Option<fnet_matchers_ext::Port>), Error> {
104    let mut inner = pair.into_inner();
105    match inner.next() {
106        Some(pair) => match pair.as_rule() {
107            Rule::invertible_subnet => {
108                let (subnet, invert_match) = util::parse_invertible_subnet(pair)?;
109                let port = match inner.next() {
110                    Some(pair) => Some(parse_port_range(pair)?),
111                    None => None,
112                };
113                Ok((
114                    Some(fnet_matchers_ext::Address {
115                        matcher: fnet_matchers_ext::AddressMatcherType::Subnet(
116                            fnet_matchers_ext::Subnet::try_from(subnet)
117                                .map_err(|err| Error::Fidl(err.into()))?,
118                        ),
119                        invert: invert_match,
120                    }),
121                    port,
122                ))
123            }
124            Rule::port_range => Ok((None, Some(parse_port_range(pair)?))),
125            _ => unreachable!("src or dst must be either an invertible subnet or port range"),
126        },
127        None => Ok((None, None)),
128    }
129}
130
131fn parse_port_range(pair: Pair<'_, Rule>) -> Result<fnet_matchers_ext::Port, Error> {
132    assert_eq!(pair.as_rule(), Rule::port_range);
133    let mut inner = pair.into_inner();
134    let pair = inner.next().unwrap();
135    match pair.as_rule() {
136        Rule::port => {
137            let port_num = util::parse_port_num(inner.next().unwrap())?;
138            fnet_matchers_ext::Port::new(port_num, port_num, false).map_err(|err| match err {
139                fnet_matchers_ext::PortError::InvalidPortRange => {
140                    Error::Invalid(InvalidReason::InvalidPortRange)
141                }
142            })
143        }
144        Rule::range => {
145            let port_start = util::parse_port_num(inner.next().unwrap())?;
146            let port_end = util::parse_port_num(inner.next().unwrap())?;
147            fnet_matchers_ext::Port::new(port_start, port_end, false).map_err(|err| match err {
148                fnet_matchers_ext::PortError::InvalidPortRange => {
149                    Error::Invalid(InvalidReason::InvalidPortRange)
150                }
151            })
152        }
153        _ => unreachable!("port range must be either a single port, or a port range"),
154    }
155}
156
157fn parse_rule(
158    pair: Pair<'_, Rule>,
159    routines: &FilterRoutines,
160    index: usize,
161) -> Result<filter_ext::Rule, Error> {
162    assert_eq!(pair.as_rule(), Rule::rule);
163    let mut pairs = pair.into_inner();
164
165    let action = parse_action(pairs.next().unwrap());
166    let direction = parse_direction(pairs.next().unwrap());
167    let proto = parse_proto(pairs.next().unwrap());
168    let port_class = parse_devclass(pairs.next().unwrap());
169    let mut in_interface = None;
170    let mut out_interface = None;
171    let routine_id = match direction {
172        Direction::LocalIngress => {
173            // Use the same RoutineId as the LocalIngress routine.
174            let Some(ref routine_id) = routines.local_ingress else {
175                return Err(Error::RoutineNotProvided(direction));
176            };
177            in_interface = port_class.map(|class| fnet_matchers_ext::Interface::PortClass(class));
178            routine_id
179        }
180        Direction::LocalEgress => {
181            // Use the same RoutineId as the LocalEgress routine.
182            let Some(ref routine_id) = routines.local_egress else {
183                return Err(Error::RoutineNotProvided(direction));
184            };
185            out_interface = port_class.map(|class| fnet_matchers_ext::Interface::PortClass(class));
186            routine_id
187        }
188    };
189    let (src_addr, src_port) = parse_src(pairs.next().unwrap())?;
190    let (dst_addr, dst_port) = parse_dst(pairs.next().unwrap())?;
191    let transport_protocol = proto.map(|proto| match proto {
192        TransportProtocol::Tcp => fnet_matchers_ext::TransportProtocol::Tcp { src_port, dst_port },
193        TransportProtocol::Udp => fnet_matchers_ext::TransportProtocol::Udp { src_port, dst_port },
194        TransportProtocol::Icmp => fnet_matchers_ext::TransportProtocol::Icmp,
195    });
196
197    Ok(filter_ext::Rule {
198        id: filter_ext::RuleId { routine: routine_id.clone(), index: index as u32 },
199        matchers: filter_ext::Matchers {
200            in_interface,
201            out_interface,
202            src_addr,
203            dst_addr,
204            transport_protocol,
205            ..Default::default()
206        },
207        action,
208    })
209}
210
211// A container for `filter_ext::Routine`s that back the
212// `filter_ext::IpInstallationHook`s currently supported
213// by the parser.
214#[derive(Debug, Default)]
215pub struct FilterRoutines {
216    pub local_ingress: Option<filter_ext::RoutineId>,
217    pub local_egress: Option<filter_ext::RoutineId>,
218}
219
220// A container for `filter_ext::Routine`s that back the
221// `filter_ext::NatInstallationHook`s currently supported
222// by the parser.
223#[derive(Debug, Default)]
224pub struct NatRoutines {}
225
226fn validate_rule(rule: &filter_ext::Rule) -> Result<(), Error> {
227    if let (Some(src_subnet), Some(dst_subnet)) = (&rule.matchers.src_addr, &rule.matchers.dst_addr)
228    {
229        if let (
230            fnet_matchers_ext::AddressMatcherType::Subnet(src_subnet),
231            fnet_matchers_ext::AddressMatcherType::Subnet(dst_subnet),
232        ) = (&src_subnet.matcher, &dst_subnet.matcher)
233        {
234            if !util::ip_version_eq(
235                &fnet::Subnet::from(*src_subnet).addr,
236                &fnet::Subnet::from(*dst_subnet).addr,
237            ) {
238                return Err(Error::Invalid(InvalidReason::MixedIPVersions));
239            }
240        }
241    }
242
243    Ok(())
244}
245
246pub fn parse_str_to_rules(
247    line: &str,
248    routines: &FilterRoutines,
249) -> Result<Vec<filter_ext::Rule>, Error> {
250    let mut pairs =
251        FilterRuleParser::parse(Rule::rules, &line).map_err(|err| Error::Pest(Box::new(err)))?;
252    let mut rules = Vec::new();
253    for (index, filter_rule) in pairs.next().unwrap().into_inner().into_iter().enumerate() {
254        match filter_rule.as_rule() {
255            Rule::rule => {
256                let rule = parse_rule(filter_rule, &routines, index)?;
257                validate_rule(&rule)?;
258                rules.push(rule);
259            }
260            Rule::EOI => (),
261            _ => unreachable!("rule must only have a rule case"),
262        }
263    }
264    Ok(rules)
265}
266
267pub fn parse_str_to_nat_rules(
268    _line: &str,
269    _routines: &NatRoutines,
270) -> Result<Vec<filter_ext::Rule>, Error> {
271    // TODO(https://fxbug.dev/323950204): Parse NAT rules once
272    // supported in filter2
273    todo!("not yet supported in the filter2 API")
274}
275
276pub fn parse_str_to_rdr_rules(
277    _line: &str,
278    _routines: &NatRoutines,
279) -> Result<Vec<filter_ext::Rule>, Error> {
280    // TODO(https://fxbug.dev/323949893): Parse NAT RDR rules once
281    // supported in filter2
282    todo!("not yet supported in the filter2 API")
283}
284
285#[cfg(test)]
286mod test {
287    use super::*;
288
289    use net_declare::fidl_subnet;
290
291    fn test_filter_routines() -> FilterRoutines {
292        FilterRoutines {
293            local_ingress: Some(local_ingress_routine()),
294            local_egress: Some(local_egress_routine()),
295        }
296    }
297
298    fn local_ingress_routine() -> filter_ext::RoutineId {
299        test_routine_id("local_ingress")
300    }
301
302    fn local_egress_routine() -> filter_ext::RoutineId {
303        test_routine_id("local_egress")
304    }
305
306    fn test_routine_id(name: &str) -> filter_ext::RoutineId {
307        filter_ext::RoutineId {
308            namespace: filter_ext::NamespaceId(String::from("namespace")),
309            name: String::from(name),
310        }
311    }
312
313    #[test]
314    fn test_rule_with_proto_any() {
315        assert_eq!(
316            parse_str_to_rules("pass in;", &test_filter_routines()),
317            Ok(vec![filter_ext::Rule {
318                id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
319                matchers: filter_ext::Matchers::default(),
320                action: filter_ext::Action::Accept,
321            }])
322        );
323    }
324
325    #[test]
326    fn test_rule_local_ingress_without_corresponding_routine() {
327        assert_eq!(
328            parse_str_to_rules("pass in;", &FilterRoutines::default()),
329            Err(Error::RoutineNotProvided(Direction::LocalIngress))
330        );
331    }
332
333    #[test]
334    fn test_rule_local_egress_without_corresponding_routine() {
335        assert_eq!(
336            parse_str_to_rules("pass out;", &FilterRoutines::default()),
337            Err(Error::RoutineNotProvided(Direction::LocalEgress))
338        );
339    }
340
341    #[test]
342    fn test_rule_with_proto_tcp() {
343        assert_eq!(
344            parse_str_to_rules("pass in proto tcp;", &test_filter_routines()),
345            Ok(vec![filter_ext::Rule {
346                id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
347                matchers: filter_ext::Matchers {
348                    transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
349                        src_port: None,
350                        dst_port: None,
351                    }),
352                    ..Default::default()
353                },
354                action: filter_ext::Action::Accept,
355            }])
356        )
357    }
358
359    #[test]
360    fn test_multiple_rules() {
361        assert_eq!(
362            parse_str_to_rules("pass in proto tcp; drop out proto udp;", &test_filter_routines()),
363            Ok(vec![
364                filter_ext::Rule {
365                    id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
366                    matchers: filter_ext::Matchers {
367                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
368                            src_port: None,
369                            dst_port: None,
370                        }),
371                        ..Default::default()
372                    },
373                    action: filter_ext::Action::Accept,
374                },
375                filter_ext::Rule {
376                    id: filter_ext::RuleId { routine: local_egress_routine(), index: 1 },
377                    matchers: filter_ext::Matchers {
378                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Udp {
379                            src_port: None,
380                            dst_port: None,
381                        }),
382                        ..Default::default()
383                    },
384                    action: filter_ext::Action::Drop,
385                }
386            ])
387        )
388    }
389
390    #[test]
391    fn test_rule_with_from_v4_address() {
392        assert_eq!(
393            parse_str_to_rules("pass in proto tcp from 1.2.3.0/24;", &test_filter_routines()),
394            Ok(vec![
395                filter_ext::Rule {
396                    id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
397                    matchers: filter_ext::Matchers {
398                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
399                            src_port: None,
400                            dst_port: None,
401                        }),
402                        src_addr: Some(fnet_matchers_ext::Address {
403                            matcher: fnet_matchers_ext::AddressMatcherType::Subnet(
404                                fnet_matchers_ext::Subnet::try_from(fidl_subnet!("1.2.3.0/24"))
405                                    .unwrap()
406                            ),
407                            invert: false,
408                        }),
409                        ..Default::default()
410                    },
411                    action: filter_ext::Action::Accept,
412                }
413                .into()
414            ])
415        )
416    }
417
418    #[test]
419    fn test_rule_with_from_port() {
420        assert_eq!(
421            parse_str_to_rules("pass in proto tcp from port 10000;", &test_filter_routines()),
422            Ok(vec![
423                filter_ext::Rule {
424                    id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
425                    matchers: filter_ext::Matchers {
426                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
427                            src_port: Some(
428                                fnet_matchers_ext::Port::new(10000, 10000, false).unwrap()
429                            ),
430                            dst_port: None,
431                        }),
432                        ..Default::default()
433                    },
434                    action: filter_ext::Action::Accept,
435                }
436                .into()
437            ])
438        )
439    }
440
441    #[test]
442    fn test_rule_with_from_range() {
443        assert_eq!(
444            parse_str_to_rules(
445                "pass in proto tcp from range 10000:10010;",
446                &test_filter_routines()
447            ),
448            Ok(vec![
449                filter_ext::Rule {
450                    id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
451                    matchers: filter_ext::Matchers {
452                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
453                            src_port: Some(
454                                fnet_matchers_ext::Port::new(10000, 10010, false).unwrap()
455                            ),
456                            dst_port: None,
457                        }),
458                        ..Default::default()
459                    },
460                    action: filter_ext::Action::Accept,
461                }
462                .into()
463            ])
464        )
465    }
466
467    #[test]
468    fn test_rule_with_from_invalid_range() {
469        assert_eq!(
470            parse_str_to_rules(
471                "pass in proto tcp from range 10005:10000;",
472                &test_filter_routines()
473            ),
474            Err(Error::Invalid(InvalidReason::InvalidPortRange))
475        );
476    }
477
478    #[test]
479    fn test_rule_with_from_v4_address_port() {
480        assert_eq!(
481            parse_str_to_rules(
482                "pass in proto tcp from 1.2.3.0/24 port 10000;",
483                &test_filter_routines()
484            ),
485            Ok(vec![
486                filter_ext::Rule {
487                    id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
488                    matchers: filter_ext::Matchers {
489                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
490                            src_port: Some(
491                                fnet_matchers_ext::Port::new(10000, 10000, false).unwrap()
492                            ),
493                            dst_port: None,
494                        }),
495                        src_addr: Some(fnet_matchers_ext::Address {
496                            matcher: fnet_matchers_ext::AddressMatcherType::Subnet(
497                                fnet_matchers_ext::Subnet::try_from(fidl_subnet!("1.2.3.0/24"))
498                                    .unwrap()
499                            ),
500                            invert: false,
501                        }),
502                        ..Default::default()
503                    },
504                    action: filter_ext::Action::Accept,
505                }
506                .into()
507            ])
508        )
509    }
510
511    #[test]
512    fn test_rule_with_from_not_v4_address_port() {
513        assert_eq!(
514            parse_str_to_rules(
515                "pass in proto tcp from !1.2.3.0/24 port 10000;",
516                &test_filter_routines()
517            ),
518            Ok(vec![
519                filter_ext::Rule {
520                    id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
521                    matchers: filter_ext::Matchers {
522                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
523                            src_port: Some(
524                                fnet_matchers_ext::Port::new(10000, 10000, false).unwrap()
525                            ),
526                            dst_port: None,
527                        }),
528                        src_addr: Some(fnet_matchers_ext::Address {
529                            matcher: fnet_matchers_ext::AddressMatcherType::Subnet(
530                                fnet_matchers_ext::Subnet::try_from(fidl_subnet!("1.2.3.0/24"))
531                                    .unwrap()
532                            ),
533                            invert: true,
534                        }),
535                        ..Default::default()
536                    },
537                    action: filter_ext::Action::Accept,
538                }
539                .into()
540            ])
541        )
542    }
543
544    #[test]
545    fn test_rule_with_from_v6_address_port() {
546        assert_eq!(
547            parse_str_to_rules(
548                "pass in proto tcp from 1234:5678::/32 port 10000;",
549                &test_filter_routines()
550            ),
551            Ok(vec![
552                filter_ext::Rule {
553                    id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
554                    matchers: filter_ext::Matchers {
555                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
556                            src_port: Some(
557                                fnet_matchers_ext::Port::new(10000, 10000, false).unwrap()
558                            ),
559                            dst_port: None,
560                        }),
561                        src_addr: Some(fnet_matchers_ext::Address {
562                            matcher: fnet_matchers_ext::AddressMatcherType::Subnet(
563                                fnet_matchers_ext::Subnet::try_from(fidl_subnet!("1234:5678::/32"))
564                                    .unwrap()
565                            ),
566                            invert: false,
567                        }),
568                        ..Default::default()
569                    },
570                    action: filter_ext::Action::Accept,
571                }
572                .into()
573            ])
574        )
575    }
576
577    #[test]
578    fn test_rule_with_to_v6_address_port() {
579        assert_eq!(
580            parse_str_to_rules(
581                "pass in proto tcp to 1234:5678::/32 port 10000;",
582                &test_filter_routines()
583            ),
584            Ok(vec![
585                filter_ext::Rule {
586                    id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
587                    matchers: filter_ext::Matchers {
588                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
589                            src_port: None,
590                            dst_port: Some(
591                                fnet_matchers_ext::Port::new(10000, 10000, false).unwrap()
592                            ),
593                        }),
594                        dst_addr: Some(fnet_matchers_ext::Address {
595                            matcher: fnet_matchers_ext::AddressMatcherType::Subnet(
596                                fnet_matchers_ext::Subnet::try_from(fidl_subnet!("1234:5678::/32"))
597                                    .unwrap()
598                            ),
599                            invert: false,
600                        }),
601                        ..Default::default()
602                    },
603                    action: filter_ext::Action::Accept,
604                }
605                .into()
606            ])
607        )
608    }
609
610    #[test]
611    fn test_rule_with_from_v6_address_port_to_v4_address_port() {
612        assert_eq!(
613            parse_str_to_rules(
614                "pass in proto tcp from 1234:5678::/32 port 10000 to 1.2.3.0/24 port 1000;",
615                &test_filter_routines()
616            ),
617            Err(Error::Invalid(InvalidReason::MixedIPVersions))
618        );
619    }
620
621    #[test]
622    fn test_rule_with_from_v6_address_port_to_v6_address_port() {
623        assert_eq!(
624            parse_str_to_rules(
625                "pass in proto tcp from 1234:5678::/32 port 10000 to 2345:6789::/32 port 1000;",
626                &test_filter_routines()
627            ),
628            Ok(vec![
629                filter_ext::Rule {
630                    id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
631                    matchers: filter_ext::Matchers {
632                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
633                            src_port: Some(
634                                fnet_matchers_ext::Port::new(10000, 10000, false).unwrap()
635                            ),
636                            dst_port: Some(
637                                fnet_matchers_ext::Port::new(1000, 1000, false).unwrap()
638                            ),
639                        }),
640                        src_addr: Some(fnet_matchers_ext::Address {
641                            matcher: fnet_matchers_ext::AddressMatcherType::Subnet(
642                                fnet_matchers_ext::Subnet::try_from(fidl_subnet!("1234:5678::/32"))
643                                    .unwrap()
644                            ),
645                            invert: false,
646                        }),
647                        dst_addr: Some(fnet_matchers_ext::Address {
648                            matcher: fnet_matchers_ext::AddressMatcherType::Subnet(
649                                fnet_matchers_ext::Subnet::try_from(fidl_subnet!("2345:6789::/32"))
650                                    .unwrap()
651                            ),
652                            invert: false,
653                        }),
654                        ..Default::default()
655                    },
656                    action: filter_ext::Action::Accept,
657                }
658                .into()
659            ])
660        )
661    }
662
663    #[test]
664    fn test_rule_with_port_class() {
665        assert_eq!(
666            parse_str_to_rules("pass in proto tcp devclass ap;", &test_filter_routines()),
667            Ok(vec![filter_ext::Rule {
668                id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
669                matchers: filter_ext::Matchers {
670                    in_interface: Some(fnet_matchers_ext::Interface::PortClass(
671                        fnet_interfaces_ext::PortClass::WlanAp,
672                    )),
673                    transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
674                        src_port: None,
675                        dst_port: None,
676                    }),
677                    ..Default::default()
678                },
679                action: filter_ext::Action::Accept,
680            }])
681        )
682    }
683
684    #[test]
685    fn test_rule_with_port_class_and_dst_range() {
686        assert_eq!(
687            parse_str_to_rules(
688                "pass in proto tcp devclass ap to range 1:2;",
689                &test_filter_routines()
690            ),
691            Ok(vec![
692                filter_ext::Rule {
693                    id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
694                    matchers: filter_ext::Matchers {
695                        in_interface: Some(fnet_matchers_ext::Interface::PortClass(
696                            fnet_interfaces_ext::PortClass::WlanAp
697                        )),
698                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
699                            src_port: None,
700                            dst_port: Some(fnet_matchers_ext::Port::new(1, 2, false).unwrap()),
701                        }),
702                        ..Default::default()
703                    },
704                    action: filter_ext::Action::Accept,
705                }
706                .into()
707            ])
708        )
709    }
710
711    // Ensure the `log` and `state` fields that are used in `filter_deprecated`
712    // can be provided, but have no impact on the parsed rule. These fields
713    // have no equivalent in filter2.
714    #[test]
715    fn test_rule_with_unused_fields() {
716        assert_eq!(
717            parse_str_to_rules("pass in proto tcp log no state;", &test_filter_routines()),
718            Ok(vec![filter_ext::Rule {
719                id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
720                matchers: filter_ext::Matchers {
721                    transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
722                        src_port: None,
723                        dst_port: None,
724                    }),
725                    ..Default::default()
726                },
727                action: filter_ext::Action::Accept,
728            }])
729        )
730    }
731}