1use crate::datalink::DataLinkType;
16use crate::util::crc16_mstp;
17
18#[derive(Debug, Clone)]
20pub struct ValidationResult {
21 pub is_valid: bool,
23 pub link_type: Option<DataLinkType>,
25 pub frame_size: usize,
27 pub errors: Vec<ValidationError>,
29 pub warnings: Vec<ValidationWarning>,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum ValidationError {
36 FrameTooShort { size: usize, minimum: usize },
38 FrameTooLong { size: usize, maximum: usize },
40 InvalidPreamble { expected: Vec<u8>, found: Vec<u8> },
42 CrcMismatch { expected: u32, calculated: u32 },
44 InvalidFrameType { value: u8 },
46 InvalidAddress { address: String, reason: String },
48 InvalidHeader { reason: String },
50 PayloadSizeMismatch { declared: usize, actual: usize },
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum ValidationWarning {
57 UnusualFrameSize { size: usize },
59 DeprecatedFrameType { frame_type: u8 },
61 NonStandardConfiguration { reason: String },
63 SecurityWarning { reason: String },
65}
66
67pub fn validate_bacnet_ip_frame(data: &[u8]) -> ValidationResult {
69 let mut result = ValidationResult {
70 is_valid: true,
71 link_type: Some(DataLinkType::BacnetIp),
72 frame_size: data.len(),
73 errors: Vec::new(),
74 warnings: Vec::new(),
75 };
76
77 if data.len() < 4 {
79 result.is_valid = false;
80 result.errors.push(ValidationError::FrameTooShort {
81 size: data.len(),
82 minimum: 4,
83 });
84 return result;
85 }
86
87 if data[0] != 0x81 {
89 result.is_valid = false;
90 result.errors.push(ValidationError::InvalidPreamble {
91 expected: vec![0x81],
92 found: vec![data[0]],
93 });
94 }
95
96 let valid_functions = [
98 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
99 ];
100 if !valid_functions.contains(&data[1]) {
101 result.is_valid = false;
102 result
103 .errors
104 .push(ValidationError::InvalidFrameType { value: data[1] });
105 }
106
107 let declared_length = ((data[2] as usize) << 8) | (data[3] as usize);
109 if declared_length != data.len() {
110 result.is_valid = false;
111 result.errors.push(ValidationError::PayloadSizeMismatch {
112 declared: declared_length,
113 actual: data.len(),
114 });
115 }
116
117 if data.len() > 1497 {
119 result.is_valid = false;
120 result.errors.push(ValidationError::FrameTooLong {
121 size: data.len(),
122 maximum: 1497,
123 });
124 }
125
126 match data[1] {
128 0x0C => {
129 result.warnings.push(ValidationWarning::SecurityWarning {
131 reason: "Secure BVLL should use proper encryption".into(),
132 });
133 }
134 0x01 | 0x08 => {
135 result.warnings.push(ValidationWarning::SecurityWarning {
137 reason: "Table modification functions should be authenticated".into(),
138 });
139 }
140 _ => {}
141 }
142
143 result
144}
145
146pub fn validate_ethernet_frame(data: &[u8]) -> ValidationResult {
148 let mut result = ValidationResult {
149 is_valid: true,
150 link_type: Some(DataLinkType::Ethernet),
151 frame_size: data.len(),
152 errors: Vec::new(),
153 warnings: Vec::new(),
154 };
155
156 if data.len() < 17 {
158 result.is_valid = false;
159 result.errors.push(ValidationError::FrameTooShort {
160 size: data.len(),
161 minimum: 17,
162 });
163 return result;
164 }
165
166 if data.len() > 1514 {
168 result.is_valid = false;
169 result.errors.push(ValidationError::FrameTooLong {
170 size: data.len(),
171 maximum: 1514,
172 });
173 }
174
175 let ether_type = ((data[12] as u16) << 8) | (data[13] as u16);
177 if ether_type != 0x82DC {
178 result.is_valid = false;
179 result.errors.push(ValidationError::InvalidHeader {
180 reason: format!(
181 "Invalid Ethernet type: 0x{:04X}, expected 0x82DC",
182 ether_type
183 ),
184 });
185 }
186
187 if data.len() >= 17 {
189 let llc = &data[14..17];
190 if llc != [0x82, 0x82, 0x03] {
191 result.is_valid = false;
192 result.errors.push(ValidationError::InvalidHeader {
193 reason: format!("Invalid LLC header: {:02X?}, expected [82, 82, 03]", llc),
194 });
195 }
196 }
197
198 if data[0] & 0x01 == 0x01 {
200 if data[0..6] == [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF] {
201 } else {
203 result
205 .warnings
206 .push(ValidationWarning::NonStandardConfiguration {
207 reason: "Multicast address used instead of broadcast".into(),
208 });
209 }
210 }
211
212 if data.len() == 60 {
214 result
215 .warnings
216 .push(ValidationWarning::UnusualFrameSize { size: data.len() });
217 }
218
219 result
220}
221
222pub fn validate_mstp_frame(data: &[u8]) -> ValidationResult {
224 let mut result = ValidationResult {
225 is_valid: true,
226 link_type: Some(DataLinkType::MsTP),
227 frame_size: data.len(),
228 errors: Vec::new(),
229 warnings: Vec::new(),
230 };
231
232 if data.len() < 8 {
234 result.is_valid = false;
235 result.errors.push(ValidationError::FrameTooShort {
236 size: data.len(),
237 minimum: 8,
238 });
239 return result;
240 }
241
242 if data[0] != 0x55 || data[1] != 0xFF {
244 result.is_valid = false;
245 result.errors.push(ValidationError::InvalidPreamble {
246 expected: vec![0x55, 0xFF],
247 found: vec![data[0], data[1]],
248 });
249 }
250
251 let frame_type = data[2];
253 if frame_type > 7 && frame_type < 128 {
254 result.is_valid = false;
255 result
256 .errors
257 .push(ValidationError::InvalidFrameType { value: frame_type });
258 } else if frame_type >= 128 {
259 result
261 .warnings
262 .push(ValidationWarning::NonStandardConfiguration {
263 reason: format!("Proprietary frame type: {}", frame_type),
264 });
265 }
266
267 let dest_addr = data[3];
269 let src_addr = data[4];
270
271 if src_addr == 255 {
273 result.is_valid = false;
274 result.errors.push(ValidationError::InvalidAddress {
275 address: format!("{}", src_addr),
276 reason: "Source address cannot be broadcast (255)".into(),
277 });
278 }
279
280 if src_addr <= 127 && (128..=254).contains(&dest_addr) && frame_type != 3 {
282 result
283 .warnings
284 .push(ValidationWarning::NonStandardConfiguration {
285 reason: "Master communicating with slave without Test Request".into(),
286 });
287 }
288
289 let data_length = ((data[5] as u16) << 8) | (data[6] as u16);
291
292 if data_length > 501 {
294 result.is_valid = false;
295 result.errors.push(ValidationError::InvalidHeader {
296 reason: format!("Data length {} exceeds maximum 501", data_length),
297 });
298 }
299
300 let header_crc = data[7];
302 let header_bytes = [data[2], data[3], data[4], data[5], data[6]];
303 let calculated_crc = calculate_mstp_header_crc(&header_bytes);
304
305 if header_crc != calculated_crc {
306 result.is_valid = false;
307 result.errors.push(ValidationError::CrcMismatch {
308 expected: header_crc as u32,
309 calculated: calculated_crc as u32,
310 });
311 }
312
313 let expected_size = 8 + data_length as usize + if data_length > 0 { 2 } else { 0 };
315 if data.len() != expected_size {
316 result.is_valid = false;
317 result.errors.push(ValidationError::PayloadSizeMismatch {
318 declared: expected_size,
319 actual: data.len(),
320 });
321 }
322
323 if data_length > 0 && data.len() >= expected_size {
325 let data_start = 8;
326 let data_end = data_start + data_length as usize;
327 let frame_data = &data[data_start..data_end];
328
329 let crc_low = data[data_end];
330 let crc_high = data[data_end + 1];
331 let received_crc = ((crc_high as u16) << 8) | (crc_low as u16);
332
333 let calculated_crc = crc16_mstp(frame_data);
334
335 if received_crc != calculated_crc {
336 result.is_valid = false;
337 result.errors.push(ValidationError::CrcMismatch {
338 expected: received_crc as u32,
339 calculated: calculated_crc as u32,
340 });
341 }
342 }
343
344 result
345}
346
347pub fn validate_frame(data: &[u8]) -> ValidationResult {
349 if data.is_empty() {
350 return ValidationResult {
351 is_valid: false,
352 link_type: None,
353 frame_size: 0,
354 errors: vec![ValidationError::FrameTooShort {
355 size: 0,
356 minimum: 1,
357 }],
358 warnings: Vec::new(),
359 };
360 }
361
362 if data.len() >= 2 && data[0] == 0x55 && data[1] == 0xFF {
366 return validate_mstp_frame(data);
367 }
368
369 if data[0] == 0x81 {
371 return validate_bacnet_ip_frame(data);
372 }
373
374 if data.len() >= 14 {
376 let ether_type = ((data[12] as u16) << 8) | (data[13] as u16);
377 if ether_type == 0x82DC {
378 return validate_ethernet_frame(data);
379 }
380 }
381
382 ValidationResult {
384 is_valid: false,
385 link_type: None,
386 frame_size: data.len(),
387 errors: vec![ValidationError::InvalidHeader {
388 reason: "Unable to determine frame type".into(),
389 }],
390 warnings: Vec::new(),
391 }
392}
393
394fn calculate_mstp_header_crc(header: &[u8; 5]) -> u8 {
396 let mut crc = 0xFFu8;
397
398 for &byte in header {
399 crc ^= byte;
400 for _ in 0..8 {
401 if crc & 0x01 != 0 {
402 crc = (crc >> 1) ^ 0x55;
403 } else {
404 crc >>= 1;
405 }
406 }
407 }
408
409 !crc
410}
411
412pub fn analyze_frame(data: &[u8]) -> FrameAnalysis {
414 let validation = validate_frame(data);
415
416 FrameAnalysis {
417 validation,
418 statistics: calculate_frame_statistics(data),
419 patterns: detect_patterns(data),
420 }
421}
422
423#[derive(Debug, Clone)]
425pub struct FrameAnalysis {
426 pub validation: ValidationResult,
428 pub statistics: FrameStatistics,
430 pub patterns: Vec<Pattern>,
432}
433
434#[derive(Debug, Clone)]
436pub struct FrameStatistics {
437 pub byte_distribution: [u32; 256],
439 pub entropy: f64,
441 pub null_bytes: usize,
443 pub high_bytes: usize,
445 pub longest_run: (u8, usize),
447}
448
449#[derive(Debug, Clone)]
451pub enum Pattern {
452 Padding {
454 start: usize,
455 length: usize,
456 value: u8,
457 },
458 RepeatedSequence {
460 start: usize,
461 pattern: Vec<u8>,
462 count: usize,
463 },
464 AsciiText { start: usize, text: String },
466 Suspicious { description: String },
468}
469
470fn calculate_frame_statistics(data: &[u8]) -> FrameStatistics {
472 let mut byte_distribution = [0u32; 256];
473 let mut null_bytes = 0;
474 let mut high_bytes = 0;
475
476 for &byte in data {
478 byte_distribution[byte as usize] += 1;
479 if byte == 0 {
480 null_bytes += 1;
481 }
482 if byte >= 0x80 {
483 high_bytes += 1;
484 }
485 }
486
487 let total = data.len() as f64;
489 let mut entropy = 0.0;
490 for count in byte_distribution.iter() {
491 if *count > 0 {
492 let probability = *count as f64 / total;
493 entropy -= probability * probability.log2();
494 }
495 }
496
497 let mut longest_run = (0u8, 0usize);
499 if !data.is_empty() {
500 let mut current_byte = data[0];
501 let mut current_run = 1;
502
503 for &byte in &data[1..] {
504 if byte == current_byte {
505 current_run += 1;
506 } else {
507 if current_run > longest_run.1 {
508 longest_run = (current_byte, current_run);
509 }
510 current_byte = byte;
511 current_run = 1;
512 }
513 }
514
515 if current_run > longest_run.1 {
516 longest_run = (current_byte, current_run);
517 }
518 }
519
520 FrameStatistics {
521 byte_distribution,
522 entropy,
523 null_bytes,
524 high_bytes,
525 longest_run,
526 }
527}
528
529fn detect_patterns(data: &[u8]) -> Vec<Pattern> {
531 let mut patterns = Vec::new();
532
533 if data.len() >= 4 {
535 let mut i = data.len() - 1;
536 let pad_byte = data[i];
537 let mut pad_len = 0;
538
539 while i > 0 && data[i] == pad_byte {
540 pad_len += 1;
541 i -= 1;
542 }
543
544 if pad_len >= 4 {
545 patterns.push(Pattern::Padding {
546 start: i + 1,
547 length: pad_len,
548 value: pad_byte,
549 });
550 }
551 }
552
553 let mut ascii_start = None;
555 let mut ascii_bytes = Vec::new();
556
557 for (i, &byte) in data.iter().enumerate() {
558 if (0x20..=0x7E).contains(&byte) {
559 if ascii_start.is_none() {
560 ascii_start = Some(i);
561 }
562 ascii_bytes.push(byte);
563 } else if !ascii_bytes.is_empty() {
564 if ascii_bytes.len() >= 4 {
565 patterns.push(Pattern::AsciiText {
566 start: ascii_start.unwrap(),
567 text: String::from_utf8_lossy(&ascii_bytes).to_string(),
568 });
569 }
570 ascii_start = None;
571 ascii_bytes.clear();
572 }
573 }
574
575 if ascii_bytes.len() >= 4 {
577 patterns.push(Pattern::AsciiText {
578 start: ascii_start.unwrap(),
579 text: String::from_utf8_lossy(&ascii_bytes).to_string(),
580 });
581 }
582
583 if data.len() >= 8 {
585 let non_padding_len = if let Some(Pattern::Padding { start, .. }) = patterns.first() {
587 *start
588 } else {
589 data.len()
590 };
591
592 if non_padding_len >= 8 && data[..non_padding_len].iter().all(|&b| b == 0) {
593 patterns.push(Pattern::Suspicious {
594 description: "Frame contains all zeros".into(),
595 });
596 }
597
598 if data.starts_with(&[0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]) {
600 patterns.push(Pattern::Suspicious {
601 description: "Frame starts with sequential test pattern".into(),
602 });
603 }
604 }
605
606 patterns
607}
608
609#[cfg(test)]
610mod tests {
611 use super::*;
612
613 #[test]
614 fn test_bacnet_ip_validation() {
615 let valid_frame = vec![
617 0x81, 0x0A, 0x00, 0x08, 0x01, 0x00, 0x00, 0x00, ];
620 let result = validate_bacnet_ip_frame(&valid_frame);
621 assert!(result.is_valid);
622 assert!(result.errors.is_empty());
623
624 let invalid_frame = vec![0x82, 0x0A, 0x00, 0x04];
626 let result = validate_bacnet_ip_frame(&invalid_frame);
627 assert!(!result.is_valid);
628 assert!(result
629 .errors
630 .iter()
631 .any(|e| matches!(e, ValidationError::InvalidPreamble { .. })));
632
633 let invalid_frame = vec![0x81, 0x0A, 0x00, 0x10, 0x01, 0x02];
635 let result = validate_bacnet_ip_frame(&invalid_frame);
636 assert!(!result.is_valid);
637 assert!(result
638 .errors
639 .iter()
640 .any(|e| matches!(e, ValidationError::PayloadSizeMismatch { .. })));
641 }
642
643 #[test]
644 fn test_ethernet_validation() {
645 let mut valid_frame = vec![0u8; 60];
647 valid_frame[12] = 0x82;
649 valid_frame[13] = 0xDC;
650 valid_frame[14] = 0x82;
652 valid_frame[15] = 0x82;
653 valid_frame[16] = 0x03;
654
655 let result = validate_ethernet_frame(&valid_frame);
656 assert!(result.is_valid);
657
658 valid_frame[12] = 0x08;
660 valid_frame[13] = 0x00;
661 let result = validate_ethernet_frame(&valid_frame);
662 assert!(!result.is_valid);
663 }
664
665 #[test]
666 fn test_mstp_validation() {
667 let frame = vec![
669 0x55, 0xFF, 0x00, 0x02, 0x01, 0x00, 0x00, 0xFC, ];
676
677 let result = validate_mstp_frame(&frame);
678 assert!(result.is_valid);
679 assert!(result.errors.is_empty());
680
681 let mut invalid_frame = frame.clone();
683 invalid_frame[0] = 0xAA;
684 let result = validate_mstp_frame(&invalid_frame);
685 assert!(!result.is_valid);
686 }
687
688 #[test]
689 fn test_auto_detection() {
690 let mstp_frame = vec![0x55, 0xFF, 0x00, 0x02, 0x01, 0x00, 0x00, 0xDB];
692 let result = validate_frame(&mstp_frame);
693 assert_eq!(result.link_type, Some(DataLinkType::MsTP));
694
695 let bip_frame = vec![0x81, 0x0A, 0x00, 0x04];
697 let result = validate_frame(&bip_frame);
698 assert_eq!(result.link_type, Some(DataLinkType::BacnetIp));
699
700 let unknown_frame = vec![0xFF, 0xFF, 0xFF];
702 let result = validate_frame(&unknown_frame);
703 assert_eq!(result.link_type, None);
704 }
705
706 #[test]
707 fn test_pattern_detection() {
708 let mut frame = vec![0x01, 0x02, 0x03, 0x04];
710 frame.extend_from_slice(&[0x00; 10]);
711
712 let patterns = detect_patterns(&frame);
713 assert!(patterns
714 .iter()
715 .any(|p| matches!(p, Pattern::Padding { .. })));
716
717 let frame = b"Test BACnet Frame";
719 let patterns = detect_patterns(frame);
720 assert!(patterns
721 .iter()
722 .any(|p| matches!(p, Pattern::AsciiText { .. })));
723 }
724}