packet_strata/tracker/
direction.rs

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