Skip to main content

iris_core/conntrack/conn/
conn_state.rs

1//! Management for per-connection state machines.
2use serde::{Deserialize, Serialize};
3use std::{cmp::Ordering, str::FromStr};
4use strum::IntoEnumIterator;
5use strum_macros::EnumIter;
6
7use crate::{
8    conntrack::Layer,
9    protocols::{stream::SessionProto, Session},
10};
11
12#[doc(hidden)]
13/// Current state of the Layer in per-connection state machine.
14/// Based on what it has seen so far in the connection.
15#[derive(PartialEq, Eq, Debug, Copy, Clone, Ord, PartialOrd, Hash, EnumIter)]
16pub enum LayerState {
17    /// Determining protocol
18    /// For L4, this indicates pre-handshake
19    Discovery,
20    /// Headers (TCP hshk, TLS hshk, HTTP hdrs, etc.)
21    /// Contains number of packets seen in headers.
22    Headers,
23    /// Headers done; new packets expected to be in layer payload
24    Payload,
25    /// This Layer and all child layers* should no longer
26    /// receive packets. This will be set based on the
27    /// result of a filter.
28    None,
29}
30
31/// The possible state transitions that a data type, filter, or callback
32/// (function and/or struct) can be associated with.
33/// Developers should refer to these in macros.
34#[derive(
35    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, EnumIter, Serialize, Deserialize,
36)]
37#[repr(u8)]
38pub enum StateTransition {
39    /// On first packet in connection
40    L4FirstPacket = 0,
41    /// Observed and reassembled SYNs and ACKs from both sides.
42    /// Note that this does not validate sequence numbers of these SYNs/ACKs.
43    /// Additionally, this should not be used to indicate the beginning
44    /// of payload, as payload may overlap with the handshake.
45    L4EndHshk,
46
47    /// Streaming anywhere in L4 connection, including TCP handshake.
48    /// Streaming anywhere in TCP or UDP connection, including TCP handshake.
49    // Packets are not TCP-reassembled.
50    InL4Conn,
51    /// Streaming anywhere in TCP or UDP connection, including TCP handshake.
52    /// Packets are TCP-reassembled.
53    InL4Stream,
54
55    /// On L7 protocol identification
56    L7OnDisc,
57    /// On L6/L7 headers parsed
58    L7EndHdrs,
59    /// L4 connection terminated by FIN/ACK sequence or timeout
60    L4Terminated,
61
62    /// Packet-level datatype. Any datatype tagged with this
63    /// is built from a single, connectionless packet (Mbuf).
64    ///
65    /// Note: Iris currently does not support subscriptions that are not tied to
66    /// a connection. Callbacks and filters should use `InL4Conn` to request
67    /// packet-level data types.
68    ///
69    /// Packet-level filters cannot be combined with datatypes or callbacks
70    /// that require connection/session tracking. Packet-level datatypes can only be
71    /// requested in higher-level filters/callbacks if a streaming level
72    /// (e.g., InL4Conn) is specified.
73    ///
74    /// Internal notes:
75    /// - This must be last in the enum variant list.
76    /// - In the connection tracker, this is used as a no-op state transition.
77    Packet,
78}
79
80impl std::fmt::Display for StateTransition {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        match self {
83            Self::InL4Conn => write!(f, "InL4Conn"),
84            Self::InL4Stream => write!(f, "InL4Stream"),
85            _ => write!(f, "{:?}", self),
86        }
87    }
88}
89
90// https://doc.rust-lang.org/reference/items/enumerations.html#casting
91impl StateTransition {
92    #[doc(hidden)]
93    pub fn as_usize(&self) -> usize {
94        *self as u8 as usize
95    }
96
97    #[doc(hidden)]
98    pub fn from_usize(i: usize) -> Self {
99        for level in StateTransition::iter() {
100            if level.as_usize() == i {
101                return level;
102            }
103        }
104        panic!("Cannot build StateTransition from {}", i);
105    }
106
107    #[doc(hidden)]
108    pub fn name(&self) -> &str {
109        match self {
110            StateTransition::L4FirstPacket => "L4FirstPacket",
111            StateTransition::L4EndHshk => "L4EndHshk",
112            StateTransition::InL4Conn => "InL4Conn",
113            StateTransition::InL4Stream => "InL4Stream",
114            StateTransition::L4Terminated => "L4Terminated",
115            StateTransition::L7OnDisc => "L7OnDisc",
116            StateTransition::L7EndHdrs => "L7EndHdrs",
117            StateTransition::Packet => "L4Pdu",
118        }
119    }
120
121    #[doc(hidden)]
122    pub fn in_transport(&self) -> bool {
123        self.name().contains("L4")
124    }
125
126    #[doc(hidden)]
127    pub fn is_streaming(&self) -> bool {
128        self.name().contains("In")
129    }
130
131    #[doc(hidden)]
132    pub fn layer_idx(&self) -> Option<usize> {
133        if self.name().contains("L7") {
134            return Some(0);
135        }
136        None
137    }
138
139    /// Returns Greater if self > Other, Less if self < Other, Equal if self == Other,
140    /// and Unknown if the two cannot be compared (different layers).
141    #[doc(hidden)]
142    pub fn compare(&self, other: &StateTransition) -> StateTxOrd {
143        if self == other {
144            return StateTxOrd::Equal;
145        }
146        // L4Pdu is any
147        if matches!(self, StateTransition::Packet) || matches!(other, StateTransition::Packet) {
148            return StateTxOrd::Any;
149        }
150
151        // End of connection is always greatest
152        if matches!(self, StateTransition::L4Terminated)
153            || matches!(other, StateTransition::L4Terminated)
154        {
155            return StateTxOrd::from_ord(self.cmp(other));
156        }
157        // Start of connection is always lowest
158        if matches!(self, StateTransition::L4FirstPacket)
159            || matches!(other, StateTransition::L4FirstPacket)
160        {
161            return StateTxOrd::from_ord(self.cmp(other));
162        }
163
164        // TCP handshake must complete before anything
165        // that requires data from both endpoints
166        if (matches!(self, StateTransition::L4EndHshk)
167            || matches!(other, StateTransition::L4EndHshk))
168            && (matches!(self, StateTransition::L7EndHdrs)
169                || matches!(other, StateTransition::L7EndHdrs))
170        {
171            return StateTxOrd::from_ord(self.cmp(other));
172        }
173
174        // Different layers
175        if self.name().contains("L4") && !other.name().contains("L4")
176            || self.name().contains("L7") && !other.name().contains("L7")
177        {
178            return StateTxOrd::Unknown;
179        }
180
181        // Exceptions to the ordering rule
182        if matches!(self, StateTransition::L4EndHshk) {
183            return StateTxOrd::Unknown;
184        }
185
186        // Streaming states throughout connection
187        if matches!(
188            self,
189            StateTransition::InL4Conn | StateTransition::InL4Stream
190        ) || matches!(
191            self,
192            StateTransition::InL4Stream | StateTransition::InL4Conn
193        ) {
194            return StateTxOrd::Unknown;
195        }
196
197        // Enum must be in listed order above.
198        StateTxOrd::from_ord(self.cmp(other))
199    }
200}
201
202/// Ordering of State Transitions in connection processing lifetime.
203/// Used to insert filter predicates in trees.
204/// Note: within a Layer, the StateTransitions have to be listed in order.
205#[doc(hidden)]
206#[derive(Debug, Eq, PartialEq)]
207pub enum StateTxOrd {
208    /// No ordering guaranteed (e.g., L4InPayload + L7OnDisc)
209    Unknown,
210    /// Not applicable
211    Any,
212    /// `self` > `other`
213    Greater,
214    /// `self` < `other`
215    Less,
216    /// `self`` == `other`
217    Equal,
218}
219
220impl StateTxOrd {
221    pub(crate) fn from_ord(ordering: Ordering) -> StateTxOrd {
222        match ordering {
223            Ordering::Greater => StateTxOrd::Greater,
224            Ordering::Less => StateTxOrd::Less,
225            Ordering::Equal => StateTxOrd::Equal,
226        }
227    }
228}
229
230/// The State Transitions that a connection can encounter.
231/// For `InX` Levels, the state transition is triggered if
232/// a streaming callback or filter changed match state (i.e.,
233/// was and is no longer active).
234/// Number of variants; used to size the `refresh_at` array
235pub(crate) const NUM_STATE_TRANSITIONS: usize = 7;
236
237/// State Transitions with associated data,
238/// used as wrappers for users to request as a parameter to a function.
239#[derive(Debug)]
240pub enum StateTxData<'a> {
241    L7OnDisc(SessionProto),
242    L7EndHdrs(&'a Session),
243    Null,
244}
245
246impl<'a> StateTxData<'a> {
247    #[doc(hidden)]
248    pub fn from_tx(state: &StateTransition, layer: &'a Layer) -> Self {
249        match layer {
250            Layer::L7(layer) => match state {
251                StateTransition::L7OnDisc => Self::L7OnDisc(layer.get_protocol()),
252                StateTransition::L7EndHdrs => {
253                    Self::L7EndHdrs(layer.sessions.last().expect("L7EndHdrs without session"))
254                }
255                _ => Self::Null,
256            },
257        }
258    }
259
260    // Should be the same as the corresponding StateTransition. For testing only.
261    #[allow(dead_code)]
262    pub(crate) fn as_usize(&self) -> usize {
263        match self {
264            StateTxData::L7OnDisc(_) => StateTransition::L7OnDisc.as_usize(),
265            StateTxData::L7EndHdrs(_) => StateTransition::L7EndHdrs.as_usize(),
266            StateTxData::Null => panic!("Invalid StateTxData"),
267        }
268    }
269}
270
271#[doc(hidden)]
272impl FromStr for StateTransition {
273    type Err = String;
274    fn from_str(s: &str) -> Result<Self, String> {
275        match s {
276            "L4FirstPacket" => Ok(StateTransition::L4FirstPacket),
277            "L4EndHshk" => Ok(StateTransition::L4EndHshk),
278            "InL4Conn" => Ok(StateTransition::InL4Conn),
279            "InL4Stream" => Ok(StateTransition::InL4Stream),
280            // Backward compat: old API used `InL4Conn(reassemble)` to request TCP reassembly.
281            "InL4Conn(reassemble)" => Ok(StateTransition::InL4Stream),
282            "L4Terminated" => Ok(StateTransition::L4Terminated),
283            "L7OnDisc" => Ok(StateTransition::L7OnDisc),
284            "L7EndHdrs" => Ok(StateTransition::L7EndHdrs),
285            "Packet" => Ok(StateTransition::Packet),
286            _ => Err(format!("Invalid StateTransition: {}", s)),
287        }
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn test_data_level_raw() {
297        assert_eq!(
298            StateTransition::Packet.as_usize(),
299            NUM_STATE_TRANSITIONS,
300            "{} != {}",
301            StateTransition::Packet.as_usize(),
302            NUM_STATE_TRANSITIONS
303        );
304    }
305}