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
//! An implementation of Ping, with focus on using little RAM and producing output suitable to a
//! (possibly polling, especially for lack of implemented observations) data retreival method.
//!
//! This works on a static list of potential pingings (always 4, to be configurable when const
//! generic defaults are usable in RIOT's used nightly).
//!
//! This is losely based on the sc_gnrc_icmpv6_echo implementation in RIOT.
//!
//! ## Usage
//!
//! When registered under `/ping` (see [ping_tree] help for why this is essential right now
//! unfortunately), send POST requests with an address to `/ping/`, fetch from the indicated
//! location with GET, and remove the obtained data with DELETE to free up resources again:
//!
//! ```shell
//! $ aiocoap-client 'coap://[2001:db8::1]/ping/' -m POST --content-format 0 --payload 2001:db8::2
//! Location options indicate new resource: /ping/47
//! $ aiocoap-client 'coap://[2001:db8::1]/ping/47'
//! CBOR message shown in naïve Python decoding
//! {'address': b'\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02',
//!  'identifier': 47,
//!  'stop_after': 20,
//!  'stats': {'sent': 10, 'seen_in_time': 9, 'repeats_in_time': 0, 'late': 0}}
//! $ aiocoap-client 'coap://[2001:db8::1]/ping/47' -m DELETE
//! ```
//!
//! Currently, only a custom (and not thought-through -- essentially, what serde happens to
//! produce) CBOR format is returned, and addresses are only accepted in text format.
//!
//! Pings do not measure time (apart from marking a packet as "late" if it's more than 16 seconds
//! behind), and are not configurable in terms of ping interval (which is currently 1 second
//! independent of the arrival of any packets).
//!
//! Rough plans for extensibility include logging of delay as minimum, average and maximum (all of
//! which can be done in constant space), finer granularity of time reporting (the current API only
//! measures times in numbers packets sent between then and now), reporting in user-defined bins,
//! and retention of responding addresses (useful in multicast pings). Possibly, those would be
//! mutually exclusive to keep space small (eg. you can get a list of address-count pairs *or* a
//! fine-grained histogram).

use coap_handler_implementations::wkc;
use coap_message::Code;
use coap_message_utils::{Error, OptionsExt, option_value::Block2RequestData};

use riot_wrappers::gnrc::{self, netapi, pktbuf};

/// Type used for all counts of pings. Makes sense as u16 because that's how wide the sequence
/// number field is (but in principle there shouldn't be anything wrong with going larger and
/// spreading identifier and sequence number into the payload).
type Count = u16;
/// Receive window type. Going for u16 just because it works well alignment-wise when mixed with
/// Count values. (Should work just as well with u128, though).
type Field = u16;

#[derive(Debug, serde::Serialize)]
// Counters ... maybe more "stats", where the stats implementation may be switchable between
// "accept long delays with little temporal granularity" (bitfield), "high precision but old
// packets are easily discarded" (timestamps), something inbetween (timestamps for short-term),
// plus time stats in a slotted fashion (means, minmax, predefined bins), or also
// just-count-but-store-ip-addresses (for multicast)?
struct PingStats {
    /// Number of requets sent (which is the precise number of ticks received)
    sent: Count,
    /// Bit field indicating for which request a response has been seen. Bit (1 << n) is 1 if
    /// message (sent - n) was received once already.
    ///
    /// (If we wanted to measure time in ticks or any other unit of time, this'd become an array of
    /// Option<send_timestamp>).
    #[serde(skip_serializing)]
    latest: Field,
    /// Number of responses received in time (ie. before going out of `latest`)
    seen_in_time: Count,
    /// Number of responses received in time to be counted again after already having been seen
    repeats_in_time: Count,
    /// Number of responses received after going out of `latest` (may be initials if there was any
    /// packet loss, may be duplicates)
    late: Count,
}

impl PingStats {
    fn new() -> Self {
        Self {
            sent: 0,
            latest: 0,
            seen_in_time: 0,
            repeats_in_time: 0,
            late: 0,
        }
    }

    fn tick(&mut self) {
        self.sent += 1;
        self.latest <<= 1;

        // Not sending the message -- that's up to the PingJob
    }

    fn receive_number(&mut self, number: Count) {
        if number < self.sent && number >= self.sent.saturating_sub(Field::BITS as _) {
            let bit = 1 << (self.sent - number);
            if self.latest & bit == 0 {
                self.latest |= bit;
                self.seen_in_time += 1;
            } else {
                self.repeats_in_time += 1;
            }
        } else {
            self.late += 1;
        }
    }
}

#[derive(Debug, serde::Serialize)]
struct PingJob {
    /// Destination address (initially without any zone identifer)
    #[serde(with = "serde_ip")]
    address: no_std_net::Ipv6Addr,
    /// Field that sets this ping apart from other pings between the same hosts (especially when
    /// multicast pinging).
    identifier: u16,

    stop_after: Count,

    stats: PingStats,
}

mod serde_ip {
    pub(super) fn serialize<S>(
        addr: &no_std_net::Ipv6Addr,
        serializer: S,
    ) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        // FIXME: tag according to RFC 9164 (tag 54 fits neatly around this)
        serializer.serialize_bytes(&addr.octets())
    }
}

impl PingJob {
    fn new(stop_after: Count, address: no_std_net::Ipv6Addr, identifier: u16) -> Self {
        Self {
            address,
            stop_after,
            identifier,
            stats: PingStats::new(),
        }
    }

    fn tick(&mut self) {
        if self.stats.sent < self.stop_after {
            self.send(self.stats.sent);

            // FIXME: Add a version that increments time for the late-comers but doesn't increment
            // the number of presumably sent packets? (It'd be tempting to just tick on, but then
            // the number of shown-sent packets increases as that's coupled to the window view of
            // the latest packets)
            self.stats.tick();
        }
    }

    fn received(&mut self, identifier: u16, seqno: u16, _sender: no_std_net::Ipv6Addr) {
        if identifier != self.identifier {
            return;
        }
        self.stats.receive_number(seqno);
    }

    // somewhat similar to sc_gnrc_icmpv6_echo.c's _pinger
    fn send(&self, counter: u16) {
        let pkt = pktbuf::Pktsnip::icmpv6_echo_build(
            gnrc::icmpv6::EchoType::Request,
            self.identifier,
            counter,
            &[],
            )
            .unwrap();
        let pkt = pkt.ipv6_hdr_build(None, Some(&self.address.into()))
            // Could unwrap too, but unlike icmpv6_echo_build this still returns an option (see
            // NotEnoughSpace comment)
            .expect("No space to create IPv6 header buffer");

        // Not checking whether it was actually taken up by anything; there's the silent assumption
        // that when IPv6 is available, there's also any link layer to take it.
        netapi::dispatch_send(
                riot_sys::gnrc_nettype_t_GNRC_NETTYPE_IPV6,
                riot_sys::GNRC_NETREG_DEMUX_CTX_ALL,
                pkt
                );
    }
}

/// Container for a number of ping jobs
///
/// To make this run, the pool's tick method needs to be called at regular intervals (at least
/// while its `any_active_now` returns true), and any incoming ICMP responses need to be turned
/// over to its `.received` method.
///
/// In parallel to that, the PingPool needs to be registered with a CoAP handler (see [ping_tree]).
#[derive(Default, Debug)]
pub struct PingPool {
    jobs: [Option<PingJob>; 4],
    // Place to store some altering numbers; by not just being the slot index, we can ensure that
    // packets that are around between two jobs will still not wind up in the old ping's count.
    next_id: u16,
}

impl PingPool {
    pub fn tick(&mut self) {
        for ping in self.jobs.iter_mut().filter_map(|x| x.as_mut()) {
            ping.tick();
        }
    }

    pub fn received(&mut self, packet: pktbuf::Pktsnip<pktbuf::Shared>) {
        // We *could* take a mutex here and thus keep it unlocked for a bit longer while processing
        // the response -- this is just a bit easier to call, and likely doesn't matter much.

        let sender = packet
            .ipv6_get_header()
            .expect("Recieved packet that's not IPv6")
            .src();
        let icmpv6 = packet
            .search_type(riot_sys::gnrc_nettype_t_GNRC_NETTYPE_ICMPV6)
            .expect("Received packet that's not ICMPv6");

        // It's probably even guaranteed through the stack (and not
        // ostrictly necessary here as the below does index-checks)
        assert!(icmpv6.data.len() >= 8);
        // Funnily, RIOT has ways of building these but none of parsing...
        let identifier = u16::from_be_bytes(icmpv6.data[4..6].try_into().unwrap());
        let seqno = u16::from_be_bytes(icmpv6.data[6..8].try_into().unwrap());

        for ping in self.jobs.iter_mut().filter_map(|x| x.as_mut()) {
            ping.received(identifier, seqno, (*sender).into());
        }
    }

    pub fn any_active_now(&self) -> bool {
        self.jobs.iter().any(|s| s.is_some())
    }
}

/// A resource tree representing a set of launchable pings.
///
/// See [PingPool] documentation on what else needs to be done with this besides being registered
/// in a CoAP handler.
///
/// Caveat: Due to shortcomings of the Handler trait and/or the tree building, this should really
/// be `.below(&["ping"], ...)`, for the /ping/ prefix is hardcoded in the Location-Path responses.
pub fn ping_tree<'a>(pings: &'a riot_wrappers::mutex::Mutex<PingPool>) -> impl coap_handler::Handler + coap_handler::Reporting + 'a {
    const ROOT_NAME: &str = "";

    #[derive(Copy, Clone)]
    enum PathState {
        Empty,
        Root,
        Id(u16),
        Derailed,
    }

    use PathState::*;

    impl PathState {
        fn feed(&mut self, segment: &str) {
            *self = match (*self, segment, segment.parse()) {
                (Empty, ROOT_NAME, _) => Root,
                (Empty, _, Ok(n)) => Id(n),
                _ => Derailed,
            };
        }
    }

    struct PingTree<'a> {
        pings: &'a riot_wrappers::mutex::Mutex<PingPool>,
    }

    enum RequestData {
        GetIndex(Block2RequestData),
        PostOk(u16),
        PostAllFull,
        Get(u16, Block2RequestData),
        DeleteOk,
    }

    use RequestData::*;
    impl<'a> coap_handler::Handler for PingTree<'a> {
        type RequestData = RequestData;

        type ExtractRequestError = Error;
        type BuildResponseError<M: coap_message::MinimalWritableMessage> = Error;

        fn extract_request_data<M: coap_message::ReadableMessage>(&mut self, m: &M) -> Result<Self::RequestData, Error> {
            let mut path = Empty;

            let mut block2 = None;
            let mut content_format: Option<u16> = None;

            m.options()
                .take_uri_path(|p| path.feed(p))
                // Careful: This is critical, so handlers that don't use it (or are already erring)
                // need to process it.
                .take_block2(&mut block2)
                // Elective
                .filter_map(|o| {
                    use coap_message::MessageOption;
                    if o.number() == coap_numbers::option::CONTENT_FORMAT {
                        content_format = o.value_uint();
                        None
                    } else {
                        Some(o)
                    }
                })
                .ignore_elective_others()?;

            match (m.code().into(), path) {
                (_, Empty | Derailed) => Err(Error::not_found()),
                (coap_numbers::code::GET, Root) => Ok(GetIndex(block2.unwrap_or_default())),
                (coap_numbers::code::POST, Root) => {
                    if block2.is_some() {
                        return Err(Error::bad_option(coap_numbers::option::BLOCK2));
                    }

                    let ip;

                    match content_format {
                        Some(0) => {
                            ip = core::str::from_utf8(m.payload())
                                .map_err(|ue| Error::bad_request_with_rbep(ue.valid_up_to()))?
                                // not even no-std-net exports its AddrParseError :-/
                                .parse()
                                // FIXME Can we get a rbep?
                                .map_err(|_| Error::bad_request())?;
                        },
                        _ => {
                            return Err(Error::bad_option(coap_numbers::option::CONTENT_FORMAT));
                        }
                    }

                    let mut pings = self.pings.try_lock()
                        // just processing incoming pings or sending some out; come back later
                        .ok_or(Error::service_unavailable())?;

                    let id = pings.next_id;
                    // We could do something terribly clever here to avoid going to a still-active
                    // slot after 64k jobs have been deleted while an old one is still active ...
                    // not worth it.
                    pings.next_id = pings.next_id.wrapping_add(1);

                    if let Some(p) = pings.jobs.iter_mut().filter(|p| p.is_none()).next() {
                        *p = Some(PingJob::new(20, ip, id));

                        // Resisting the temptation to send a tick right away: There's a good code
                        // path to it after the timer triggers, possibly in another thread if Gcoap
                        // is used, so no need to be special here.

                        Ok(PostOk(id))
                    } else {
                        Ok(PostAllFull)
                    }
                }
                // Not checking if actually available -- might change until the response handler
                // anyway (at least in theory, as we don't hold a lock across)
                (coap_numbers::code::GET, Id(n)) => Ok(Get(n, block2.unwrap_or_default())),
                (coap_numbers::code::DELETE, Id(n)) => {
                    if block2.is_some() {
                        return Err(Error::bad_option(coap_numbers::option::BLOCK2));
                    }

                    let mut pings = self.pings.try_lock()
                        .ok_or(Error::service_unavailable())?;

                    for slot in pings.jobs.iter_mut() {
                        match slot {
                            Some(PingJob { ref identifier, .. }) if identifier == &n => {
                                *slot = None
                            }
                            _ => ()
                        }
                    }
                    Ok(DeleteOk)
                }
                (_, _) => Err(Error::method_not_allowed()),
            }
        }
        fn estimate_length(&mut self, _rd: &Self::RequestData) -> usize {
            // No clue yet, really
            1025
        }
        fn build_response<M: coap_message::MutableWritableMessage>(&mut self, m: &mut M, rd: Self::RequestData) -> Result<(), Error> {
            match rd {
                PostOk(n) => {
                    m.set_code(coap_numbers::code::CHANGED.try_into().map_err(|_| ()).unwrap());
                    let mut buf: heapless::String<5> = heapless::String::new();
                    use core::fmt::Write;
                    write!(buf, "{}", n).expect("Number does fit");
                    // BIG FIXME: How to get that cleanly back? Would the same stripper that
                    // removed the prefix also inject the Location in a special-purpose response
                    // type? But how about any inline data?
                    let location_path = || coap_message::OptionNumber::new(coap_numbers::option::LOCATION_PATH)
                        .map_err(Error::from_unionerror);
                    m.add_option(location_path()?, b"ping")
                        .map_err(Error::from_unionerror)?;
                    m.add_option(location_path()?, buf.as_ref())
                        .map_err(Error::from_unionerror)?;
                    m.set_payload(b"")
                        .map_err(Error::from_unionerror)?;
                }
                Get(i, block2) => {
                    let pings = match self.pings.try_lock() {
                        Some(pings) => pings,
                        None => return Err(Error::service_unavailable()),
                    };

                    let ping = match pings.jobs.iter().find(|p| p.as_ref().map(|p| p.identifier == i).unwrap_or(false)) {
                        Some(Some(p)) => p,
                        _ => return Err(Error::not_found()),
                    };

                    m.set_code(coap_numbers::code::CONTENT.try_into().ok().unwrap());
                    use serde::Serialize;
                    // FIXME: This doesn't send a content-format
                    coap_handler_implementations::helpers::block2_write(block2, m, |win| ping.serialize(&mut serde_cbor::ser::Serializer::new(win)));
                }
                GetIndex(block2) => {
                    let pings = match self.pings.try_lock() {
                        Some(pings) => pings,
                        None => return Err(Error::service_unavailable()),
                    };

                    // FIXME: Say something sensible (but that's rather hard given we'd have to
                    // create a TypeRequestData by masking away the path and sending it
                    // through extract_request_data rather than getting one out from the options).
                    //
                    // Ideally this'd be a CoRAL document, and then we also don't have to worry
                    // about path-absolute references.

                    m.set_code(coap_numbers::code::CONTENT.try_into().ok().unwrap());
                    use serde::Serialize;
                    // FIXME: This doesn't send a content-format
                    let pings: &PingPool = &*pings;
                    let jobs: heapless::Vec<u16, 4> = pings.jobs.iter().filter_map(|j| j.as_ref()).map(|j| j.identifier).collect();
                    coap_handler_implementations::helpers::block2_write(block2, m, |win| jobs.serialize(&mut serde_cbor::ser::Serializer::new(win)));
                }
                PostAllFull => {
                    m.set_code(Code::new(coap_numbers::code::SERVICE_UNAVAILABLE)
                        .map_err(Error::from_unionerror)?);
                    m.set_payload(b"DELETE some old pings")
                        .map_err(Error::from_unionerror)?;
                }
                DeleteOk => {
                    // For idempotency we just send DELETE always
                    m.set_code(Code::new(coap_numbers::code::DELETED)
                        .map_err(Error::from_unionerror)?);
                }
            };
            Ok(())
        }
    }

    let ping_tree = PingTree {
        pings
    };
    wkc::ConstantSingleRecordReport::new_with_path(ping_tree, &[coap_handler::Attribute::Title("Send ICMP pings")], &[ROOT_NAME])
}