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