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 as fnet, 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(
227                &fnet::Subnet::from(*src_subnet).addr,
228                &fnet::Subnet::from(*dst_subnet).addr,
229            ) {
230                return Err(Error::Invalid(InvalidReason::MixedIPVersions));
231            }
232        }
233    }
234
235    Ok(())
236}
237
238pub fn parse_str_to_rules(
239    line: &str,
240    routines: &FilterRoutines,
241) -> Result<Vec<filter_ext::Rule>, Error> {
242    let mut pairs =
243        FilterRuleParser::parse(Rule::rules, &line).map_err(|err| Error::Pest(Box::new(err)))?;
244    let mut rules = Vec::new();
245    for (index, filter_rule) in pairs.next().unwrap().into_inner().into_iter().enumerate() {
246        match filter_rule.as_rule() {
247            Rule::rule => {
248                let rule = parse_rule(filter_rule, &routines, index)?;
249                let () = validate_rule(&rule)?;
250                rules.push(rule);
251            }
252            Rule::EOI => (),
253            _ => unreachable!("rule must only have a rule case"),
254        }
255    }
256    Ok(rules)
257}
258
259pub fn parse_str_to_nat_rules(
260    _line: &str,
261    _routines: &NatRoutines,
262) -> Result<Vec<filter_ext::Rule>, Error> {
263    // TODO(https://fxbug.dev/323950204): Parse NAT rules once
264    // supported in filter2
265    todo!("not yet supported in the filter2 API")
266}
267
268pub fn parse_str_to_rdr_rules(
269    _line: &str,
270    _routines: &NatRoutines,
271) -> Result<Vec<filter_ext::Rule>, Error> {
272    // TODO(https://fxbug.dev/323949893): Parse NAT RDR rules once
273    // supported in filter2
274    todo!("not yet supported in the filter2 API")
275}
276
277#[cfg(test)]
278mod test {
279    use super::*;
280
281    use net_declare::fidl_subnet;
282
283    fn test_filter_routines() -> FilterRoutines {
284        FilterRoutines {
285            local_ingress: Some(local_ingress_routine()),
286            local_egress: Some(local_egress_routine()),
287        }
288    }
289
290    fn local_ingress_routine() -> filter_ext::RoutineId {
291        test_routine_id("local_ingress")
292    }
293
294    fn local_egress_routine() -> filter_ext::RoutineId {
295        test_routine_id("local_egress")
296    }
297
298    fn test_routine_id(name: &str) -> filter_ext::RoutineId {
299        filter_ext::RoutineId {
300            namespace: filter_ext::NamespaceId(String::from("namespace")),
301            name: String::from(name),
302        }
303    }
304
305    #[test]
306    fn test_rule_with_proto_any() {
307        assert_eq!(
308            parse_str_to_rules("pass in;", &test_filter_routines()),
309            Ok(vec![filter_ext::Rule {
310                id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
311                matchers: filter_ext::Matchers::default(),
312                action: filter_ext::Action::Accept,
313            }])
314        );
315    }
316
317    #[test]
318    fn test_rule_local_ingress_without_corresponding_routine() {
319        assert_eq!(
320            parse_str_to_rules("pass in;", &FilterRoutines::default()),
321            Err(Error::RoutineNotProvided(Direction::LocalIngress))
322        );
323    }
324
325    #[test]
326    fn test_rule_local_egress_without_corresponding_routine() {
327        assert_eq!(
328            parse_str_to_rules("pass out;", &FilterRoutines::default()),
329            Err(Error::RoutineNotProvided(Direction::LocalEgress))
330        );
331    }
332
333    #[test]
334    fn test_rule_with_proto_tcp() {
335        assert_eq!(
336            parse_str_to_rules("pass in proto tcp;", &test_filter_routines()),
337            Ok(vec![filter_ext::Rule {
338                id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
339                matchers: filter_ext::Matchers {
340                    transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
341                        src_port: None,
342                        dst_port: None,
343                    }),
344                    ..Default::default()
345                },
346                action: filter_ext::Action::Accept,
347            }])
348        )
349    }
350
351    #[test]
352    fn test_multiple_rules() {
353        assert_eq!(
354            parse_str_to_rules("pass in proto tcp; drop out proto udp;", &test_filter_routines()),
355            Ok(vec![
356                filter_ext::Rule {
357                    id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
358                    matchers: filter_ext::Matchers {
359                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
360                            src_port: None,
361                            dst_port: None,
362                        }),
363                        ..Default::default()
364                    },
365                    action: filter_ext::Action::Accept,
366                },
367                filter_ext::Rule {
368                    id: filter_ext::RuleId { routine: local_egress_routine(), index: 1 },
369                    matchers: filter_ext::Matchers {
370                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Udp {
371                            src_port: None,
372                            dst_port: None,
373                        }),
374                        ..Default::default()
375                    },
376                    action: filter_ext::Action::Drop,
377                }
378            ])
379        )
380    }
381
382    #[test]
383    fn test_rule_with_from_v4_address() {
384        assert_eq!(
385            parse_str_to_rules("pass in proto tcp from 1.2.3.0/24;", &test_filter_routines()),
386            Ok(vec![
387                filter_ext::Rule {
388                    id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
389                    matchers: filter_ext::Matchers {
390                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
391                            src_port: None,
392                            dst_port: None,
393                        }),
394                        src_addr: Some(fnet_matchers_ext::Address {
395                            matcher: fnet_matchers_ext::AddressMatcherType::Subnet(
396                                fnet_matchers_ext::Subnet::try_from(fidl_subnet!("1.2.3.0/24"))
397                                    .unwrap()
398                            ),
399                            invert: false,
400                        }),
401                        ..Default::default()
402                    },
403                    action: filter_ext::Action::Accept,
404                }
405                .into()
406            ])
407        )
408    }
409
410    #[test]
411    fn test_rule_with_from_port() {
412        assert_eq!(
413            parse_str_to_rules("pass in proto tcp from port 10000;", &test_filter_routines()),
414            Ok(vec![
415                filter_ext::Rule {
416                    id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
417                    matchers: filter_ext::Matchers {
418                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
419                            src_port: Some(
420                                fnet_matchers_ext::Port::new(10000, 10000, false).unwrap()
421                            ),
422                            dst_port: None,
423                        }),
424                        ..Default::default()
425                    },
426                    action: filter_ext::Action::Accept,
427                }
428                .into()
429            ])
430        )
431    }
432
433    #[test]
434    fn test_rule_with_from_range() {
435        assert_eq!(
436            parse_str_to_rules(
437                "pass in proto tcp from range 10000:10010;",
438                &test_filter_routines()
439            ),
440            Ok(vec![
441                filter_ext::Rule {
442                    id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
443                    matchers: filter_ext::Matchers {
444                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
445                            src_port: Some(
446                                fnet_matchers_ext::Port::new(10000, 10010, false).unwrap()
447                            ),
448                            dst_port: None,
449                        }),
450                        ..Default::default()
451                    },
452                    action: filter_ext::Action::Accept,
453                }
454                .into()
455            ])
456        )
457    }
458
459    #[test]
460    fn test_rule_with_from_invalid_range() {
461        assert_eq!(
462            parse_str_to_rules(
463                "pass in proto tcp from range 10005:10000;",
464                &test_filter_routines()
465            ),
466            Err(Error::Invalid(InvalidReason::InvalidPortRange))
467        );
468    }
469
470    #[test]
471    fn test_rule_with_from_v4_address_port() {
472        assert_eq!(
473            parse_str_to_rules(
474                "pass in proto tcp from 1.2.3.0/24 port 10000;",
475                &test_filter_routines()
476            ),
477            Ok(vec![
478                filter_ext::Rule {
479                    id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
480                    matchers: filter_ext::Matchers {
481                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
482                            src_port: Some(
483                                fnet_matchers_ext::Port::new(10000, 10000, false).unwrap()
484                            ),
485                            dst_port: None,
486                        }),
487                        src_addr: Some(fnet_matchers_ext::Address {
488                            matcher: fnet_matchers_ext::AddressMatcherType::Subnet(
489                                fnet_matchers_ext::Subnet::try_from(fidl_subnet!("1.2.3.0/24"))
490                                    .unwrap()
491                            ),
492                            invert: false,
493                        }),
494                        ..Default::default()
495                    },
496                    action: filter_ext::Action::Accept,
497                }
498                .into()
499            ])
500        )
501    }
502
503    #[test]
504    fn test_rule_with_from_not_v4_address_port() {
505        assert_eq!(
506            parse_str_to_rules(
507                "pass in proto tcp from !1.2.3.0/24 port 10000;",
508                &test_filter_routines()
509            ),
510            Ok(vec![
511                filter_ext::Rule {
512                    id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
513                    matchers: filter_ext::Matchers {
514                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
515                            src_port: Some(
516                                fnet_matchers_ext::Port::new(10000, 10000, false).unwrap()
517                            ),
518                            dst_port: None,
519                        }),
520                        src_addr: Some(fnet_matchers_ext::Address {
521                            matcher: fnet_matchers_ext::AddressMatcherType::Subnet(
522                                fnet_matchers_ext::Subnet::try_from(fidl_subnet!("1.2.3.0/24"))
523                                    .unwrap()
524                            ),
525                            invert: true,
526                        }),
527                        ..Default::default()
528                    },
529                    action: filter_ext::Action::Accept,
530                }
531                .into()
532            ])
533        )
534    }
535
536    #[test]
537    fn test_rule_with_from_v6_address_port() {
538        assert_eq!(
539            parse_str_to_rules(
540                "pass in proto tcp from 1234:5678::/32 port 10000;",
541                &test_filter_routines()
542            ),
543            Ok(vec![
544                filter_ext::Rule {
545                    id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
546                    matchers: filter_ext::Matchers {
547                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
548                            src_port: Some(
549                                fnet_matchers_ext::Port::new(10000, 10000, false).unwrap()
550                            ),
551                            dst_port: None,
552                        }),
553                        src_addr: Some(fnet_matchers_ext::Address {
554                            matcher: fnet_matchers_ext::AddressMatcherType::Subnet(
555                                fnet_matchers_ext::Subnet::try_from(fidl_subnet!("1234:5678::/32"))
556                                    .unwrap()
557                            ),
558                            invert: false,
559                        }),
560                        ..Default::default()
561                    },
562                    action: filter_ext::Action::Accept,
563                }
564                .into()
565            ])
566        )
567    }
568
569    #[test]
570    fn test_rule_with_to_v6_address_port() {
571        assert_eq!(
572            parse_str_to_rules(
573                "pass in proto tcp to 1234:5678::/32 port 10000;",
574                &test_filter_routines()
575            ),
576            Ok(vec![
577                filter_ext::Rule {
578                    id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
579                    matchers: filter_ext::Matchers {
580                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
581                            src_port: None,
582                            dst_port: Some(
583                                fnet_matchers_ext::Port::new(10000, 10000, false).unwrap()
584                            ),
585                        }),
586                        dst_addr: Some(fnet_matchers_ext::Address {
587                            matcher: fnet_matchers_ext::AddressMatcherType::Subnet(
588                                fnet_matchers_ext::Subnet::try_from(fidl_subnet!("1234:5678::/32"))
589                                    .unwrap()
590                            ),
591                            invert: false,
592                        }),
593                        ..Default::default()
594                    },
595                    action: filter_ext::Action::Accept,
596                }
597                .into()
598            ])
599        )
600    }
601
602    #[test]
603    fn test_rule_with_from_v6_address_port_to_v4_address_port() {
604        assert_eq!(
605            parse_str_to_rules(
606                "pass in proto tcp from 1234:5678::/32 port 10000 to 1.2.3.0/24 port 1000;",
607                &test_filter_routines()
608            ),
609            Err(Error::Invalid(InvalidReason::MixedIPVersions))
610        );
611    }
612
613    #[test]
614    fn test_rule_with_from_v6_address_port_to_v6_address_port() {
615        assert_eq!(
616            parse_str_to_rules(
617                "pass in proto tcp from 1234:5678::/32 port 10000 to 2345:6789::/32 port 1000;",
618                &test_filter_routines()
619            ),
620            Ok(vec![
621                filter_ext::Rule {
622                    id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
623                    matchers: filter_ext::Matchers {
624                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
625                            src_port: Some(
626                                fnet_matchers_ext::Port::new(10000, 10000, false).unwrap()
627                            ),
628                            dst_port: Some(
629                                fnet_matchers_ext::Port::new(1000, 1000, false).unwrap()
630                            ),
631                        }),
632                        src_addr: Some(fnet_matchers_ext::Address {
633                            matcher: fnet_matchers_ext::AddressMatcherType::Subnet(
634                                fnet_matchers_ext::Subnet::try_from(fidl_subnet!("1234:5678::/32"))
635                                    .unwrap()
636                            ),
637                            invert: false,
638                        }),
639                        dst_addr: Some(fnet_matchers_ext::Address {
640                            matcher: fnet_matchers_ext::AddressMatcherType::Subnet(
641                                fnet_matchers_ext::Subnet::try_from(fidl_subnet!("2345:6789::/32"))
642                                    .unwrap()
643                            ),
644                            invert: false,
645                        }),
646                        ..Default::default()
647                    },
648                    action: filter_ext::Action::Accept,
649                }
650                .into()
651            ])
652        )
653    }
654
655    #[test]
656    fn test_rule_with_port_class() {
657        assert_eq!(
658            parse_str_to_rules("pass in proto tcp devclass ap;", &test_filter_routines()),
659            Ok(vec![filter_ext::Rule {
660                id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
661                matchers: filter_ext::Matchers {
662                    in_interface: Some(fnet_matchers_ext::Interface::PortClass(
663                        fnet_interfaces_ext::PortClass::WlanAp,
664                    )),
665                    transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
666                        src_port: None,
667                        dst_port: None,
668                    }),
669                    ..Default::default()
670                },
671                action: filter_ext::Action::Accept,
672            }])
673        )
674    }
675
676    #[test]
677    fn test_rule_with_port_class_and_dst_range() {
678        assert_eq!(
679            parse_str_to_rules(
680                "pass in proto tcp devclass ap to range 1:2;",
681                &test_filter_routines()
682            ),
683            Ok(vec![
684                filter_ext::Rule {
685                    id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
686                    matchers: filter_ext::Matchers {
687                        in_interface: Some(fnet_matchers_ext::Interface::PortClass(
688                            fnet_interfaces_ext::PortClass::WlanAp
689                        )),
690                        transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
691                            src_port: None,
692                            dst_port: Some(fnet_matchers_ext::Port::new(1, 2, false).unwrap()),
693                        }),
694                        ..Default::default()
695                    },
696                    action: filter_ext::Action::Accept,
697                }
698                .into()
699            ])
700        )
701    }
702
703    // Ensure the `log` and `state` fields that are used in `filter_deprecated`
704    // can be provided, but have no impact on the parsed rule. These fields
705    // have no equivalent in filter2.
706    #[test]
707    fn test_rule_with_unused_fields() {
708        assert_eq!(
709            parse_str_to_rules("pass in proto tcp log no state;", &test_filter_routines()),
710            Ok(vec![filter_ext::Rule {
711                id: filter_ext::RuleId { routine: local_ingress_routine(), index: 0 },
712                matchers: filter_ext::Matchers {
713                    transport_protocol: Some(fnet_matchers_ext::TransportProtocol::Tcp {
714                        src_port: None,
715                        dst_port: None,
716                    }),
717                    ..Default::default()
718                },
719                action: filter_ext::Action::Accept,
720            }])
721        )
722    }
723}