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}