Skip to main content

bacnet_rs/datalink/
validation.rs

1//! Frame Validation Utilities
2//!
3//! This module provides comprehensive validation utilities for BACnet data link frames
4//! across all supported data link types (BACnet/IP, Ethernet, MS/TP, etc.).
5//!
6//! # Overview
7//!
8//! Frame validation includes:
9//! - Structure validation (correct headers, sizes)
10//! - CRC/checksum verification
11//! - Address validation
12//! - Protocol-specific checks
13//! - Common error detection patterns
14
15use crate::datalink::DataLinkType;
16use crate::util::crc16_mstp;
17
18/// Frame validation result with detailed information
19#[derive(Debug, Clone)]
20pub struct ValidationResult {
21    /// Whether the frame is valid
22    pub is_valid: bool,
23    /// Data link type detected
24    pub link_type: Option<DataLinkType>,
25    /// Frame size
26    pub frame_size: usize,
27    /// Validation errors found
28    pub errors: Vec<ValidationError>,
29    /// Validation warnings (non-fatal)
30    pub warnings: Vec<ValidationWarning>,
31}
32
33/// Validation error types
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum ValidationError {
36    /// Frame too short for any valid protocol
37    FrameTooShort { size: usize, minimum: usize },
38    /// Frame too long for protocol
39    FrameTooLong { size: usize, maximum: usize },
40    /// Invalid preamble or magic bytes
41    InvalidPreamble { expected: Vec<u8>, found: Vec<u8> },
42    /// CRC mismatch
43    CrcMismatch { expected: u32, calculated: u32 },
44    /// Invalid frame type
45    InvalidFrameType { value: u8 },
46    /// Invalid address
47    InvalidAddress { address: String, reason: String },
48    /// Invalid header structure
49    InvalidHeader { reason: String },
50    /// Payload size mismatch
51    PayloadSizeMismatch { declared: usize, actual: usize },
52}
53
54/// Validation warning types (non-fatal issues)
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum ValidationWarning {
57    /// Unusual but valid frame size
58    UnusualFrameSize { size: usize },
59    /// Deprecated frame type
60    DeprecatedFrameType { frame_type: u8 },
61    /// Non-standard but valid configuration
62    NonStandardConfiguration { reason: String },
63    /// Potential security issue
64    SecurityWarning { reason: String },
65}
66
67/// Validate a BACnet/IP frame
68pub 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    // Check minimum size (BVLC header)
78    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    // Check BVLC type
88    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    // Check BVLC function
97    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    // Check BVLC length
108    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    // Check maximum size
118    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    // Warnings for specific functions
127    match data[1] {
128        0x0C => {
129            // Secure BVLL - warn if not using proper security
130            result.warnings.push(ValidationWarning::SecurityWarning {
131                reason: "Secure BVLL should use proper encryption".into(),
132            });
133        }
134        0x01 | 0x08 => {
135            // Write-BDT or Delete-FDT-Entry - potential security risk
136            result.warnings.push(ValidationWarning::SecurityWarning {
137                reason: "Table modification functions should be authenticated".into(),
138            });
139        }
140        _ => {}
141    }
142
143    result
144}
145
146/// Validate an Ethernet frame
147pub 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    // Check minimum size (Ethernet header + LLC)
157    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    // Check maximum size
167    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    // Check Ethernet type for BACnet
176    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    // Check LLC header
188    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    // Check for multicast/broadcast
199    if data[0] & 0x01 == 0x01 {
200        if data[0..6] == [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF] {
201            // Broadcast - this is fine
202        } else {
203            // Multicast - warn as BACnet typically uses broadcast
204            result
205                .warnings
206                .push(ValidationWarning::NonStandardConfiguration {
207                    reason: "Multicast address used instead of broadcast".into(),
208                });
209        }
210    }
211
212    // Warn about small frames (likely padded)
213    if data.len() == 60 {
214        result
215            .warnings
216            .push(ValidationWarning::UnusualFrameSize { size: data.len() });
217    }
218
219    result
220}
221
222/// Validate an MS/TP frame
223pub 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    // Check minimum size (header)
233    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    // Check preamble
243    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    // Check frame type
252    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        // Proprietary frame types
260        result
261            .warnings
262            .push(ValidationWarning::NonStandardConfiguration {
263                reason: format!("Proprietary frame type: {}", frame_type),
264            });
265    }
266
267    // Get addresses
268    let dest_addr = data[3];
269    let src_addr = data[4];
270
271    // Validate addresses
272    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    // Check for master talking to slave without poll
281    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    // Get data length
290    let data_length = ((data[5] as u16) << 8) | (data[6] as u16);
291
292    // Check data length
293    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    // Verify header CRC
301    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    // Check frame size
314    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    // Verify data CRC if present
324    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
347/// Automatically detect and validate frame type
348pub 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    // Try to detect frame type by examining headers
363
364    // Check for MS/TP (starts with 0x55, 0xFF)
365    if data.len() >= 2 && data[0] == 0x55 && data[1] == 0xFF {
366        return validate_mstp_frame(data);
367    }
368
369    // Check for BACnet/IP (starts with 0x81)
370    if data[0] == 0x81 {
371        return validate_bacnet_ip_frame(data);
372    }
373
374    // Check for Ethernet (has BACnet Ethernet type at offset 12-13)
375    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    // Unknown frame type
383    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
394/// Calculate MS/TP header CRC (for validation)
395fn 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
412/// Perform deep frame analysis
413pub 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/// Frame analysis results
424#[derive(Debug, Clone)]
425pub struct FrameAnalysis {
426    /// Basic validation results
427    pub validation: ValidationResult,
428    /// Frame statistics
429    pub statistics: FrameStatistics,
430    /// Detected patterns
431    pub patterns: Vec<Pattern>,
432}
433
434/// Frame statistics
435#[derive(Debug, Clone)]
436pub struct FrameStatistics {
437    /// Byte value distribution
438    pub byte_distribution: [u32; 256],
439    /// Entropy estimate (0.0 - 8.0 bits)
440    pub entropy: f64,
441    /// Number of null bytes
442    pub null_bytes: usize,
443    /// Number of high bytes (>= 0x80)
444    pub high_bytes: usize,
445    /// Longest run of same byte
446    pub longest_run: (u8, usize),
447}
448
449/// Detected patterns in frame
450#[derive(Debug, Clone)]
451pub enum Pattern {
452    /// Padding detected
453    Padding {
454        start: usize,
455        length: usize,
456        value: u8,
457    },
458    /// Repeated sequence
459    RepeatedSequence {
460        start: usize,
461        pattern: Vec<u8>,
462        count: usize,
463    },
464    /// Possible ASCII text
465    AsciiText { start: usize, text: String },
466    /// Suspicious pattern
467    Suspicious { description: String },
468}
469
470/// Calculate frame statistics
471fn 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    // Count byte occurrences
477    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    // Calculate entropy
488    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    // Find longest run
498    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
529/// Detect patterns in frame data
530fn detect_patterns(data: &[u8]) -> Vec<Pattern> {
531    let mut patterns = Vec::new();
532
533    // Detect padding
534    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    // Detect ASCII text
554    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    // Check remaining ASCII
576    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    // Detect suspicious patterns
584    if data.len() >= 8 {
585        // Check for all zeros (except in padding)
586        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        // Check for obvious test patterns
599        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        // Valid frame
616        let valid_frame = vec![
617            0x81, 0x0A, 0x00, 0x08, // BVLC header
618            0x01, 0x00, 0x00, 0x00, // NPDU
619        ];
620        let result = validate_bacnet_ip_frame(&valid_frame);
621        assert!(result.is_valid);
622        assert!(result.errors.is_empty());
623
624        // Invalid BVLC type
625        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        // Length mismatch
634        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        // Valid frame
646        let mut valid_frame = vec![0u8; 60];
647        // Set Ethernet type
648        valid_frame[12] = 0x82;
649        valid_frame[13] = 0xDC;
650        // Set LLC header
651        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        // Wrong Ethernet type
659        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        // Valid token frame
668        let frame = vec![
669            0x55, 0xFF, // Preamble
670            0x00, // Token frame
671            0x02, // Destination
672            0x01, // Source
673            0x00, 0x00, // Data length = 0
674            0xFC, // Header CRC (correct value for this header)
675        ];
676
677        let result = validate_mstp_frame(&frame);
678        assert!(result.is_valid);
679        assert!(result.errors.is_empty());
680
681        // Invalid preamble
682        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        // MS/TP frame
691        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        // BACnet/IP frame
696        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        // Unknown frame
701        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        // Frame with padding
709        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        // Frame with ASCII text
718        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}