huginn_net_db/
tcp.rs

1use tracing::debug;
2
3#[derive(Clone, Debug, PartialEq)]
4pub struct Signature {
5    pub version: IpVersion,
6    /// initial TTL used by the OS.
7    pub ittl: Ttl,
8    /// length of IPv4 options or IPv6 extension headers.
9    pub olen: u8,
10    /// maximum segment size, if specified in TCP options.
11    pub mss: Option<u16>,
12    /// window size.
13    pub wsize: WindowSize,
14    /// window scaling factor, if specified in TCP options.
15    pub wscale: Option<u8>,
16    /// layout and ordering of TCP options, if any.
17    pub olayout: Vec<TcpOption>,
18    /// properties and quirks observed in IP or TCP headers.
19    pub quirks: Vec<Quirk>,
20    /// payload size classification
21    pub pclass: PayloadSize,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum TcpMatchQuality {
26    High,
27    Medium,
28    Low,
29}
30
31impl TcpMatchQuality {
32    pub fn as_score(self) -> u32 {
33        match self {
34            TcpMatchQuality::High => 0,
35            TcpMatchQuality::Medium => 1,
36            TcpMatchQuality::Low => 2,
37        }
38    }
39}
40
41impl crate::db_matching_trait::MatchQuality for TcpMatchQuality {
42    // TCP has 9 components, each can contribute max 2 points (Low)
43    const MAX_DISTANCE: u32 = 18;
44
45    fn distance_to_score(distance: u32) -> f32 {
46        match distance {
47            0 => 1.0,
48            1 => 0.95,
49            2 => 0.90,
50            3..=4 => 0.80,
51            5..=6 => 0.70,
52            7..=9 => 0.60,
53            10..=12 => 0.40,
54            13..=15 => 0.20,
55            d if d <= Self::MAX_DISTANCE => 0.10,
56            _ => 0.05,
57        }
58    }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub enum IpVersion {
63    V4,
64    V6,
65    Any,
66}
67impl IpVersion {
68    pub fn distance_ip_version(&self, other: &IpVersion) -> Option<u32> {
69        if other == &IpVersion::Any {
70            Some(TcpMatchQuality::High.as_score())
71        } else {
72            match (self, other) {
73                (IpVersion::V4, IpVersion::V4) | (IpVersion::V6, IpVersion::V6) => {
74                    Some(TcpMatchQuality::High.as_score())
75                }
76                _ => None,
77            }
78        }
79    }
80}
81
82/// Time To Live (TTL) representation used for OS fingerprinting and network distance calculation
83#[derive(Clone, Debug, PartialEq)]
84pub enum Ttl {
85    /// Raw TTL value when we don't have enough context to determine initial TTL
86    /// Contains the observed TTL value from the IP header
87    Value(u8),
88
89    /// TTL with calculated network distance
90    /// First u8 is the observed TTL value
91    /// Second u8 is the estimated number of hops (distance = initial_ttl - observed_ttl)
92    Distance(u8, u8),
93
94    /// TTL value that's been guessed based on common OS initial values
95    /// Contains the estimated initial TTL (e.g., 64 for Linux, 128 for Windows)
96    Guess(u8),
97
98    /// Invalid or problematic TTL value
99    /// Contains the raw TTL value that was deemed invalid (e.g., 0)
100    Bad(u8),
101}
102
103impl Ttl {
104    pub fn distance_ttl(&self, other: &Ttl) -> Option<u32> {
105        match (self, other) {
106            (Ttl::Value(a), Ttl::Value(b)) => {
107                if a == b {
108                    Some(TcpMatchQuality::High.as_score())
109                } else {
110                    Some(TcpMatchQuality::Low.as_score())
111                }
112            }
113            (Ttl::Distance(a1, a2), Ttl::Distance(b1, b2)) => {
114                if a1 == b1 && a2 == b2 {
115                    Some(TcpMatchQuality::High.as_score())
116                } else {
117                    Some(TcpMatchQuality::Low.as_score())
118                }
119            }
120            (Ttl::Distance(a1, a2), Ttl::Value(b1)) => {
121                if a1.saturating_add(*a2) == *b1 {
122                    Some(TcpMatchQuality::High.as_score())
123                } else {
124                    Some(TcpMatchQuality::Low.as_score())
125                }
126            }
127            (Ttl::Guess(a), Ttl::Guess(b)) => {
128                if a == b {
129                    Some(TcpMatchQuality::High.as_score())
130                } else {
131                    Some(TcpMatchQuality::Low.as_score())
132                }
133            }
134            (Ttl::Bad(a), Ttl::Bad(b)) => {
135                if a == b {
136                    Some(TcpMatchQuality::High.as_score())
137                } else {
138                    Some(TcpMatchQuality::Low.as_score())
139                }
140            }
141            (Ttl::Guess(a), Ttl::Value(b)) => {
142                if a == b {
143                    Some(TcpMatchQuality::High.as_score())
144                } else {
145                    Some(TcpMatchQuality::Low.as_score())
146                }
147            }
148            (Ttl::Value(a), Ttl::Distance(b1, b2)) => {
149                if *a == b1.saturating_add(*b2) {
150                    Some(TcpMatchQuality::High.as_score())
151                } else {
152                    Some(TcpMatchQuality::Low.as_score())
153                }
154            }
155            (Ttl::Value(a), Ttl::Guess(b)) => {
156                if a == b {
157                    Some(TcpMatchQuality::High.as_score())
158                } else {
159                    Some(TcpMatchQuality::Low.as_score())
160                }
161            }
162            _ => None,
163        }
164    }
165}
166
167/// TCP Window Size representation used for fingerprinting different TCP stacks
168#[derive(Clone, Debug, PartialEq)]
169pub enum WindowSize {
170    /// Window size is a multiple of MSS (Maximum Segment Size)
171    /// The u8 value represents the multiplier (e.g., Mss(4) means window = MSS * 4)
172    Mss(u8),
173
174    /// Window size is a multiple of MTU (Maximum Transmission Unit)
175    /// The u8 value represents the multiplier (e.g., Mtu(4) means window = MTU * 4)
176    Mtu(u8),
177
178    /// Raw window size value when it doesn't match any pattern
179    /// Contains the actual window size value from the TCP header
180    Value(u16),
181
182    /// Window size follows a modulo pattern
183    /// The u16 value represents the modulo base (e.g., Mod(1024) means window % 1024 == 0)
184    Mod(u16),
185
186    /// Represents any window size (wildcard matcher)
187    Any,
188}
189
190impl WindowSize {
191    pub fn distance_window_size(&self, other: &WindowSize, mss: Option<u16>) -> Option<u32> {
192        match (self, other) {
193            (WindowSize::Mss(a), WindowSize::Mss(b)) => {
194                if a == b {
195                    Some(TcpMatchQuality::High.as_score())
196                } else {
197                    Some(TcpMatchQuality::Low.as_score())
198                }
199            }
200            (WindowSize::Mtu(a), WindowSize::Mtu(b)) => {
201                if a == b {
202                    Some(TcpMatchQuality::High.as_score())
203                } else {
204                    Some(TcpMatchQuality::Low.as_score())
205                }
206            }
207            (WindowSize::Value(a), WindowSize::Mss(b)) => {
208                if let Some(mss_value) = mss {
209                    if let Some(ratio_other) = a.checked_div(mss_value) {
210                        if *b as u16 == ratio_other {
211                            debug!(
212                                "window size difference: a {}, b {} == ratio_other {}",
213                                a, b, ratio_other
214                            );
215                            Some(TcpMatchQuality::High.as_score())
216                        } else {
217                            Some(TcpMatchQuality::Low.as_score())
218                        }
219                    } else {
220                        Some(TcpMatchQuality::Low.as_score())
221                    }
222                } else {
223                    Some(TcpMatchQuality::Low.as_score())
224                }
225            }
226            (WindowSize::Mod(a), WindowSize::Mod(b)) => {
227                if a == b {
228                    Some(TcpMatchQuality::High.as_score())
229                } else {
230                    Some(TcpMatchQuality::Low.as_score())
231                }
232            }
233            (WindowSize::Value(a), WindowSize::Value(b)) => {
234                if a == b {
235                    Some(TcpMatchQuality::High.as_score())
236                } else {
237                    Some(TcpMatchQuality::Low.as_score())
238                }
239            }
240            (_, WindowSize::Any) => Some(TcpMatchQuality::High.as_score()),
241            _ => None,
242        }
243    }
244}
245
246#[derive(Clone, Debug, PartialEq)]
247pub enum TcpOption {
248    /// eol+n  - explicit end of options, followed by n bytes of padding
249    Eol(u8),
250    /// nop    - no-op option
251    Nop,
252    /// mss    - maximum segment size
253    Mss,
254    /// ws     - window scaling
255    Ws,
256    /// sok    - selective ACK permitted
257    Sok,
258    /// sack   - selective ACK (should not be seen)
259    Sack,
260    /// ts     - timestamp
261    TS,
262    /// ?n     - unknown option ID n
263    Unknown(u8),
264}
265
266#[derive(Clone, Debug, PartialEq)]
267pub enum Quirk {
268    /// df     - "don't fragment" set (probably PMTUD); ignored for IPv6
269    Df,
270    /// id+    - DF set but IPID non-zero; ignored for IPv6
271    NonZeroID,
272    /// id-    - DF not set but IPID is zero; ignored for IPv6
273    ZeroID,
274    /// ecn    - explicit congestion notification support
275    Ecn,
276    /// 0+     - "must be zero" field not zero; ignored for IPv6
277    MustBeZero,
278    /// flow   - non-zero IPv6 flow ID; ignored for IPv4
279    FlowID,
280    /// seq-   - sequence number is zero
281    SeqNumZero,
282    /// ack+   - ACK number is non-zero, but ACK flag not set
283    AckNumNonZero,
284    /// ack-   - ACK number is zero, but ACK flag set
285    AckNumZero,
286    /// uptr+  - URG pointer is non-zero, but URG flag not set
287    NonZeroURG,
288    /// urgf+  - URG flag used
289    Urg,
290    /// pushf+ - PUSH flag used
291    Push,
292    /// ts1-   - own timestamp specified as zero
293    OwnTimestampZero,
294    /// ts2+   - non-zero peer timestamp on initial SYN
295    PeerTimestampNonZero,
296    /// opt+   - trailing non-zero data in options segment
297    TrailinigNonZero,
298    /// exws   - excessive window scaling factor (> 14)
299    ExcessiveWindowScaling,
300    /// bad    - malformed TCP options
301    OptBad,
302}
303
304/// Classification of TCP payload sizes used in fingerprinting
305#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
306pub enum PayloadSize {
307    /// Packet has no payload (empty)
308    /// Common in SYN packets and some control messages
309    Zero,
310
311    /// Packet contains data in the payload
312    /// Typical for data transfer packets
313    NonZero,
314
315    /// Matches any payload size
316    /// Used as a wildcard in signature matching
317    Any,
318}
319
320impl PayloadSize {
321    pub fn distance_payload_size(&self, other: &PayloadSize) -> Option<u32> {
322        if other == &PayloadSize::Any || self == other {
323            Some(TcpMatchQuality::High.as_score())
324        } else {
325            None
326        }
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    #[test]
335    fn test_distance_ttl_matching_cases() {
336        assert_eq!(
337            Ttl::Value(64).distance_ttl(&Ttl::Value(64)),
338            Some(TcpMatchQuality::High.as_score())
339        );
340        assert_eq!(
341            Ttl::Distance(57, 7).distance_ttl(&Ttl::Distance(57, 7)),
342            Some(TcpMatchQuality::High.as_score())
343        );
344        assert_eq!(
345            Ttl::Distance(57, 7).distance_ttl(&Ttl::Value(64)),
346            Some(TcpMatchQuality::High.as_score())
347        );
348        assert_eq!(
349            Ttl::Guess(64).distance_ttl(&Ttl::Value(64)),
350            Some(TcpMatchQuality::High.as_score())
351        );
352    }
353
354    #[test]
355    fn test_distance_ttl_non_matching_cases() {
356        assert_eq!(
357            Ttl::Value(64).distance_ttl(&Ttl::Value(128)),
358            Some(TcpMatchQuality::Low.as_score())
359        );
360        assert_eq!(
361            Ttl::Distance(57, 7).distance_ttl(&Ttl::Value(128)),
362            Some(TcpMatchQuality::Low.as_score())
363        );
364        assert_eq!(
365            Ttl::Bad(0).distance_ttl(&Ttl::Bad(1)),
366            Some(TcpMatchQuality::Low.as_score())
367        );
368    }
369
370    #[test]
371    fn test_distance_ttl_additional_cases() {
372        assert_eq!(
373            Ttl::Value(64).distance_ttl(&Ttl::Distance(57, 7)),
374            Some(TcpMatchQuality::High.as_score())
375        );
376        assert_eq!(
377            Ttl::Value(64).distance_ttl(&Ttl::Guess(64)),
378            Some(TcpMatchQuality::High.as_score())
379        );
380        assert_eq!(
381            Ttl::Value(64).distance_ttl(&Ttl::Distance(60, 7)),
382            Some(TcpMatchQuality::Low.as_score())
383        );
384    }
385
386    #[test]
387    fn test_distance_ttl_incompatible_types() {
388        assert_eq!(Ttl::Bad(0).distance_ttl(&Ttl::Value(64)), None);
389        assert_eq!(Ttl::Distance(64, 7).distance_ttl(&Ttl::Bad(0)), None);
390        assert_eq!(Ttl::Guess(64).distance_ttl(&Ttl::Distance(64, 7)), None);
391    }
392}