Skip to main content

packet_strata/tracker/
direction.rs

1use crate::packet::{header::TransportLayer, icmp::IcmpType, icmp6::Icmp6Type, Packet};
2use std::{cmp::Ordering, fmt};
3
4// Well-known ports
5const PORT_DNS: u16 = 53;
6const PORT_DHCP_SERVER: u16 = 67;
7const PORT_DHCP_CLIENT: u16 = 68;
8const PORT_NTP: u16 = 123;
9const PORT_NETBIOS_NS: u16 = 137;
10const PORT_NETBIOS_DGM: u16 = 138;
11const PORT_SNMP: u16 = 161;
12const PORT_SNMP_TRAP: u16 = 162;
13const PORT_CLDAP: u16 = 389;
14const PORT_HTTPS: u16 = 443;
15const PORT_IKE: u16 = 500;
16const PORT_SYSLOG: u16 = 514;
17const PORT_RIP: u16 = 520;
18const PORT_DHCPV6_CLIENT: u16 = 546;
19const PORT_DHCPV6_SERVER: u16 = 547;
20const PORT_OPENVPN: u16 = 1194;
21const PORT_SSDP: u16 = 1900;
22const PORT_IPSEC_NATT: u16 = 4500;
23const PORT_MDNS: u16 = 5353;
24const PORT_LLMNR: u16 = 5355;
25const PORT_HTTPS_ALT: u16 = 8443;
26const PORT_HTTP: u16 = 80;
27const PORT_HTTP_ALT: u16 = 8080;
28const PORT_STUN: u16 = 3478;
29const PORT_STUN_TLS: u16 = 5349;
30
31// Protocol constants
32const DNS_QR_BIT_MASK: u8 = 0x80;
33const NTP_MODE_MASK: u8 = 0x07;
34const TLS_HANDSHAKE_CONTENT_TYPE: u8 = 0x16;
35const TLS_CLIENT_HELLO: u8 = 0x01;
36const TLS_SERVER_HELLO: u8 = 0x02;
37
38/// Represents the direction of a packet in a flow.
39///
40/// Direction is determined from the perspective of the client-server model:
41/// - `Upwards`: From client to server (request/query/initiator)
42/// - `Downwards`: From server to client (response/reply)
43///
44/// For stateless analysis (mid-connection packets), the first packet seen
45/// is assumed to be from the initiator and therefore `Upwards`.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
47pub enum PacketDirection {
48    #[default]
49    Upwards,
50    Downwards,
51}
52
53impl fmt::Display for PacketDirection {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        match self {
56            PacketDirection::Upwards => write!(f, "Upwards"),
57            PacketDirection::Downwards => write!(f, "Downwards"),
58        }
59    }
60}
61
62impl PacketDirection {
63    /// Infers the direction of a packet based on transport layer information.
64    ///
65    /// This method determines if a packet is `Upwards` (Client -> Server) or `Downwards` (Server -> Client) using:
66    /// - **TCP**: Analysis of SYN/SYN-ACK flags and well-known ports.
67    /// - **UDP**: Well-known ports (DNS, DHCP, NTP) and lightweight payload inspection (DPI Lite).
68    /// - **ICMP**: Message types (e.g., Echo Request vs Echo Reply).
69    ///
70    /// If the direction cannot be definitively determined, it defaults to `Upwards`.
71    pub fn infer(pkt: &Packet<'_>) -> PacketDirection {
72        let transport = pkt.transport();
73        match transport {
74            Some(TransportLayer::Tcp(tcp)) => {
75                // TCP: Use SYN/SYN-ACK for connection establishment
76                if tcp.header.has_syn() {
77                    if tcp.header.has_ack() {
78                        return PacketDirection::Downwards; // SYN-ACK
79                    } else {
80                        return PacketDirection::Upwards; // SYN
81                    }
82                }
83
84                return Self::infer_direction_tcp(tcp.src_port(), tcp.dst_port(), pkt.data());
85            }
86            Some(TransportLayer::Udp(udp)) => {
87                Self::infer_direction_udp(udp.src_port(), udp.dst_port(), pkt.data())
88            }
89
90            Some(TransportLayer::Icmp(icmp)) => match icmp.icmp_type() {
91                IcmpType::ECHO => PacketDirection::Upwards,
92                IcmpType::ECHO_REPLY => PacketDirection::Downwards,
93                IcmpType::TIMESTAMP => PacketDirection::Upwards,
94                IcmpType::TIMESTAMP_REPLY => PacketDirection::Downwards,
95                IcmpType::INFO_REQUEST => PacketDirection::Upwards,
96                IcmpType::INFO_REPLY => PacketDirection::Downwards,
97                IcmpType::ADDRESS => PacketDirection::Upwards,
98                IcmpType::ADDRESS_REPLY => PacketDirection::Downwards,
99                IcmpType::EX_ECHO => PacketDirection::Upwards,
100                IcmpType::EX_ECHO_REPLY => PacketDirection::Downwards,
101                IcmpType::DEST_UNREACH => PacketDirection::Upwards,
102                IcmpType::SOURCE_QUENCH => PacketDirection::Upwards,
103                IcmpType::REDIRECT => PacketDirection::Upwards,
104                IcmpType::ROUTER_ADV => PacketDirection::Downwards,
105                IcmpType::ROUTER_SOLICIT => PacketDirection::Upwards,
106                IcmpType::TIME_EXCEEDED => PacketDirection::Upwards,
107                IcmpType::PARAMETER_PROBLEM => PacketDirection::Upwards,
108                _ => PacketDirection::Upwards,
109            },
110            Some(TransportLayer::Icmp6(icmp6)) => match icmp6.icmp6_type() {
111                Icmp6Type::DST_UNREACH => PacketDirection::Upwards,
112                Icmp6Type::PACKET_TOO_BIG => PacketDirection::Upwards,
113                Icmp6Type::TIME_EXCEEDED => PacketDirection::Upwards,
114                Icmp6Type::PARAM_PROB => PacketDirection::Upwards,
115                Icmp6Type::ECHO_REQUEST => PacketDirection::Upwards,
116                Icmp6Type::ECHO_REPLY => PacketDirection::Downwards,
117                Icmp6Type::MLD_LISTENER_QUERY => PacketDirection::Downwards,
118                Icmp6Type::MLD_LISTENER_REPORT => PacketDirection::Upwards,
119                Icmp6Type::MLD_LISTENER_REDUCTION => PacketDirection::Upwards,
120                Icmp6Type::ROUTER_SOLICITATION => PacketDirection::Upwards,
121                Icmp6Type::ROUTER_ADVERTISEMENT => PacketDirection::Downwards,
122                Icmp6Type::NEIGHBOR_SOLICITATION => PacketDirection::Upwards,
123                Icmp6Type::NEIGHBOR_ADVERTISEMENT => PacketDirection::Downwards,
124                Icmp6Type::REDIRECT_MESSAGE => PacketDirection::Upwards,
125                Icmp6Type::ROUTER_RENUMBERING => PacketDirection::Downwards,
126                Icmp6Type::NODE_INFORMATION_QUERY => PacketDirection::Upwards,
127                Icmp6Type::NODE_INFORMATION_RESPONSE => PacketDirection::Downwards,
128                Icmp6Type::INVERSE_NEIGHBOR_DISCOVERY_SOLICITATION => PacketDirection::Upwards,
129                Icmp6Type::INVERSE_NEIGHBOR_DISCOVERY_ADVERTISEMENT => PacketDirection::Downwards,
130                Icmp6Type::MULTICAST_LISTENER_DISCOVERY_REPORTS => PacketDirection::Upwards,
131                Icmp6Type::HOME_AGENT_ADDRESS_DISCOVERY_REQUEST => PacketDirection::Upwards,
132                Icmp6Type::HOME_AGENT_ADDRESS_DISCOVERY_REPLY => PacketDirection::Downwards,
133                Icmp6Type::MOBILE_PREFIX_SOLICITATION => PacketDirection::Upwards,
134                Icmp6Type::MOBILE_PREFIX_ADVERTISEMENT => PacketDirection::Downwards,
135                Icmp6Type::MULTICAST_ROUTER_SOLICITATION => PacketDirection::Upwards,
136                Icmp6Type::MULTICAST_ROUTER_TERMINATION => PacketDirection::Upwards,
137                Icmp6Type::FMIPV6 => PacketDirection::Upwards,
138                Icmp6Type::RPL_CONTROL_MESSAGE => PacketDirection::Upwards,
139                Icmp6Type::ILNPV6_LOCATOR_UPDATE => PacketDirection::Upwards,
140                Icmp6Type::DUPLICATE_ADDRESS_REQUEST => PacketDirection::Upwards,
141                Icmp6Type::DUPLICATE_ADDRESS_CONFIRM => PacketDirection::Downwards,
142                Icmp6Type::MPL_CONTROL_MESSAGE => PacketDirection::Upwards,
143                Icmp6Type::EXTENDED_ECHO_REQUEST => PacketDirection::Upwards,
144                Icmp6Type::EXTENDED_ECHO_REPLY => PacketDirection::Downwards,
145                _ => PacketDirection::Upwards,
146            },
147            Some(TransportLayer::Sctp(_)) | None => PacketDirection::Upwards,
148        }
149    }
150
151    /// Simplified UDP direction inference: port-based + minimal DPI (max 2 bytes)
152    ///
153    /// DPI Lite checks:
154    /// - DHCP: port pairs
155    /// - DNS: QR bit (byte 2)
156    /// - NTP: mode field (byte 0)
157    /// - Payload length heuristic for symmetric traffic
158    fn infer_direction_udp(source: u16, dest: u16, data: &[u8]) -> PacketDirection {
159        // --- 1. Exact Port Pairs ---
160        match (source, dest) {
161            (PORT_DHCP_CLIENT, PORT_DHCP_SERVER) | (PORT_DHCPV6_CLIENT, PORT_DHCPV6_SERVER) => {
162                return PacketDirection::Upwards;
163            }
164            (PORT_DHCP_SERVER, PORT_DHCP_CLIENT) | (PORT_DHCPV6_SERVER, PORT_DHCPV6_CLIENT) => {
165                return PacketDirection::Downwards;
166            }
167            _ => {}
168        }
169
170        // --- 2. Minimal DPI ---
171
172        // Extended DNS family (DNS, mDNS, LLMNR, NetBIOS NS)
173        if (matches!(source, PORT_DNS | PORT_MDNS | PORT_LLMNR | PORT_NETBIOS_NS)
174            || matches!(dest, PORT_DNS | PORT_MDNS | PORT_LLMNR | PORT_NETBIOS_NS))
175            && source != dest
176            && data.len() >= 3
177        {
178            let is_response = (data[2] & DNS_QR_BIT_MASK) != 0;
179            return if is_response { PacketDirection::Downwards } else { PacketDirection::Upwards };
180        }
181
182        // SSDP (UDP 1900)
183        if (source == PORT_SSDP || dest == PORT_SSDP) && data.len() >= 8 {
184            if data.starts_with(b"HTTP/1.") {
185                return PacketDirection::Downwards;
186            }
187            if data.starts_with(b"M-SE") {
188                return PacketDirection::Upwards;
189            }
190        }
191
192        // QUIC (UDP 443)
193        if (source == PORT_HTTPS || dest == PORT_HTTPS) && data.len() >= 1200 {
194            if (data[0] & 0xC0) == 0xC0 {
195                return PacketDirection::Upwards;
196            }
197        }
198
199        // STUN / WebRTC (UDP 3478, 5349)
200        if (source == PORT_STUN || dest == PORT_STUN || source == PORT_STUN_TLS || dest == PORT_STUN_TLS) && data.len() >= 20 {
201            if (data[0] & 0xC0) == 0x00 {
202                let msg_type = ((data[0] as u16) << 8) | (data[1] as u16);
203                let is_response = (msg_type & 0x0110) != 0;
204                return if is_response { PacketDirection::Downwards } else { PacketDirection::Upwards };
205            }
206        }
207
208        // NTP - Mode field in byte 0 (bits 0-2)
209        if (source == PORT_NTP || dest == PORT_NTP) && !data.is_empty() {
210            match data[0] & NTP_MODE_MASK {
211                1 | 3 => return PacketDirection::Upwards,   // Symmetric Active, Client
212                2 | 4 | 5 => return PacketDirection::Downwards, // Symmetric Passive, Server, Broadcast
213                _ => {
214                    if source != dest {
215                        return if dest == PORT_NTP { PacketDirection::Upwards } else { PacketDirection::Downwards };
216                    }
217                }
218            }
219        }
220
221        // --- 3. Payload Length Heuristic for Symmetric Traffic ---
222        if source == dest {
223            let threshold = match source {
224                PORT_DNS => Some(64),          // DNS (queries typically < 64 bytes)
225                PORT_NTP => Some(48),          // NTP (request = 48 bytes exactly in v3/v4)
226                PORT_NETBIOS_NS => Some(60),   // NetBIOS Name Service
227                PORT_NETBIOS_DGM => Some(100), // NetBIOS Datagram
228                PORT_SNMP => Some(80),         // SNMP
229                PORT_SNMP_TRAP => Some(80),    // SNMP Traps
230                PORT_CLDAP => Some(150),       // CLDAP
231                PORT_IKE => Some(200),         // IKE (initiator packets often smaller in phase 1)
232                PORT_SYSLOG => Some(200), // Syslog (assume larger = more log data = server aggregating)
233                PORT_RIP => Some(60),     // RIP (requests are smaller)
234                PORT_OPENVPN => Some(100), // OpenVPN
235                PORT_SSDP => Some(200),   // SSDP (M-SEARCH requests are small)
236                PORT_IPSEC_NATT => Some(200), // IPsec NAT-T
237                PORT_MDNS => Some(80),    // mDNS
238                PORT_LLMNR => Some(64),   // LLMNR
239                _ => None,
240            };
241
242            if let Some(t) = threshold {
243                return if data.len() <= t {
244                    PacketDirection::Upwards
245                } else {
246                    PacketDirection::Downwards
247                };
248            }
249        }
250
251        // --- 4. Port Rank Logic ---
252        Self::infer_direction_from_ports(source, dest).unwrap_or(PacketDirection::Upwards)
253    }
254
255    /// Simplified TCP direction inference: port-based + minimal DPI (max 2 bytes)
256    ///
257    /// DPI Lite checks:
258    /// - TLS: ContentType (byte 0) + Handshake type (byte 5)
259    /// - DNS over TCP: QR bit (byte 4, after 2-byte length prefix)
260    fn infer_direction_tcp(source: u16, dest: u16, data: &[u8]) -> PacketDirection {
261        // --- 1. Minimal DPI (port + max 2 bytes) ---
262
263        // HTTP (TCP 80, 8080)
264        if (source == PORT_HTTP || dest == PORT_HTTP || source == PORT_HTTP_ALT || dest == PORT_HTTP_ALT) && data.len() >= 8 {
265            if data.starts_with(b"HTTP/1.") {
266                return PacketDirection::Downwards;
267            }
268            if data.starts_with(b"GET ") || data.starts_with(b"POST") || data.starts_with(b"PUT ") || data.starts_with(b"HEAD") {
269                return PacketDirection::Upwards;
270            }
271        }
272
273        // TLS - ContentType (byte 0) and Handshake type (byte 5)
274        if (source == PORT_HTTPS
275            || dest == PORT_HTTPS
276            || source == PORT_HTTPS_ALT
277            || dest == PORT_HTTPS_ALT)
278            && data.len() >= 6
279        {
280            if data[0] == TLS_HANDSHAKE_CONTENT_TYPE {
281                // ContentType 0x16 = Handshake
282                let handshake_type = data[5];
283                return match handshake_type {
284                    TLS_CLIENT_HELLO => PacketDirection::Upwards, // ClientHello
285                    TLS_SERVER_HELLO => PacketDirection::Downwards, // ServerHello
286                    _ => {
287                        if dest == PORT_HTTPS || dest == PORT_HTTPS_ALT {
288                            PacketDirection::Upwards
289                        } else {
290                            PacketDirection::Downwards
291                        }
292                    }
293                };
294            }
295        }
296
297        // DNS over TCP - QR bit at byte 4 (after 2-byte length prefix, then byte 2 of DNS)
298        if (source == PORT_DNS || dest == PORT_DNS) && data.len() >= 5 {
299            let is_response = (data[4] & DNS_QR_BIT_MASK) != 0;
300            return if is_response {
301                PacketDirection::Downwards
302            } else {
303                PacketDirection::Upwards
304            };
305        }
306
307        // --- 2. Port Rank Logic ---
308        Self::infer_direction_from_ports(source, dest).unwrap_or(PacketDirection::Upwards)
309    }
310
311    /// Infers direction based on port hierarchy: System (≤1024) < User (1025-49151) < Dynamic (>49151)
312    /// Falls back to absolute port number comparison if ranks are equal
313    fn infer_direction_from_ports(source: u16, dest: u16) -> Option<PacketDirection> {
314        if source == dest {
315            return None; // Could not determine direction from identical ports.
316        }
317
318        let port_rank = |p: u16| -> u8 {
319            match p {
320                0..=1024 => 0,
321                1025..=49151 => 1,
322                49152..=u16::MAX => 2,
323            }
324        };
325
326        match port_rank(source).cmp(&port_rank(dest)) {
327            Ordering::Greater => Some(PacketDirection::Upwards), // Client (high rank) → Server (low rank)
328            Ordering::Less => Some(PacketDirection::Downwards), // Server (low rank) → Client (high rank)
329            Ordering::Equal => {
330                if source > dest {
331                    Some(PacketDirection::Upwards)
332                } else {
333                    Some(PacketDirection::Downwards)
334                }
335            }
336        }
337    }
338}