Skip to main content

mabi_modbus/fault_injection/
wrong_function_code.rs

1//! Wrong function code fault injection.
2//!
3//! Corrupts the function code byte in Modbus responses, triggering
4//! `ResponseValidator` FC matching failures in trap-modbus.
5
6use rand::Rng;
7
8use super::config::{FaultTypeConfig, FcCorruptionMode};
9use super::stats::{FaultStats, FaultStatsSnapshot};
10use super::targeting::FaultTarget;
11use super::{FaultAction, ModbusFault, ModbusFaultContext};
12
13/// Corrupts the function code in Modbus response PDUs.
14///
15/// The function code is always at byte 0 of the PDU. This fault modifies
16/// that byte according to the configured mode.
17///
18/// # Modes
19///
20/// - `Fixed`: Always use a specific function code
21/// - `Increment`: Add 1 to the original FC
22/// - `Random`: Use a random FC (different from original)
23/// - `SwapRW`: Swap between read FCs (01-04) and write FCs (05, 06, 0F, 10)
24pub struct WrongFunctionCodeFault {
25    mode: FcCorruptionMode,
26    fixed_fc: u8,
27    target: FaultTarget,
28    stats: FaultStats,
29}
30
31impl WrongFunctionCodeFault {
32    /// Create a new wrong function code fault.
33    pub fn new(mode: FcCorruptionMode, target: FaultTarget) -> Self {
34        Self {
35            mode,
36            fixed_fc: 0xFF,
37            target,
38            stats: FaultStats::new(),
39        }
40    }
41
42    /// Set a fixed function code (for Fixed mode).
43    pub fn with_fixed_fc(mut self, fc: u8) -> Self {
44        self.fixed_fc = fc;
45        self
46    }
47
48    /// Create from config.
49    pub fn from_config(config: &FaultTypeConfig, target: FaultTarget) -> Self {
50        Self {
51            mode: config.fc_mode.unwrap_or(FcCorruptionMode::Increment),
52            fixed_fc: config.fixed_fc.unwrap_or(0xFF),
53            target,
54            stats: FaultStats::new(),
55        }
56    }
57
58    /// Compute the corrupted function code.
59    fn corrupt_fc(&self, original: u8) -> u8 {
60        match self.mode {
61            FcCorruptionMode::Fixed => self.fixed_fc,
62            FcCorruptionMode::Increment => original.wrapping_add(1),
63            FcCorruptionMode::Random => {
64                let mut rng = rand::thread_rng();
65                loop {
66                    let candidate: u8 = rng.gen_range(1..=0x7F);
67                    if candidate != original {
68                        return candidate;
69                    }
70                }
71            }
72            FcCorruptionMode::SwapRW => {
73                // Read FCs: 0x01, 0x02, 0x03, 0x04
74                // Write FCs: 0x05, 0x06, 0x0F, 0x10
75                match original {
76                    0x01 => 0x05,
77                    0x02 => 0x0F,
78                    0x03 => 0x10,
79                    0x04 => 0x06,
80                    0x05 => 0x01,
81                    0x06 => 0x04,
82                    0x0F => 0x02,
83                    0x10 => 0x03,
84                    0x16 => 0x03,
85                    0x17 => 0x03,
86                    _ => original.wrapping_add(1),
87                }
88            }
89        }
90    }
91}
92
93impl ModbusFault for WrongFunctionCodeFault {
94    fn fault_type(&self) -> &'static str {
95        "wrong_function_code"
96    }
97
98    fn is_enabled(&self) -> bool {
99        self.stats.is_enabled()
100    }
101
102    fn set_enabled(&self, enabled: bool) {
103        self.stats.set_enabled(enabled);
104    }
105
106    fn should_activate(&self, ctx: &ModbusFaultContext) -> bool {
107        self.stats.record_check();
108        self.target.should_activate(ctx.unit_id, ctx.function_code)
109    }
110
111    fn apply(&self, ctx: &ModbusFaultContext) -> FaultAction {
112        self.stats.record_activation();
113        self.stats.record_affected();
114
115        let mut response = ctx.response_pdu.clone();
116        if !response.is_empty() {
117            response[0] = self.corrupt_fc(ctx.function_code);
118        }
119
120        FaultAction::SendResponse(response)
121    }
122
123    fn stats(&self) -> FaultStatsSnapshot {
124        self.stats.snapshot()
125    }
126
127    fn reset_stats(&self) {
128        self.stats.reset();
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    fn test_ctx() -> ModbusFaultContext {
137        ModbusFaultContext::tcp(
138            1,
139            0x03,
140            &[0x03, 0x00, 0x00, 0x00, 0x01],
141            &[0x03, 0x02, 0x00, 0x64],
142            1,
143            1,
144        )
145    }
146
147    #[test]
148    fn test_fixed_mode() {
149        let fault = WrongFunctionCodeFault::new(FcCorruptionMode::Fixed, FaultTarget::new())
150            .with_fixed_fc(0x10);
151        let action = fault.apply(&test_ctx());
152
153        match action {
154            FaultAction::SendResponse(pdu) => {
155                assert_eq!(pdu[0], 0x10);
156                assert_eq!(&pdu[1..], &[0x02, 0x00, 0x64]); // rest intact
157            }
158            _ => panic!("Expected SendResponse"),
159        }
160    }
161
162    #[test]
163    fn test_increment_mode() {
164        let fault = WrongFunctionCodeFault::new(FcCorruptionMode::Increment, FaultTarget::new());
165        let action = fault.apply(&test_ctx());
166
167        match action {
168            FaultAction::SendResponse(pdu) => {
169                assert_eq!(pdu[0], 0x04); // 0x03 + 1
170            }
171            _ => panic!("Expected SendResponse"),
172        }
173    }
174
175    #[test]
176    fn test_random_mode() {
177        let fault = WrongFunctionCodeFault::new(FcCorruptionMode::Random, FaultTarget::new());
178        let ctx = test_ctx();
179        let action = fault.apply(&ctx);
180
181        match action {
182            FaultAction::SendResponse(pdu) => {
183                assert_ne!(pdu[0], 0x03);
184            }
185            _ => panic!("Expected SendResponse"),
186        }
187    }
188
189    #[test]
190    fn test_swap_rw_read_to_write() {
191        let fault = WrongFunctionCodeFault::new(FcCorruptionMode::SwapRW, FaultTarget::new());
192
193        // FC 0x03 (Read Holding) -> 0x10 (Write Multiple)
194        let ctx = test_ctx();
195        match fault.apply(&ctx) {
196            FaultAction::SendResponse(pdu) => assert_eq!(pdu[0], 0x10),
197            _ => panic!("Expected SendResponse"),
198        }
199    }
200
201    #[test]
202    fn test_swap_rw_write_to_read() {
203        let fault = WrongFunctionCodeFault::new(FcCorruptionMode::SwapRW, FaultTarget::new());
204
205        // FC 0x10 (Write Multiple) -> 0x03 (Read Holding)
206        let ctx = ModbusFaultContext::tcp(1, 0x10, &[0x10], &[0x10, 0x00], 1, 1);
207        match fault.apply(&ctx) {
208            FaultAction::SendResponse(pdu) => assert_eq!(pdu[0], 0x03),
209            _ => panic!("Expected SendResponse"),
210        }
211    }
212
213    #[test]
214    fn test_swap_rw_all_mappings() {
215        let fault = WrongFunctionCodeFault::new(FcCorruptionMode::SwapRW, FaultTarget::new());
216
217        let mappings = vec![
218            (0x01, 0x05),
219            (0x02, 0x0F),
220            (0x03, 0x10),
221            (0x04, 0x06),
222            (0x05, 0x01),
223            (0x06, 0x04),
224            (0x0F, 0x02),
225            (0x10, 0x03),
226        ];
227
228        for (from, to) in mappings {
229            let ctx = ModbusFaultContext::tcp(1, from, &[from], &[from, 0x00], 1, 1);
230            match fault.apply(&ctx) {
231                FaultAction::SendResponse(pdu) => {
232                    assert_eq!(pdu[0], to, "FC 0x{:02X} should map to 0x{:02X}", from, to)
233                }
234                _ => panic!("Expected SendResponse"),
235            }
236        }
237    }
238
239    #[test]
240    fn test_empty_response() {
241        let fault = WrongFunctionCodeFault::new(FcCorruptionMode::Fixed, FaultTarget::new())
242            .with_fixed_fc(0x10);
243        let ctx = ModbusFaultContext::tcp(1, 0x03, &[0x03], &[], 1, 1);
244        let action = fault.apply(&ctx);
245
246        match action {
247            FaultAction::SendResponse(pdu) => {
248                assert!(pdu.is_empty());
249            }
250            _ => panic!("Expected SendResponse"),
251        }
252    }
253
254    #[test]
255    fn test_from_config() {
256        let config = FaultTypeConfig {
257            fc_mode: Some(FcCorruptionMode::Fixed),
258            fixed_fc: Some(0x42),
259            ..Default::default()
260        };
261        let fault = WrongFunctionCodeFault::from_config(&config, FaultTarget::new());
262        let ctx = test_ctx();
263        match fault.apply(&ctx) {
264            FaultAction::SendResponse(pdu) => assert_eq!(pdu[0], 0x42),
265            _ => panic!("Expected SendResponse"),
266        }
267    }
268}