tramex_tools/interface/association/
rules.rs1use crate::data::Trace;
4use crate::interface::layer::Layer;
5use crate::interface::types::Direction;
6
7#[derive(Debug, Clone, Copy, PartialEq, Default)]
9pub enum SearchDirection {
10 #[default]
12 BackwardFirst,
13 ForwardFirst,
15 BackwardOnly,
17 ForwardOnly,
19}
20
21impl SearchDirection {
22 pub fn from_direction(direction: &Direction) -> Self {
26 match direction {
27 Direction::UL | Direction::FROM => SearchDirection::BackwardFirst,
28 Direction::DL | Direction::TO => SearchDirection::ForwardFirst,
29 Direction::NA => SearchDirection::BackwardFirst,
30 }
31 }
32}
33
34pub trait AssociationRule: Send + Sync {
36 fn source_layer(&self) -> Layer;
38
39 fn target_layer(&self) -> Layer;
41
42 fn matches(&self, trace: &Trace, candidate: &Trace) -> bool;
51
52 fn preferred_direction(&self, source: &Trace) -> SearchDirection {
54 source
55 .additional_infos
56 .get_direction()
57 .map(|d| SearchDirection::from_direction(&d))
58 .unwrap_or_default()
59 }
60
61 fn window_size(&self) -> usize {
63 10 }
65
66 fn valid_source_messages(&self) -> &[&str] {
69 &[] }
71
72 fn valid_target_messages(&self) -> &[&str] {
75 &[] }
77
78 fn source_is_child(&self) -> bool {
82 true }
84}
85
86pub struct AssociationRules {
88 rules: Vec<Box<dyn AssociationRule>>,
90}
91
92impl Default for AssociationRules {
93 fn default() -> Self {
94 Self::new()
95 }
96}
97
98impl AssociationRules {
99 pub fn new() -> Self {
101 let rules: Vec<Box<dyn AssociationRule>> = vec![Box::new(NasToRrcRule::new()), Box::new(NgapToNasRule::new())];
102 Self { rules }
103 }
104
105 pub fn rules_for_layer(&self, layer: &Layer) -> Vec<&dyn AssociationRule> {
107 self.rules
108 .iter()
109 .filter(|r| &r.source_layer() == layer)
110 .map(|r| r.as_ref())
111 .collect()
112 }
113}
114
115#[derive(Debug, Default)]
119pub struct NasToRrcRule {
120 pub byte_match_length: usize,
123 pub valid_rrc_messages: &'static [&'static str],
125 pub valid_nas_messages: &'static [&'static str],
127}
128
129impl NasToRrcRule {
130 pub fn new() -> Self {
132 Self {
133 byte_match_length: 14, valid_rrc_messages: &[
135 "dl information transfer",
136 "ul information transfer",
137 "rrc setup complete",
138 "rrc reconfiguration",
139 ], valid_nas_messages: &["service request", "registration accept", "deregistration request"], }
142 }
143
144 fn extract_rrc_dedicated_nas_binary(&self, trace: &Trace) -> Option<Vec<u8>> {
147 let text = trace.text.as_ref()?;
148
149 for line in text.iter() {
150 let trimmed = line.trim();
151
152 if trimmed.contains("dedicatedNAS-Message") || trimmed.contains("nas-MessageContainer") {
155 if let Some(hex_start) = trimmed.find('\'')
158 && let Some(hex_end) = trimmed[hex_start + 1..].find('\'')
159 {
160 let hex_str = &trimmed[hex_start + 1..hex_start + 1 + hex_end];
161 return Self::hex_string_to_bytes(hex_str);
163 }
164 }
165 }
166 None
167 }
168
169 fn hex_string_to_bytes(hex_str: &str) -> Option<Vec<u8>> {
171 let hex_clean: String = hex_str.chars().filter(|c| c.is_ascii_hexdigit()).collect();
172 if !hex_clean.len().is_multiple_of(2) {
173 return None;
174 }
175
176 let mut bytes = Vec::new();
177 for i in (0..hex_clean.len()).step_by(2) {
178 if let Ok(byte) = u8::from_str_radix(&hex_clean[i..i + 2], 16) {
179 bytes.push(byte);
180 } else {
181 return None;
182 }
183 }
184
185 Some(bytes)
186 }
187}
188
189impl AssociationRule for NasToRrcRule {
190 fn source_layer(&self) -> Layer {
191 Layer::NAS
192 }
193
194 fn target_layer(&self) -> Layer {
195 Layer::RRC
196 }
197
198 fn valid_source_messages(&self) -> &[&str] {
199 self.valid_nas_messages
200 }
201
202 fn valid_target_messages(&self) -> &[&str] {
203 self.valid_rrc_messages
204 }
205
206 fn matches(&self, trace: &Trace, candidate: &Trace) -> bool {
207 let (nas_trace, rrc_trace) = if trace.layer == Layer::NAS && candidate.layer == Layer::RRC {
209 (trace, candidate)
210 } else if trace.layer == Layer::RRC && candidate.layer == Layer::NAS {
211 (candidate, trace)
212 } else {
213 return false;
214 };
215
216 let rrc_msg_name = rrc_trace
218 .additional_infos
219 .get_message_name()
220 .unwrap_or_default()
221 .to_lowercase();
222
223 let is_valid_candidate = self.valid_rrc_messages.iter().any(|&msg| rrc_msg_name == msg);
224
225 if !is_valid_candidate {
226 return false;
227 }
228
229 let nas_binary = match &nas_trace.binary {
231 Some(b) => b,
232 None => return false,
233 };
234
235 let rrc_nas_binary = match self.extract_rrc_dedicated_nas_binary(rrc_trace) {
237 Some(b) => b,
238 None => return false,
239 };
240
241 let match_len = self.byte_match_length.min(nas_binary.len()).min(rrc_nas_binary.len());
244 if match_len == 0 {
245 return false;
246 }
247
248 log::debug!("match found");
250 nas_binary[..match_len] == rrc_nas_binary[..match_len]
251 }
252}
253
254#[derive(Debug, Default)]
256pub struct NgapToNasRule {
257 pub byte_match_length: usize,
260 pub valid_ngap_messages: &'static [&'static str],
262 pub valid_nas_messages: &'static [&'static str],
264}
265
266impl NgapToNasRule {
267 pub fn new() -> Self {
269 Self {
270 byte_match_length: 14, valid_ngap_messages: &[
272 "initial ue message",
273 "downlink nas transport",
274 "uplink nas transport",
275 "initial context setup request",
276 ], valid_nas_messages: &[
278 "service request",
279 "registration accept",
280 "deregistration request",
281 "ul nas transport",
282 "dl nas transport",
283 ], }
285 }
286
287 fn extract_ngap_nas_binary(&self, trace: &Trace) -> Option<Vec<u8>> {
299 let text = trace.text.as_ref()?;
300
301 let mut found_nas_pdu = false;
302
303 for line in text.iter() {
304 let trimmed = line.trim();
305
306 if trimmed.contains("id-NAS-PDU") {
308 found_nas_pdu = true;
309 continue;
310 }
311
312 if found_nas_pdu && trimmed.starts_with("value ") {
314 if let Some(hex_start) = trimmed.find('\'')
316 && let Some(hex_end) = trimmed[hex_start + 1..].find('\'')
317 {
318 let hex_str = &trimmed[hex_start + 1..hex_start + 1 + hex_end];
319 return Self::hex_string_to_bytes(hex_str);
320 }
321 found_nas_pdu = false;
323 }
324
325 if found_nas_pdu && trimmed.starts_with('}') {
327 found_nas_pdu = false;
328 }
329 }
330 None
331 }
332
333 fn hex_string_to_bytes(hex_str: &str) -> Option<Vec<u8>> {
335 let hex_clean: String = hex_str.chars().filter(|c| c.is_ascii_hexdigit()).collect();
336
337 if !hex_clean.len().is_multiple_of(2) {
338 return None;
339 }
340
341 let mut bytes = Vec::new();
342 for i in (0..hex_clean.len()).step_by(2) {
343 if let Ok(byte) = u8::from_str_radix(&hex_clean[i..i + 2], 16) {
344 bytes.push(byte);
345 } else {
346 return None;
347 }
348 }
349
350 Some(bytes)
351 }
352}
353
354impl AssociationRule for NgapToNasRule {
355 fn source_layer(&self) -> Layer {
356 Layer::NGAP
357 }
358
359 fn target_layer(&self) -> Layer {
360 Layer::NAS
361 }
362
363 fn valid_source_messages(&self) -> &[&str] {
364 self.valid_ngap_messages
365 }
366
367 fn valid_target_messages(&self) -> &[&str] {
368 self.valid_nas_messages
369 }
370
371 fn source_is_child(&self) -> bool {
372 false }
374
375 fn preferred_direction(&self, source: &Trace) -> SearchDirection {
376 if source.additional_infos.get_direction().unwrap() == Direction::UL
377 || source.additional_infos.get_direction().unwrap() == Direction::TO
378 {
379 SearchDirection::BackwardFirst
380 } else {
381 SearchDirection::ForwardFirst
382 }
383 }
384
385 fn matches(&self, trace: &Trace, candidate: &Trace) -> bool {
386 if trace.layer != Layer::NGAP || candidate.layer != Layer::NAS {
388 return false;
389 }
390 let (ngap_trace, nas_trace) = (trace, candidate);
391
392 let nas_binary = match &nas_trace.binary {
394 Some(b) => b,
395 None => return false,
396 };
397
398 let ngap_nas_binary = match self.extract_ngap_nas_binary(ngap_trace) {
400 Some(b) => b,
401 None => return false,
402 };
403
404 let match_len = self.byte_match_length.min(nas_binary.len()).min(ngap_nas_binary.len());
407 if match_len == 0 {
408 return false;
409 }
410
411 log::debug!("match found");
413 nas_binary[..match_len] == ngap_nas_binary[..match_len]
414 }
415}