simple-someip 0.10.0

A lightweight SOME/IP serialization and communication library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
use crate::protocol::sd::RebootFlag;
use core::net::SocketAddr;
use heapless::index_map::FnvIndexMap;

/// Max number of distinct `(sender, transport, service, instance)` tuples tracked
/// for reboot detection. Must be a power of two (heapless `FnvIndexMap`
/// requirement). Sized for a small fleet of peers each offering several
/// services; bare-metal builds with more peers may need to edit this constant.
const SESSION_CAP: usize = 64;

/// Distinguishes multicast vs unicast transport for per-sender session tracking.
/// The AUTOSAR spec requires separate session ID tracking per transport.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum TransportKind {
    Multicast,
    #[allow(dead_code)]
    Unicast,
}

/// Composite key identifying a specific service instance from a sender on a
/// specific transport. Tracking per service instance avoids false reboot
/// detection when a sender interleaves SD offers for multiple services, each
/// with its own independent session counter.
type SessionKey = (SocketAddr, TransportKind, u16, u16);

/// Per-service-instance session state for reboot detection.
#[derive(Clone, Copy, Debug)]
struct SessionState {
    last_session_id: u16,
    last_reboot_flag: RebootFlag,
}

/// Result of checking a sender's session ID and reboot flag against stored state.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SessionVerdict {
    /// Session is valid (normal increment or first message with matching state).
    Ok,
    /// Sender has rebooted (reboot flag transitioned `Continuous → RecentlyRebooted`,
    /// or session ID decreased while the reboot flag remains `RecentlyRebooted`
    /// within the same service instance stream).
    Reboot,
    /// First message ever seen from this service instance on this transport.
    Initial,
}

/// Tracks per-service-instance session state for reboot detection.
///
/// A reboot is detected when, for a given `(sender, transport, service_id,
/// instance_id)` tuple:
/// - The reboot flag transitions from `Continuous` to `RecentlyRebooted`, **or**
/// - The session ID decreases while the reboot flag remains `RecentlyRebooted`
///
/// Tracking per service instance (rather than per sender) avoids false
/// positives when a sensor interleaves SD offers for multiple services
/// with independent session counters on the same source address.
///
/// Capacity is bounded at compile time by [`SESSION_CAP`].
/// When the map is full, new sender entries are dropped with a `warn!` log
/// and reboot detection for those senders is disabled.
///
/// # Security posture
///
/// The backing map uses FNV hashing rather than the DoS-resistant hasher used
/// by `std::collections::HashMap`. For SOME/IP on isolated automotive or
/// sensor networks this is not a concern. Deployments where `SessionKey`
/// inputs (notably `SocketAddr`) are adversary-controlled should be aware
/// that an attacker can craft keys to force collisions and degrade lookup
/// cost; the blast radius is bounded by [`SESSION_CAP`].
#[derive(Debug)]
pub struct SessionTracker {
    state: FnvIndexMap<SessionKey, SessionState, SESSION_CAP>,
    /// Set after the first saturation warning. Prevents the saturated-map
    /// log from firing on every `check()` for every new key once capacity
    /// is reached — which would spam the log at the packet rate.
    saturation_warned: bool,
}

impl Default for SessionTracker {
    fn default() -> Self {
        Self {
            state: FnvIndexMap::new(),
            saturation_warned: false,
        }
    }
}

impl SessionTracker {
    /// Check the session ID and reboot flag for a specific service instance
    /// and return a verdict.
    ///
    /// On the normal (non-saturated) path, the stored state is updated
    /// after the check so subsequent calls see the latest session id and
    /// reboot flag for the key.
    ///
    /// # Capacity behavior
    ///
    /// The tracker is backed by a `heapless::FnvIndexMap` bounded by
    /// [`SESSION_CAP`]. If the map is already full and the incoming key
    /// is new, the insert fails and stored state is **not** updated for
    /// that key — subsequent `check()` calls with the same new key will
    /// continue to return [`SessionVerdict::Initial`] until an existing
    /// key is evicted or capacity is raised. A single `warn!` fires the
    /// first time saturation is hit (further saturation drops are
    /// suppressed to avoid log spam at the packet rate). For existing
    /// keys under saturation the update still succeeds, because
    /// `FnvIndexMap::insert` replaces in place.
    ///
    /// Call this once per service entry in an SD message (not once per message),
    /// so each service instance gets its own session counter.
    pub fn check(
        &mut self,
        sender: SocketAddr,
        transport: TransportKind,
        service_id: u16,
        instance_id: u16,
        session_id: u16,
        reboot_flag: RebootFlag,
    ) -> SessionVerdict {
        let key = (sender, transport, service_id, instance_id);
        let verdict = match self.state.get(&key) {
            None => SessionVerdict::Initial,
            Some(prev) => {
                if prev.last_reboot_flag == RebootFlag::Continuous
                    && reboot_flag == RebootFlag::RecentlyRebooted
                {
                    // Continuous → RecentlyRebooted transition — authoritative reboot signal
                    SessionVerdict::Reboot
                } else if prev.last_reboot_flag == RebootFlag::RecentlyRebooted
                    && reboot_flag == RebootFlag::RecentlyRebooted
                    && session_id < prev.last_session_id
                    && !(prev.last_session_id == u16::MAX && session_id <= 1)
                {
                    // Session ID decreased within the same service instance
                    // while reboot flag stays `RecentlyRebooted` — this is a reboot.
                    // Exception: 0xFFFF→1 is the spec-compliant counter wrap; 0xFFFF→0
                    // is tolerated for non-compliant implementations. Neither is a reboot.
                    SessionVerdict::Reboot
                } else {
                    SessionVerdict::Ok
                }
            }
        };
        let new_state = SessionState {
            last_session_id: session_id,
            last_reboot_flag: reboot_flag,
        };
        if self.state.insert(key, new_state).is_err() {
            // Map at capacity and key is new — silently dropping the update
            // would lose reboot-detection state. Log the first time we hit
            // the wall so bare-metal users can size `SESSION_CAP` up, then
            // suppress further warnings so a saturated tracker does not
            // spam the log at the incoming-packet rate.
            if !self.saturation_warned {
                crate::log::warn!(
                    "SessionTracker at capacity ({}); dropping new sender state for \
                     sender={} transport={:?} svc=0x{:04X} inst=0x{:04X}. Reboot \
                     detection disabled for this entry and any further new entries \
                     (subsequent drops not logged).",
                    SESSION_CAP,
                    sender,
                    transport,
                    service_id,
                    instance_id
                );
                self.saturation_warned = true;
            }
        }
        verdict
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::net::{Ipv4Addr, SocketAddr};

    fn addr(port: u16) -> SocketAddr {
        SocketAddr::new(Ipv4Addr::new(192, 168, 1, 10).into(), port)
    }

    const SVC: u16 = 0x0047;
    const INST: u16 = 0x0001;
    const SVC_B: u16 = 0x005D;
    const RB: RebootFlag = RebootFlag::RecentlyRebooted;
    const CONT: RebootFlag = RebootFlag::Continuous;

    #[test]
    fn first_message_returns_initial() {
        let mut tracker = SessionTracker::default();
        let verdict = tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 1, RB);
        assert_eq!(verdict, SessionVerdict::Initial);
    }

    #[test]
    fn normal_increment_returns_ok() {
        let mut tracker = SessionTracker::default();
        tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 1, RB);
        let verdict = tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 2, RB);
        assert_eq!(verdict, SessionVerdict::Ok);
    }

    #[test]
    fn reboot_flag_continuous_to_recently_rebooted_returns_reboot() {
        let mut tracker = SessionTracker::default();
        tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 100, CONT);
        let verdict = tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 1, RB);
        assert_eq!(verdict, SessionVerdict::Reboot);
    }

    #[test]
    fn session_id_decrease_same_service_with_recently_rebooted_returns_reboot() {
        // Within a single service instance, session ID decrease is a real reboot.
        let mut tracker = SessionTracker::default();
        tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 100, RB);
        let verdict = tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 50, RB);
        assert_eq!(verdict, SessionVerdict::Reboot);
    }

    #[test]
    fn session_id_decrease_different_services_no_false_reboot() {
        // Different service instances have independent counters — interleaving
        // does not cause false reboots.
        let mut tracker = SessionTracker::default();
        tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 100, RB);
        // Different service, lower session ID — this is Initial, not Reboot.
        let verdict = tracker.check(addr(1000), TransportKind::Multicast, SVC_B, INST, 50, RB);
        assert_eq!(verdict, SessionVerdict::Initial);
    }

    #[test]
    fn interleaved_sd_offers_no_false_reboot() {
        // Simulates the real-world scenario: sensor sends alternating SD offers
        // for service A (session 1,2,3...) and service B (session 1,2,3...).
        // The old per-sender tracking would see: 1, 1(decrease!), 2, 2(decrease!), ...
        // Per-service tracking sees each stream independently.
        let mut tracker = SessionTracker::default();
        // Service A: session 1
        tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 1, RB);
        // Service B: session 1 (would have been "decrease" with per-sender tracking)
        let v = tracker.check(addr(1000), TransportKind::Multicast, SVC_B, INST, 1, RB);
        assert_eq!(v, SessionVerdict::Initial);
        // Service A: session 2
        let v = tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 2, RB);
        assert_eq!(v, SessionVerdict::Ok);
        // Service B: session 2
        let v = tracker.check(addr(1000), TransportKind::Multicast, SVC_B, INST, 2, RB);
        assert_eq!(v, SessionVerdict::Ok);
    }

    #[test]
    fn session_id_decrease_with_continuous_returns_ok() {
        let mut tracker = SessionTracker::default();
        tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 100, CONT);
        // Session ID decrease while Continuous — not a reboot
        let verdict = tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 50, CONT);
        assert_eq!(verdict, SessionVerdict::Ok);
    }

    #[test]
    fn different_transports_tracked_separately() {
        let mut tracker = SessionTracker::default();
        tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 100, RB);
        // Same sender+service, different transport — first message on Unicast
        let verdict = tracker.check(addr(1000), TransportKind::Unicast, SVC, INST, 1, RB);
        assert_eq!(verdict, SessionVerdict::Initial);
    }

    #[test]
    fn different_senders_tracked_separately() {
        let mut tracker = SessionTracker::default();
        tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 100, RB);
        // Different sender — first message
        let verdict = tracker.check(addr(2000), TransportKind::Multicast, SVC, INST, 1, RB);
        assert_eq!(verdict, SessionVerdict::Initial);
    }

    #[test]
    fn reboot_flag_recently_rebooted_to_continuous_returns_ok() {
        let mut tracker = SessionTracker::default();
        tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 100, RB);
        // RecentlyRebooted→Continuous is not a reboot (it means session ID wrapped)
        let verdict = tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 101, CONT);
        assert_eq!(verdict, SessionVerdict::Ok);
    }

    #[test]
    fn same_session_id_with_recently_rebooted_returns_ok() {
        let mut tracker = SessionTracker::default();
        tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 5, RB);
        // Same session ID (not a decrease) should be OK
        let verdict = tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 5, RB);
        assert_eq!(verdict, SessionVerdict::Ok);
    }

    #[test]
    fn different_instance_ids_tracked_separately() {
        let mut tracker = SessionTracker::default();
        tracker.check(addr(1000), TransportKind::Multicast, SVC, 0x0001, 100, RB);
        // Same service, different instance — first message
        let verdict = tracker.check(addr(1000), TransportKind::Multicast, SVC, 0x0002, 1, RB);
        assert_eq!(verdict, SessionVerdict::Initial);
    }

    #[test]
    fn session_id_wrap_around_returns_ok() {
        // 0xFFFF→1 with RecentlyRebooted is a normal counter wrap, not a reboot.
        let mut tracker = SessionTracker::default();
        tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 65535, RB);
        let verdict = tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 1, RB);
        assert_eq!(verdict, SessionVerdict::Ok);
    }

    #[test]
    fn session_id_wrap_around_then_normal_increment() {
        // After a wrap (0xFFFF→1), normal incrementing should continue as Ok.
        let mut tracker = SessionTracker::default();
        tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 65535, RB);
        tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 1, RB);
        let verdict = tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 2, RB);
        assert_eq!(verdict, SessionVerdict::Ok);
    }

    #[test]
    fn session_id_wrap_to_zero_returns_ok() {
        // 0xFFFF→0: non-spec-compliant wrap scheme, still treated as a normal wrap.
        let mut tracker = SessionTracker::default();
        tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 65535, RB);
        let verdict = tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 0, RB);
        assert_eq!(verdict, SessionVerdict::Ok);
    }

    #[test]
    fn reboot_flag_transition_with_session_id_decrease_both_signal_reboot() {
        // Both indicators fire at once (Continuous→RecentlyRebooted AND session ID reset).
        let mut tracker = SessionTracker::default();
        tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 100, CONT);
        let verdict = tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 1, RB);
        assert_eq!(verdict, SessionVerdict::Reboot);
    }

    #[test]
    fn multiple_reboots_in_sequence() {
        let mut tracker = SessionTracker::default();
        tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 1, RB);
        tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 50, RB);
        // First reboot (session ID decrease)
        let v = tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 1, RB);
        assert_eq!(v, SessionVerdict::Reboot);
        // Normal traffic after reboot
        let v = tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 2, RB);
        assert_eq!(v, SessionVerdict::Ok);
        // Second reboot (flag transition)
        tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 10, CONT);
        let v = tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 1, RB);
        assert_eq!(v, SessionVerdict::Reboot);
    }

    #[test]
    fn interleaved_offers_with_real_reboot() {
        // Two services interleaving, then one experiences a real reboot.
        let mut tracker = SessionTracker::default();
        tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 10, RB);
        tracker.check(addr(1000), TransportKind::Multicast, SVC_B, INST, 10, RB);
        tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 11, RB);
        tracker.check(addr(1000), TransportKind::Multicast, SVC_B, INST, 11, RB);

        // Sensor reboots — both services restart at session 1 with RecentlyRebooted.
        // Flag was already RecentlyRebooted, so session ID decrease triggers reboot.
        let v = tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 1, RB);
        assert_eq!(v, SessionVerdict::Reboot);
        let v = tracker.check(addr(1000), TransportKind::Multicast, SVC_B, INST, 1, RB);
        assert_eq!(v, SessionVerdict::Reboot);
    }

    #[test]
    fn normal_increment_with_continuous_returns_ok() {
        let mut tracker = SessionTracker::default();
        tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 1, CONT);
        let verdict = tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 2, CONT);
        assert_eq!(verdict, SessionVerdict::Ok);
    }

    #[test]
    fn capacity_overflow_drops_new_entries_but_keeps_existing_tracking() {
        // Fill the tracker to capacity with unique (sender, service) tuples.
        let mut tracker = SessionTracker::default();
        for i in 0..super::SESSION_CAP {
            let port = 1000 + u16::try_from(i).unwrap();
            let v = tracker.check(addr(port), TransportKind::Multicast, SVC, INST, 1, RB);
            assert_eq!(v, SessionVerdict::Initial);
        }

        // One more insert — map is full, new entry dropped. The verdict is
        // still Initial (no prior state for this key), but the state is
        // never stored so a follow-up is also Initial.
        let overflow_addr = addr(9999);
        let v = tracker.check(overflow_addr, TransportKind::Multicast, SVC, INST, 1, RB);
        assert_eq!(v, SessionVerdict::Initial);
        // Because the insert failed, a second call with the same key still
        // sees no stored state.
        let v = tracker.check(overflow_addr, TransportKind::Multicast, SVC, INST, 2, RB);
        assert_eq!(v, SessionVerdict::Initial);

        // Previously-tracked senders continue to work normally.
        let v = tracker.check(addr(1000), TransportKind::Multicast, SVC, INST, 2, RB);
        assert_eq!(v, SessionVerdict::Ok);
    }

    #[test]
    fn capacity_overflow_warns_only_on_first_hit() {
        // `saturation_warned` is the latch that guards the crate::log::warn!
        // call in `check()`. It must flip false → true on the first
        // rejected insert and stay true for subsequent hits — otherwise
        // a saturated tracker spams the log at the packet rate.
        let mut tracker = SessionTracker::default();
        for i in 0..super::SESSION_CAP {
            let port = 1000 + u16::try_from(i).unwrap();
            tracker.check(addr(port), TransportKind::Multicast, SVC, INST, 1, RB);
        }
        assert!(
            !tracker.saturation_warned,
            "filling to exactly capacity must not trip the warn flag",
        );

        // First overflowing key: flag flips to true.
        tracker.check(addr(9001), TransportKind::Multicast, SVC, INST, 1, RB);
        assert!(tracker.saturation_warned);

        // Subsequent overflows leave the flag true; the flag is what the
        // implementation checks before emitting a fresh warn!.
        tracker.check(addr(9002), TransportKind::Multicast, SVC, INST, 1, RB);
        tracker.check(addr(9003), TransportKind::Multicast, SVC, INST, 1, RB);
        assert!(tracker.saturation_warned);
    }

    #[test]
    fn interleaved_transports_for_same_instance_do_not_false_reboot() {
        // A sensor keeps independent SD session-id domains per transport
        // (multicast ~1468, unicast ~739). Tracked under distinct keys they
        // never look like a reboot when interleaved; a real counter reset
        // within one domain still does.
        let mut t = SessionTracker::default();
        let a = addr(30490);
        assert_eq!(
            t.check(a, TransportKind::Multicast, SVC, INST, 1468, RB),
            SessionVerdict::Initial
        );
        assert_eq!(
            t.check(a, TransportKind::Unicast, SVC, INST, 739, RB),
            SessionVerdict::Initial
        );
        assert_eq!(
            t.check(a, TransportKind::Multicast, SVC, INST, 1469, RB),
            SessionVerdict::Ok
        );
        assert_eq!(
            t.check(a, TransportKind::Unicast, SVC, INST, 740, RB),
            SessionVerdict::Ok
        );
        assert_eq!(
            t.check(a, TransportKind::Multicast, SVC, INST, 3, RB),
            SessionVerdict::Reboot
        );
    }

    #[test]
    fn same_transport_mis_tag_false_reboots() {
        // Documents the caller bug this fix targets: a unicast datagram
        // mis-tagged Multicast collapses two domains onto one key, so its low
        // session id looks like a decrease and is wrongly reported as a reboot.
        let mut t = SessionTracker::default();
        let a = addr(30490);
        t.check(a, TransportKind::Multicast, SVC, INST, 1468, RB);
        assert_eq!(
            t.check(a, TransportKind::Multicast, SVC, INST, 739, RB),
            SessionVerdict::Reboot
        );
    }
}