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}