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