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