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