Skip to main content

ace_doip/session/
activation.rs

1//! Routing Activation state machine - one instance per TCP connection.
2//!
3//! Regulation defines the activation sequence:
4//!     1. Tester sends RoutingActivationRequest
5//!     2. Gateway validates source address, activation type, auth data
6//!     3. Gateway sends RoutingActivationResponse
7//!     4. If successful, connection transitions to Active
8//!
9//! The activation line maps onto this state machine - the connection only enters Active after the
10//! gateway confirms activation. The ActivationAuthProvider hook allows OEM-specific authentication
11//! for CentralSecurity activation type.
12
13// region: Imports
14
15use ace_proto::doip::constants::{
16    DOIP_ROUTING_ACTIVATION_REQ_ISO_LEN, DOIP_ROUTING_ACTIVATION_REQ_OEM_LEN,
17};
18
19use crate::payload::{
20    ActivationCode, ActivationType, RoutingActivationRequest, RoutingActivationResponse,
21};
22
23// endregion: Imports
24
25// region: ActivationDenialReason
26
27/// Reasons the gateway may deny a routing activation request.
28///
29/// Maps directly onto ActivationCode denial values
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum ActivationDenialReason {
32    /// Source address not registered with this gateway.
33    UnknownSourceAddress,
34
35    /// No TCP socket slots available.
36    TcpSocketsFull,
37
38    /// This TCP socket already has an active routing activation.
39    AlreadyConnected,
40
41    /// The source address is already active on another socket.
42    SourceAlreadyActive,
43
44    /// CentralSecurity activation type requires authentication data.
45    MissingAuthentication,
46
47    /// Authentication data was rejected.
48    RejectedConfirmation,
49
50    /// Activation type not supported by this gateway configuration.
51    UnsupportedActivationType,
52
53    /// Gateway requires TLS - unencrypted connection rejected.
54    RequiresTls,
55}
56impl From<ActivationDenialReason> for ActivationCode {
57    fn from(value: ActivationDenialReason) -> Self {
58        match value {
59            ActivationDenialReason::RejectedConfirmation => Self::DeniedRejectedConfirmation,
60            ActivationDenialReason::MissingAuthentication => Self::DeniedMissingAuthentication,
61            ActivationDenialReason::UnsupportedActivationType => {
62                Self::DeniedUnsupportedRoutingActivationType
63            }
64            ActivationDenialReason::RequiresTls => Self::DeniedRequestEncryptedTlsConnection,
65            ActivationDenialReason::SourceAlreadyActive => Self::DeniedSourceIsAlreadyActive,
66            ActivationDenialReason::TcpSocketsFull => Self::DeniedTcpSocketsFull,
67            ActivationDenialReason::AlreadyConnected => Self::DeniedTcpSocketAlreadyConnected,
68            ActivationDenialReason::UnknownSourceAddress => Self::DeniedUnknownSourceAddress,
69        }
70    }
71}
72
73// endregion: ActivationDenialReason
74
75// region: ActivationAuthProvider
76
77/// Hook for OEM-specific routing activation authentication.
78///
79/// Called by the activation state machine when a `CentralSecurity` (0xFF) activation request is
80/// received. The implementation validates the OEM-specific data in the request buffer and returns
81/// either `Ok(())` to allow activation or `Err(reason)` to deny it.
82///
83/// For `Default` (0x00) and `WwhObd` (0x01) activation types the gateway does not call this hook -
84/// activation is granted based on source address validity alone.
85///
86/// # Simulation
87///
88/// The test implementation should use the `ace-sim` seeded RNG so authentication outcomes are
89/// reproducible across simulation runs.
90pub trait ActivationAuthProvider {
91    /// Validates OEM-specific authentication data for a CentralSecurity activation request.
92    ///
93    /// `source_address` - the logical address of the tester
94    /// `oem_data` - the 4-byte ISO reserved / OEM buffer from the request
95    fn authenticate(
96        &mut self,
97        source_address: u16,
98        oem_data: &[u8],
99    ) -> Result<(), ActivationDenialReason>;
100}
101
102// region: AlwaysAllow
103
104/// An `ActivationAuthProvider` that always grants CentralSecurity activation. Suitable for testing
105/// and development environments only.
106pub struct AlwaysAllow;
107
108impl ActivationAuthProvider for AlwaysAllow {
109    fn authenticate(
110        &mut self,
111        _source_address: u16,
112        _oem_data: &[u8],
113    ) -> Result<(), ActivationDenialReason> {
114        Ok(())
115    }
116}
117
118// endregion: AlwaysAllow
119
120// region: AlwaysDeny
121
122/// An `ActivationAuthProvider` that always denies CentralSecurity activation. Useful for testing
123/// denial paths.
124pub struct AlwaysDeny {
125    pub reason: ActivationDenialReason,
126}
127
128impl ActivationAuthProvider for AlwaysDeny {
129    fn authenticate(
130        &mut self,
131        _source_address: u16,
132        _oem_data: &[u8],
133    ) -> Result<(), ActivationDenialReason> {
134        Err(self.reason.clone())
135    }
136}
137
138// endregion: AlwaysDeny
139
140// endregion: ActivationAuthProvider
141
142// region: ActivationLineState
143
144/// The state of the logical activation line for a single TCP connection.
145///
146/// In vehicle terms this maps to the hardware activation line state. The line must be in `Active`
147/// before `DiagnosticMessage` frames are forwarded to ECU nodes.
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub enum ActivationLineState {
150    /// No routing activation has been attempted on this connection.
151    Idle,
152
153    /// A `RoutingActivationRequest` was received and is being processed.
154    Pending {
155        source_address: u16,
156        activation_type: ActivationType,
157    },
158
159    /// Routing activation succeeded - diagnostic messages permitted.
160    Active {
161        /// Logical address of the activated tester.
162        source_address: u16,
163
164        /// Activation type that was used to activate this connection.
165        activation_type: ActivationType,
166    },
167
168    /// Routing activation was denies or the line was dropped. The connection should be closed.
169    Deactivated { reason: ActivationDenialReason },
170}
171
172impl ActivationLineState {
173    pub fn is_active(&self) -> bool {
174        matches!(self, Self::Active { .. })
175    }
176
177    pub fn active_source_address(&self) -> Option<u16> {
178        match self {
179            Self::Active { source_address, .. } => Some(*source_address),
180            _ => None,
181        }
182    }
183}
184
185// endregion: ActivationLineState
186
187// region: ActivationStateMachine
188
189/// Per-connection routing activation state machine.
190///
191/// The gateway creates one instance per TCP connection. The state machine processes
192/// `RoutingActivationRequest` frames and produces `RoutingActivationResponse` frames. It enforces
193/// the activation line state - only `Active` connections may carry `DiagnosticMessage` frames.
194#[derive(Debug)]
195pub struct ActivationStateMachine<A: ActivationAuthProvider> {
196    /// Logical address of this gateway entity.
197    gateway_address: u16,
198
199    /// Set of registered tester source addresses this gateway recognises. In a real gateway this
200    /// is provisioned at build time.
201    registered_addresses: heapless::Vec<u16, 16>,
202
203    /// Supported activation types for this gateway configuration.
204    supported_types: heapless::Vec<ActivationType, 4>,
205
206    /// OEM authentication provider - called for CentralSecurity activations.
207    auth: A,
208
209    /// Current activation line state for this connection.
210    pub state: ActivationLineState,
211}
212
213impl<A: ActivationAuthProvider> ActivationStateMachine<A> {
214    pub fn new(
215        gateway_address: u16,
216        registered_addresses: heapless::Vec<u16, 16>,
217        supported_types: heapless::Vec<ActivationType, 4>,
218        auth: A,
219    ) -> Self {
220        Self {
221            gateway_address,
222            registered_addresses,
223            supported_types,
224            auth,
225            state: ActivationLineState::Idle,
226        }
227    }
228
229    // region: Request processing
230
231    /// Processes a `RoutingActivationRequest` and returns the response to send back to the tester.
232    ///
233    /// Transitions the activation line state based on the outcome. The caller is responsible for
234    /// encoding the response into a DoIP frame.
235    pub fn process_request(&mut self, req: &RoutingActivationRequest) -> RoutingActivationResponse {
236        let source_address = u16::from_be_bytes(req.source_address);
237        let activation_type = req.activation_type.clone();
238
239        if !self.registered_addresses.contains(&source_address) {
240            return self.deny(source_address, ActivationDenialReason::UnknownSourceAddress);
241        }
242
243        if !self.supported_types.contains(&activation_type) {
244            return self.deny(
245                source_address,
246                ActivationDenialReason::UnsupportedActivationType,
247            );
248        }
249
250        if self.state.is_active() {
251            return self.deny(source_address, ActivationDenialReason::AlreadyConnected);
252        }
253
254        if activation_type == ActivationType::CentralSecurity {
255            let mut oem_data =
256                [0u8; DOIP_ROUTING_ACTIVATION_REQ_ISO_LEN + DOIP_ROUTING_ACTIVATION_REQ_OEM_LEN];
257
258            let (iso, oem) = oem_data.split_at_mut(DOIP_ROUTING_ACTIVATION_REQ_ISO_LEN);
259            iso.copy_from_slice(&req.reserved);
260            oem.copy_from_slice(&req.reserved_for_oem);
261
262            match self.auth.authenticate(source_address, &oem_data) {
263                Ok(()) => {}
264                Err(reason) => return self.deny(source_address, reason),
265            }
266        }
267
268        self.state = ActivationLineState::Active {
269            source_address,
270            activation_type,
271        };
272
273        RoutingActivationResponse {
274            logical_address: req.source_address,
275            source_address: self.gateway_address.to_be_bytes(),
276            activation_code: ActivationCode::SuccessfullyActivated,
277            reserved: [0u8; 4],
278            reserved_for_oem: None,
279        }
280    }
281
282    // endregion: Request processing
283
284    // region: Line drop
285
286    /// Drops the activation line - models ignition off or power loss.
287    ///
288    /// Transitions to `Deactivated`. The gateway could close the TCP connection after calling
289    /// this.
290    pub fn drop_line(&mut self, reason: ActivationDenialReason) {
291        self.state = ActivationLineState::Deactivated { reason };
292    }
293
294    // endregion: Line drop
295
296    // region: Helpers
297
298    fn deny(
299        &mut self,
300        source_address: u16,
301        reason: ActivationDenialReason,
302    ) -> RoutingActivationResponse {
303        let code = ActivationCode::from(reason.clone());
304
305        self.state = ActivationLineState::Deactivated { reason };
306
307        RoutingActivationResponse {
308            logical_address: source_address.to_be_bytes(),
309            source_address: self.gateway_address.to_be_bytes(),
310            activation_code: code,
311            reserved: [0u8; 4],
312            reserved_for_oem: None,
313        }
314    }
315
316    // endregion: Helpers
317}
318
319// endregion: ActivationStateMachine