Skip to main content

retina_filtergen/
lib.rs

1#![allow(clippy::needless_doctest_main)]
2//! A macro for defining filters in Retina.
3//!
4//! [`filter`](macro@self::filter) is an attribute macro that parses a user-defined filter
5//! expression to generate sub-filters that are statically inlined at each processing layer. This
6//! verifies filter expressions at compile time and avoids overheads associated with interpreting
7//! filters at runtime.
8//!
9//! # Usage
10//! ```rust
11//! use retina_core::config::default_config;
12//! use retina_core::Runtime;
13//! use retina_core::subscription::Connection;
14//! use retina_filtergen::filter;
15//!
16//! #[filter("(ipv4 and tcp.port >= 100 and tls.sni ~ 'netflix') or http")]
17//! fn main() {
18//!    let cfg = default_config();
19//!    let callback = |conn: Connection| {
20//!        println!("{:#?}", conn);
21//!    };
22//!    let mut runtime = Runtime::new(cfg, filter, callback).unwrap();
23//!    runtime.run();
24//! }
25//! ```
26//!
27//! # Syntax
28//! The Retina filter syntax is similar to that of [Wireshark display
29//! filters](https://wiki.wireshark.org/DisplayFilters). However, Retina is capable of filtering on
30//! live traffic for online analysis, whereas display filters can only be applied for offline
31//! analysis.
32//!
33//! A filter expression is a logical expression of constraints on attributes of the input traffic.
34//! Each constraint is either a binary predicate that compares the value of an entity's attribute
35//! with a constant, or a unary predicate that matches against the entity itself.
36//!
37//! ## Protocols
38//! All protocol identifiers are valid as long as Retina contains an appropriate [protocol
39//! module](../retina_core/protocols) of the same name. The [`filter`](macro@self::filter) macro
40//! automatically generates filtering code using structs defined in the protocol's corresponding
41//! parser module. The exception to this is `ethernet`, which Retina filters for by default.
42//!
43//! For example, [`ipv4`](../retina_core/protocols/packet/ipv4) and
44//! [`tls`](../retina_core/protocols/stream/tls) are filterable protocols because they are both
45//! protocol modules included in Retina.
46//!
47//! Retina will also automatically expand filter expressions to their fully-qualified form. For
48//! example, the filter `tcp` is equivalent to `(ipv4 and tcp) or (ipv6 and tcp)`.
49//!
50//! ## Fields
51//! All field identifiers are valid as long as Retina exposes a public accessor method for the
52//! corresponding protocol struct of the same name, and the method returns a supported RHS field
53//! type.
54//!
55//! For example,
56//! [`ipv4.src_addr`](../retina_core/protocols/packet/ipv4/struct.Ipv4.html#method.src_addr) and
57//! [`tls.sni`](../retina_core/protocols/stream/tls/struct.Tls.html#method.sni) are both filterable
58//! fields because `src_addr()` is a public method associated with the `Ipv4` struct that returns an
59//! `Ipv4Addr`, and `sni()` is a public method associated with the `Tls` struct that returns a
60//! `String`.
61//!
62//! Retina also supports two combined fields: `addr` and `port`. Logically, these are equivalent to
63//! `src_addr or dst_addr` and `src_port or dst_port`, respectively, except in predicates that use
64//! the `!=` comparison operator (details below).
65//!
66//! ## Field types (RHS values)
67//! | Type          | Example            |
68//! |---------------|--------------------|
69//! | IPv4 address  | `1.2.3.4`          |
70//! | IPv4 prefix   | `1.2.3.4/24`       |
71//! | IPv6 address  | `2001:db8::1`      |
72//! | IPv6 prefix   | `2001:db8::1/64`   |
73//! | Integer       | `443`              |
74//! | String        | `'Safari'`         |
75//! | Integer range | `1024..5000`       |
76//!
77//! ## Binary comparison operators
78//! | Operator |   Alias   |         Description        | Example                         |
79//! |----------|-----------|----------------------------|---------------------------------|
80//! | `=`      |           | Equals                     | `ipv4.addr = 127.0.0.1`         |
81//! | `!=`     | `ne`      | Not equals                 | `udp.dst_port != 53`            |
82//! | `>=`     | `ge`      | Greater than or equals     | `tcp.port >= 1024`              |
83//! | `<=`     | `le`      | Less than or equals        | `tls.version <= 771`            |
84//! | `>`      | `gt`      | Greater than               | `ipv4.time_to_live > 64`        |
85//! | `<`      | `lt`      | Less than                  | `ipv6.payload_length < 500`     |
86//! | `in`     |           | In a range, or in a subnet | `ipv4.src_addr in 1.2.3.4/16`   |
87//! | `~`      | `matches` | Regular expression match   | `tls.sni ~ 'netflix\\.com$'`    |
88//!
89//! **Possible pitfalls involving `!=`**
90//!
91//! Retina differs from Wireshark behavior regarding the `!=` operator. When applied to combined
92//! fields (i.e., `addr` or `port`), Retina follows connection-level semantics instead of
93//! packet-level semantics. For example, `tcp.port != 80` is equivalent to `tcp.src_port != 80 and
94//! tcp.dst_port != 80`.
95//!
96//! **Regular expressions**
97//!
98//! Retina compiles regular expressions exactly once, using the
99//! [`regex`](https://crates.io/crates/regex) and
100//! [`lazy_static`](https://crates.io/crates/lazy_static) crates. As of this writing, `proc-macro`
101//! crates like this one cannot export any items other than procedural macros, thus requiring
102//! applications that wish to use Retina's regular expression filtering to specify `regex` and
103//! `lazy_static` as dependencies. Also note that regular expressions are written as normal strings
104//! in Rust, and not as [raw string
105//! literals](https://doc.rust-lang.org/stable/reference/tokens.html#raw-string-literals). They are
106//! allowed to match anywhere in the text, unless start (`^`) and end (`$`) anchors are used.
107//!
108//! ## Logical operators
109//! | Operator | Alias | Description | Example                                      |
110//! |----------|-------|-------------|----------------------------------------------|
111//! | `and`    | `AND` | Logical AND | `tcp.port = 443 and tls`                     |
112//! | `or`     | `OR`  | Logical OR  | `http.method = 'GET' or http.method = 'POST'`|
113//!
114//! `AND` takes precedence over `OR` in the absence of parentheses.
115//!
116//! Retina does not yet support the logical `NOT` operator. For some expressions, it can be
117//! approximated using the `!=` binary comparison operator, taking the above mentioned pitfall into
118//! consideration.
119
120mod connection_filter;
121mod packet_filter;
122mod session_filter;
123mod util;
124
125use proc_macro::TokenStream;
126use quote::quote;
127use syn::parse_macro_input;
128
129use retina_core::filter::Filter;
130
131use crate::connection_filter::gen_connection_filter;
132use crate::packet_filter::gen_packet_filter;
133use crate::session_filter::gen_session_filter;
134
135/// Macro for generating filters.
136///
137/// ## Examples
138/// ```
139/// #[filter("")] // no filter
140/// fn main() {}
141/// ```
142///
143/// ```
144/// #[filter("tcp.port = 80")]
145/// fn main() {}
146/// ```
147///
148/// ```
149/// #[filter("http.method = 'GET'")]
150/// fn main() {}
151/// ```
152///
153/// ```
154/// #[filter("(ipv4 and tcp.port >= 100 and tls.sni ~ 'netflix') or http")]
155/// fn main() {}
156/// ```
157#[proc_macro_attribute]
158pub fn filter(args: TokenStream, input: TokenStream) -> TokenStream {
159    let filter_str = parse_macro_input!(args as syn::LitStr).value();
160    let input = parse_macro_input!(input as syn::ItemFn);
161    // let input_sig = &input.sig.ident;
162
163    let filter = Filter::from_str(&filter_str, false).unwrap();
164
165    let mut ptree = filter.to_ptree();
166    ptree.prune_branches();
167    // Displays the predicate trie during compilation.
168    println!("{}", ptree);
169
170    // store lazily evaluated statics like pre-compiled Regex
171    let mut statics: Vec<proc_macro2::TokenStream> = vec![];
172
173    let (packet_filter_body, pt_nodes) = gen_packet_filter(&ptree, &mut statics);
174    let (connection_filter_body, ct_nodes) = gen_connection_filter(&ptree, &mut statics, pt_nodes);
175    let session_filter_body = gen_session_filter(&ptree, &mut statics, ct_nodes);
176
177    let lazy_statics = if statics.is_empty() {
178        quote! {}
179    } else {
180        quote! {
181            lazy_static::lazy_static! {
182                #( #statics )*
183            }
184        }
185    };
186
187    let packet_filter_fn = quote! {
188        #[inline]
189        fn packet_filter(mbuf: &retina_core::Mbuf) -> retina_core::filter::FilterResult {
190            #packet_filter_body
191        }
192    };
193
194    let connection_filter_fn = quote! {
195        #[inline]
196        fn connection_filter(conn: &retina_core::protocols::stream::ConnData) -> retina_core::filter::FilterResult {
197            #connection_filter_body
198        }
199    };
200
201    let session_filter_fn = quote! {
202        #[inline]
203        fn session_filter(session: &retina_core::protocols::stream::Session, idx: usize) -> bool {
204            #session_filter_body
205        }
206    };
207
208    let filtergen = quote! {
209        fn filter() -> retina_core::filter::FilterFactory {
210            #packet_filter_fn
211            #connection_filter_fn
212            #session_filter_fn
213            retina_core::filter::FilterFactory::new(#filter_str, packet_filter, connection_filter, session_filter)
214        }
215
216        #lazy_statics
217        #input
218
219    };
220    filtergen.into()
221}