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, 0x03,
139            &[0x03, 0x00, 0x00, 0x00, 0x01],
140            &[0x03, 0x02, 0x00, 0x64],
141            1, 1,
142        )
143    }
144
145    #[test]
146    fn test_fixed_mode() {
147        let fault = WrongFunctionCodeFault::new(FcCorruptionMode::Fixed, FaultTarget::new())
148            .with_fixed_fc(0x10);
149        let action = fault.apply(&test_ctx());
150
151        match action {
152            FaultAction::SendResponse(pdu) => {
153                assert_eq!(pdu[0], 0x10);
154                assert_eq!(&pdu[1..], &[0x02, 0x00, 0x64]); // rest intact
155            }
156            _ => panic!("Expected SendResponse"),
157        }
158    }
159
160    #[test]
161    fn test_increment_mode() {
162        let fault = WrongFunctionCodeFault::new(FcCorruptionMode::Increment, FaultTarget::new());
163        let action = fault.apply(&test_ctx());
164
165        match action {
166            FaultAction::SendResponse(pdu) => {
167                assert_eq!(pdu[0], 0x04); // 0x03 + 1
168            }
169            _ => panic!("Expected SendResponse"),
170        }
171    }
172
173    #[test]
174    fn test_random_mode() {
175        let fault = WrongFunctionCodeFault::new(FcCorruptionMode::Random, FaultTarget::new());
176        let ctx = test_ctx();
177        let action = fault.apply(&ctx);
178
179        match action {
180            FaultAction::SendResponse(pdu) => {
181                assert_ne!(pdu[0], 0x03);
182            }
183            _ => panic!("Expected SendResponse"),
184        }
185    }
186
187    #[test]
188    fn test_swap_rw_read_to_write() {
189        let fault = WrongFunctionCodeFault::new(FcCorruptionMode::SwapRW, FaultTarget::new());
190
191        // FC 0x03 (Read Holding) -> 0x10 (Write Multiple)
192        let ctx = test_ctx();
193        match fault.apply(&ctx) {
194            FaultAction::SendResponse(pdu) => assert_eq!(pdu[0], 0x10),
195            _ => panic!("Expected SendResponse"),
196        }
197    }
198
199    #[test]
200    fn test_swap_rw_write_to_read() {
201        let fault = WrongFunctionCodeFault::new(FcCorruptionMode::SwapRW, FaultTarget::new());
202
203        // FC 0x10 (Write Multiple) -> 0x03 (Read Holding)
204        let ctx = ModbusFaultContext::tcp(1, 0x10, &[0x10], &[0x10, 0x00], 1, 1);
205        match fault.apply(&ctx) {
206            FaultAction::SendResponse(pdu) => assert_eq!(pdu[0], 0x03),
207            _ => panic!("Expected SendResponse"),
208        }
209    }
210
211    #[test]
212    fn test_swap_rw_all_mappings() {
213        let fault = WrongFunctionCodeFault::new(FcCorruptionMode::SwapRW, FaultTarget::new());
214
215        let mappings = vec![
216            (0x01, 0x05), (0x02, 0x0F), (0x03, 0x10), (0x04, 0x06),
217            (0x05, 0x01), (0x06, 0x04), (0x0F, 0x02), (0x10, 0x03),
218        ];
219
220        for (from, to) in mappings {
221            let ctx = ModbusFaultContext::tcp(1, from, &[from], &[from, 0x00], 1, 1);
222            match fault.apply(&ctx) {
223                FaultAction::SendResponse(pdu) => assert_eq!(pdu[0], to, "FC 0x{:02X} should map to 0x{:02X}", from, to),
224                _ => panic!("Expected SendResponse"),
225            }
226        }
227    }
228
229    #[test]
230    fn test_empty_response() {
231        let fault = WrongFunctionCodeFault::new(FcCorruptionMode::Fixed, FaultTarget::new())
232            .with_fixed_fc(0x10);
233        let ctx = ModbusFaultContext::tcp(1, 0x03, &[0x03], &[], 1, 1);
234        let action = fault.apply(&ctx);
235
236        match action {
237            FaultAction::SendResponse(pdu) => {
238                assert!(pdu.is_empty());
239            }
240            _ => panic!("Expected SendResponse"),
241        }
242    }
243
244    #[test]
245    fn test_from_config() {
246        let config = FaultTypeConfig {
247            fc_mode: Some(FcCorruptionMode::Fixed),
248            fixed_fc: Some(0x42),
249            ..Default::default()
250        };
251        let fault = WrongFunctionCodeFault::from_config(&config, FaultTarget::new());
252        let ctx = test_ctx();
253        match fault.apply(&ctx) {
254            FaultAction::SendResponse(pdu) => assert_eq!(pdu[0], 0x42),
255            _ => panic!("Expected SendResponse"),
256        }
257    }
258}