netstack3_ip/
counters.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Facilities for tracking the counts of various IP events.
6
7use core::fmt::Debug;
8use net_types::ip::{GenericOverIp, Ip, Ipv4, Ipv6};
9use netstack3_base::{
10    Counter, CounterRepr, Inspectable, Inspector, InspectorExt as _, TestOnlyFrom,
11    TestOnlyPartialEq,
12};
13
14use crate::internal::fragmentation::FragmentationCounters;
15
16/// An IP extension trait supporting counters at the IP layer.
17pub trait IpCountersIpExt: Ip {
18    /// Receive counters.
19    type RxCounters<C: CounterRepr>: Default
20        + Debug
21        + Inspectable
22        + TestOnlyPartialEq
23        + for<'a> TestOnlyFrom<&'a Self::RxCounters<Counter>>;
24}
25
26impl IpCountersIpExt for Ipv4 {
27    type RxCounters<C: CounterRepr> = Ipv4RxCounters<C>;
28}
29
30impl IpCountersIpExt for Ipv6 {
31    type RxCounters<C: CounterRepr> = Ipv6RxCounters<C>;
32}
33
34/// Ip layer counters.
35#[derive(Default, Debug, GenericOverIp)]
36#[generic_over_ip(I, Ip)]
37#[cfg_attr(any(test, feature = "testutils"), derive(PartialEq))]
38pub struct IpCounters<I: IpCountersIpExt, C: CounterRepr = Counter> {
39    /// Count of incoming IP unicast packets delivered.
40    pub deliver_unicast: C,
41    /// Count of incoming IP multicast packets delivered.
42    pub deliver_multicast: C,
43    /// Count of incoming IP packets that are dispatched to the appropriate protocol.
44    pub dispatch_receive_ip_packet: C,
45    /// Count of incoming IP packets destined to another host.
46    pub dispatch_receive_ip_packet_other_host: C,
47    /// Count of incoming IP packets received by the stack.
48    pub receive_ip_packet: C,
49    /// Count of sent outgoing IP packets.
50    pub send_ip_packet: C,
51    /// Count of packets to be forwarded which are instead dropped because
52    /// forwarding is disabled.
53    pub forwarding_disabled: C,
54    /// Count of incoming packets forwarded to another host.
55    pub forward: C,
56    /// Count of incoming packets which cannot be forwarded because there is no
57    /// route to the destination host.
58    pub no_route_to_host: C,
59    /// Count of incoming packets which cannot be forwarded because the MTU has
60    /// been exceeded.
61    pub mtu_exceeded: C,
62    /// Count of incoming packets which cannot be forwarded because the TTL has
63    /// expired.
64    pub ttl_expired: C,
65    /// Count of ICMP error messages received.
66    pub receive_icmp_error: C,
67    /// Count of IP fragment reassembly errors.
68    pub fragment_reassembly_error: C,
69    /// Count of IP fragments that could not be reassembled because more
70    /// fragments were needed.
71    pub need_more_fragments: C,
72    /// Count of IP fragments that could not be reassembled because the fragment
73    /// was invalid.
74    pub invalid_fragment: C,
75    /// Count of IP fragments that could not be reassembled because the stack's
76    /// per-IP-protocol fragment cache was full.
77    pub fragment_cache_full: C,
78    /// Count of incoming IP packets not delivered because of a parameter problem.
79    pub parameter_problem: C,
80    /// Count of incoming IP packets with an unspecified destination address.
81    pub unspecified_destination: C,
82    /// Count of incoming IP packets with an unspecified source address.
83    pub unspecified_source: C,
84    /// Count of incoming IP packets dropped.
85    pub dropped: C,
86    /// Number of frames rejected because they'd cause illegal loopback
87    /// addresses on the wire.
88    pub tx_illegal_loopback_address: C,
89    /// Version specific rx counters.
90    pub version_rx: I::RxCounters<C>,
91    /// Count of incoming IP multicast packets that were dropped because
92    /// The stack doesn't have any sockets that belong to the multicast group,
93    /// and the stack isn't configured to forward the multicast packet.
94    pub multicast_no_interest: C,
95    /// Count of looped-back packets that held a cached conntrack entry that could
96    /// not be downcasted to the expected type. This would happen if, for example, a
97    /// packet was modified to a different IP version between EGRESS and INGRESS.
98    pub invalid_cached_conntrack_entry: C,
99    /// IP fragmentation counters.
100    pub fragmentation: FragmentationCounters<C>,
101}
102
103impl<I: IpCountersIpExt, C: CounterRepr> Inspectable for IpCounters<I, C> {
104    fn record<II: Inspector>(&self, inspector: &mut II) {
105        let IpCounters {
106            deliver_unicast,
107            deliver_multicast,
108            dispatch_receive_ip_packet,
109            dispatch_receive_ip_packet_other_host,
110            receive_ip_packet,
111            send_ip_packet,
112            forwarding_disabled,
113            forward,
114            no_route_to_host,
115            mtu_exceeded,
116            ttl_expired,
117            receive_icmp_error,
118            fragment_reassembly_error,
119            need_more_fragments,
120            invalid_fragment,
121            fragment_cache_full,
122            parameter_problem,
123            unspecified_destination,
124            unspecified_source,
125            dropped,
126            tx_illegal_loopback_address,
127            version_rx,
128            multicast_no_interest,
129            invalid_cached_conntrack_entry,
130            fragmentation,
131        } = self;
132        inspector.record_child("PacketTx", |inspector| {
133            inspector.record_counter("Sent", send_ip_packet);
134            inspector.record_counter("IllegalLoopbackAddress", tx_illegal_loopback_address);
135        });
136        inspector.record_child("PacketRx", |inspector| {
137            inspector.record_counter("Received", receive_ip_packet);
138            inspector.record_counter("Dispatched", dispatch_receive_ip_packet);
139            inspector.record_counter("OtherHost", dispatch_receive_ip_packet_other_host);
140            inspector.record_counter("ParameterProblem", parameter_problem);
141            inspector.record_counter("UnspecifiedDst", unspecified_destination);
142            inspector.record_counter("UnspecifiedSrc", unspecified_source);
143            inspector.record_counter("Dropped", dropped);
144            inspector.record_counter("MulticastNoInterest", multicast_no_interest);
145            inspector.record_counter("DeliveredUnicast", deliver_unicast);
146            inspector.record_counter("DeliveredMulticast", deliver_multicast);
147            inspector.record_counter("InvalidCachedConntrackEntry", invalid_cached_conntrack_entry);
148            inspector.delegate_inspectable(version_rx);
149        });
150        inspector.record_child("Forwarding", |inspector| {
151            inspector.record_counter("Forwarded", forward);
152            inspector.record_counter("ForwardingDisabled", forwarding_disabled);
153            inspector.record_counter("NoRouteToHost", no_route_to_host);
154            inspector.record_counter("MtuExceeded", mtu_exceeded);
155            inspector.record_counter("TtlExpired", ttl_expired);
156        });
157        inspector.record_counter("RxIcmpError", receive_icmp_error);
158        inspector.record_child("FragmentsRx", |inspector| {
159            inspector.record_counter("ReassemblyError", fragment_reassembly_error);
160            inspector.record_counter("NeedMoreFragments", need_more_fragments);
161            inspector.record_counter("InvalidFragment", invalid_fragment);
162            inspector.record_counter("CacheFull", fragment_cache_full);
163        });
164        inspector.record_child("FragmentsTx", |inspector| {
165            let FragmentationCounters {
166                fragmentation_required,
167                fragments,
168                error_not_allowed,
169                error_mtu_too_small,
170                error_body_too_long,
171                error_inner_size_limit_exceeded,
172                error_fragmented_serializer,
173            } = fragmentation;
174            inspector.record_counter("FragmentationRequired", fragmentation_required);
175            inspector.record_counter("Fragments", fragments);
176            inspector.record_counter("ErrorNotAllowed", error_not_allowed);
177            inspector.record_counter("ErrorMtuTooSmall", error_mtu_too_small);
178            inspector.record_counter("ErrorBodyTooLong", error_body_too_long);
179            inspector
180                .record_counter("ErrorInnerSizeLimitExceeded", error_inner_size_limit_exceeded);
181            inspector.record_counter("ErrorFragmentedSerializer", error_fragmented_serializer);
182        });
183    }
184}
185
186/// IPv4-specific Rx counters.
187#[derive(Default, Debug)]
188#[cfg_attr(any(test, feature = "testutils"), derive(PartialEq))]
189pub struct Ipv4RxCounters<C: CounterRepr = Counter> {
190    /// Count of incoming broadcast IPv4 packets delivered.
191    pub deliver_broadcast: C,
192}
193
194impl<C: CounterRepr> Inspectable for Ipv4RxCounters<C> {
195    fn record<I: Inspector>(&self, inspector: &mut I) {
196        let Self { deliver_broadcast } = self;
197        inspector.record_counter("DeliveredBroadcast", deliver_broadcast);
198    }
199}
200
201/// IPv6-specific Rx counters.
202#[derive(Default, Debug)]
203#[cfg_attr(any(test, feature = "testutils"), derive(PartialEq))]
204pub struct Ipv6RxCounters<C: CounterRepr = Counter> {
205    /// Count of incoming IPv6 packets dropped because the destination address
206    /// is only tentatively assigned to the device.
207    pub drop_for_tentative: C,
208    /// Count of incoming IPv6 packets dropped due to a non-unicast source address.
209    pub non_unicast_source: C,
210    /// Count of incoming IPv6 packets discarded while processing extension
211    /// headers.
212    pub extension_header_discard: C,
213    /// Count of incoming neighbor solicitations discarded as looped-back
214    /// DAD probes.
215    pub drop_looped_back_dad_probe: C,
216}
217
218impl<C: CounterRepr> Inspectable for Ipv6RxCounters<C> {
219    fn record<I: Inspector>(&self, inspector: &mut I) {
220        let Self {
221            drop_for_tentative,
222            non_unicast_source,
223            extension_header_discard,
224            drop_looped_back_dad_probe,
225        } = self;
226        inspector.record_counter("DroppedTentativeDst", drop_for_tentative);
227        inspector.record_counter("DroppedNonUnicastSrc", non_unicast_source);
228        inspector.record_counter("DroppedExtensionHeader", extension_header_discard);
229        inspector.record_counter("DroppedLoopedBackDadProbe", drop_looped_back_dad_probe);
230    }
231}
232
233#[cfg(any(test, feature = "testutils"))]
234pub mod testutil {
235    use super::*;
236
237    use netstack3_base::ResourceCounterContext;
238
239    impl<C: CounterRepr> From<&Ipv4RxCounters> for Ipv4RxCounters<C> {
240        fn from(counters: &Ipv4RxCounters) -> Ipv4RxCounters<C> {
241            let Ipv4RxCounters { deliver_broadcast } = counters;
242            Ipv4RxCounters { deliver_broadcast: deliver_broadcast.into_repr() }
243        }
244    }
245
246    impl<C: CounterRepr> From<&Ipv6RxCounters> for Ipv6RxCounters<C> {
247        fn from(counters: &Ipv6RxCounters) -> Ipv6RxCounters<C> {
248            let Ipv6RxCounters {
249                drop_for_tentative,
250                non_unicast_source,
251                extension_header_discard,
252                drop_looped_back_dad_probe,
253            } = counters;
254            Ipv6RxCounters {
255                drop_for_tentative: drop_for_tentative.get().into_repr(),
256                non_unicast_source: non_unicast_source.into_repr(),
257                extension_header_discard: extension_header_discard.into_repr(),
258                drop_looped_back_dad_probe: drop_looped_back_dad_probe.into_repr(),
259            }
260        }
261    }
262
263    /// Expected values of [`IpCounters<I>`].
264    pub type IpCounterExpectations<I> = IpCounters<I, u64>;
265
266    impl<I: IpCountersIpExt> From<&IpCounters<I>> for IpCounterExpectations<I> {
267        fn from(counters: &IpCounters<I>) -> IpCounterExpectations<I> {
268            let IpCounters {
269                deliver_unicast,
270                deliver_multicast,
271                dispatch_receive_ip_packet,
272                dispatch_receive_ip_packet_other_host,
273                receive_ip_packet,
274                send_ip_packet,
275                forwarding_disabled,
276                forward,
277                no_route_to_host,
278                mtu_exceeded,
279                ttl_expired,
280                receive_icmp_error,
281                fragment_reassembly_error,
282                need_more_fragments,
283                invalid_fragment,
284                fragment_cache_full,
285                parameter_problem,
286                unspecified_destination,
287                unspecified_source,
288                dropped,
289                tx_illegal_loopback_address,
290                version_rx,
291                multicast_no_interest,
292                invalid_cached_conntrack_entry,
293                fragmentation,
294            } = counters;
295            IpCounterExpectations {
296                deliver_unicast: deliver_unicast.get(),
297                deliver_multicast: deliver_multicast.get(),
298                dispatch_receive_ip_packet: dispatch_receive_ip_packet.get(),
299                dispatch_receive_ip_packet_other_host: dispatch_receive_ip_packet_other_host.get(),
300                receive_ip_packet: receive_ip_packet.get(),
301                send_ip_packet: send_ip_packet.get(),
302                forwarding_disabled: forwarding_disabled.get(),
303                forward: forward.get(),
304                no_route_to_host: no_route_to_host.get(),
305                mtu_exceeded: mtu_exceeded.get(),
306                ttl_expired: ttl_expired.get(),
307                receive_icmp_error: receive_icmp_error.get(),
308                fragment_reassembly_error: fragment_reassembly_error.get(),
309                need_more_fragments: need_more_fragments.get(),
310                invalid_fragment: invalid_fragment.get(),
311                fragment_cache_full: fragment_cache_full.get(),
312                parameter_problem: parameter_problem.get(),
313                unspecified_destination: unspecified_destination.get(),
314                unspecified_source: unspecified_source.get(),
315                dropped: dropped.get(),
316                tx_illegal_loopback_address: tx_illegal_loopback_address.get(),
317                version_rx: version_rx.into(),
318                multicast_no_interest: multicast_no_interest.get(),
319                invalid_cached_conntrack_entry: invalid_cached_conntrack_entry.get(),
320                fragmentation: fragmentation.into(),
321            }
322        }
323    }
324
325    impl<I: IpCountersIpExt> IpCounterExpectations<I> {
326        /// Constructs the expected counter state when the given count of IP
327        /// packets have been received & dispatched.
328        pub fn expect_dispatched(count: u64) -> Self {
329            IpCounterExpectations {
330                receive_ip_packet: count,
331                dispatch_receive_ip_packet: count,
332                deliver_unicast: count,
333                ..Default::default()
334            }
335        }
336
337        /// Assert that the counters tracked by `core_ctx` match expectations.
338        #[track_caller]
339        pub fn assert_counters<D, CC: ResourceCounterContext<D, IpCounters<I>>>(
340            self,
341            core_ctx: &CC,
342            device: &D,
343        ) {
344            assert_eq!(
345                &IpCounterExpectations::from(core_ctx.counters()),
346                &self,
347                "stack-wide counters"
348            );
349            assert_eq!(
350                &IpCounterExpectations::from(core_ctx.per_resource_counters(device)),
351                &self,
352                "per-device counters"
353            );
354        }
355    }
356}