netstack3_udp/
diagnostics.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
5use core::convert::Infallible as Never;
6use core::num::NonZeroU16;
7
8use net_types::ip::Ip;
9use netstack3_base::socket::SocketCookie;
10use netstack3_base::{
11    Marks, Matcher, MaybeSocketTransportProperties, SocketTransportProtocolMatcher,
12    UdpSocketProperties, UdpSocketState as UdpSocketMatcherState, WeakDeviceIdentifier,
13};
14use netstack3_datagram::{
15    DatagramSocketDiagnosticsSpec, IpExt, SocketInfo, SocketState as DatagramSocketState,
16};
17
18use crate::internal::base::{Udp, UdpBindingsTypes, UdpSocketId, UdpSocketState};
19
20/// Publicly-accessible diagnostic information about UDP sockets.
21//
22// The reason this isn't on the datagram API is that we don't have plans to
23// support other datagram socket types at this time.
24#[derive(Debug)]
25#[cfg_attr(any(test, feature = "testutils"), derive(PartialEq, Eq))]
26#[allow(missing_docs)]
27pub struct UdpSocketDiagnostics<I: Ip> {
28    pub state: UdpSocketDiagnosticTuple<I>,
29    pub cookie: SocketCookie,
30    pub marks: Marks,
31}
32
33/// UDP socket tuple information for diagnostics.
34#[derive(Debug)]
35#[cfg_attr(any(test, feature = "testutils"), derive(PartialEq, Eq))]
36#[allow(missing_docs)]
37pub enum UdpSocketDiagnosticTuple<I: Ip> {
38    Bound { src_addr: Option<I::Addr>, src_port: NonZeroU16 },
39    Connected { src_addr: I::Addr, src_port: NonZeroU16, dst_addr: I::Addr, dst_port: u16 },
40}
41
42impl<I: Ip> UdpSocketDiagnosticTuple<I> {
43    /// Returns the source address of the socket.
44    pub fn src_addr(&self) -> Option<I::Addr> {
45        match self {
46            Self::Bound { src_addr, src_port: _ } => *src_addr,
47            Self::Connected { src_addr, src_port: _, dst_addr: _, dst_port: _ } => Some(*src_addr),
48        }
49    }
50
51    /// Returns the source port of the socket.
52    pub fn src_port(&self) -> Option<NonZeroU16> {
53        match self {
54            Self::Bound { src_addr: _, src_port }
55            | Self::Connected { src_addr: _, src_port, dst_addr: _, dst_port: _ } => {
56                Some(*src_port)
57            }
58        }
59    }
60
61    /// Returns the destination address of the socket.
62    pub fn dst_addr(&self) -> Option<I::Addr> {
63        match self {
64            Self::Bound { src_addr: _, src_port: _ } => None,
65            Self::Connected { src_addr: _, src_port: _, dst_addr, dst_port: _ } => Some(*dst_addr),
66        }
67    }
68
69    /// Returns the destination port of the socket.
70    pub fn dst_port(&self) -> Option<u16> {
71        match self {
72            Self::Bound { src_addr: _, src_port: _ } => None,
73            Self::Connected { src_addr: _, src_port: _, dst_addr: _, dst_port } => Some(*dst_port),
74        }
75    }
76}
77
78/// A wrapper around [`UdpSocketState`], which is defined in
79/// `netstack3_datagram`, to allow implementing traits on it.
80pub struct UdpTransportProtocolDiagnosticsProperties<'a, I, D, BT>(&'a UdpSocketState<I, D, BT>)
81where
82    I: IpExt,
83    D: WeakDeviceIdentifier,
84    BT: UdpBindingsTypes;
85
86impl<I, D, BT> MaybeSocketTransportProperties
87    for UdpTransportProtocolDiagnosticsProperties<'_, I, D, BT>
88where
89    I: IpExt,
90    D: WeakDeviceIdentifier,
91    BT: UdpBindingsTypes,
92{
93    type TcpProps<'a>
94        = Never
95    where
96        Self: 'a;
97
98    type UdpProps<'a>
99        = Self
100    where
101        Self: 'a;
102
103    fn tcp_socket_properties(&self) -> Option<&Self::TcpProps<'_>> {
104        None
105    }
106
107    fn udp_socket_properties(&self) -> Option<&Self::UdpProps<'_>> {
108        Some(self)
109    }
110}
111
112impl<I, D, BT> UdpSocketProperties for UdpTransportProtocolDiagnosticsProperties<'_, I, D, BT>
113where
114    I: IpExt,
115    D: WeakDeviceIdentifier,
116    BT: UdpBindingsTypes,
117{
118    fn src_port_matches(&self, matcher: &netstack3_base::BoundPortMatcher) -> bool {
119        let Self(udp_state) = self;
120        matcher.matches(&udp_state.local_identifier().map(|p| p.get()))
121    }
122
123    fn dst_port_matches(&self, matcher: &netstack3_base::BoundPortMatcher) -> bool {
124        let Self(udp_state) = self;
125        matcher.matches(&udp_state.remote_identifier())
126    }
127
128    fn state_matches(&self, matcher: &netstack3_base::UdpStateMatcher) -> bool {
129        let Self(udp_state) = self;
130        matcher.matches(match udp_state.to_socket_info() {
131            SocketInfo::Unbound => return false,
132            SocketInfo::Listener(_) => &UdpSocketMatcherState::Bound,
133            SocketInfo::Connected(_) => &UdpSocketMatcherState::Connected,
134        })
135    }
136}
137
138impl<BT: UdpBindingsTypes> DatagramSocketDiagnosticsSpec for Udp<BT> {
139    type DeviceClass = BT::DeviceClass;
140
141    fn transport_protocol_matches<I: IpExt, D: WeakDeviceIdentifier>(
142        state: &DatagramSocketState<I, D, Udp<BT>>,
143        matcher: &SocketTransportProtocolMatcher,
144    ) -> bool {
145        matcher.matches(&UdpTransportProtocolDiagnosticsProperties(state))
146    }
147
148    fn cookie_matches<I: IpExt, D: WeakDeviceIdentifier>(
149        id: &UdpSocketId<I, D, BT>,
150        matcher: &netstack3_base::SocketCookieMatcher,
151    ) -> bool {
152        matcher.matches(&id.socket_cookie().export_value())
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use alloc::string::ToString;
159    use alloc::vec;
160    use alloc::vec::Vec;
161    use core::num::NonZeroU16;
162
163    use ip_test_macro::ip_test;
164    use net_types::ip::Subnet;
165    use net_types::{Witness as _, ZonedAddr};
166    use netstack3_base::testutil::{FakeDeviceId, set_logger_for_test};
167    use netstack3_base::{
168        AddressMatcher, AddressMatcherEither, AddressMatcherType, BoundAddressMatcherEither,
169        BoundInterfaceMatcher, BoundPortMatcher, InterfaceMatcher, IpSocketMatcher, Mark,
170        MarkDomain, MarkMatcher, PortMatcher, SocketCookieMatcher, SocketTransportProtocolMatcher,
171        SubnetMatcher, TcpSocketMatcher, UdpSocketMatcher, UdpStateMatcher,
172    };
173
174    use crate::internal::base::testutils::{FakeUdpCoreCtx, TestIpExt, UdpFakeDeviceCtx};
175    use crate::internal::base::{UdpApi, UdpRemotePort};
176
177    use super::*;
178
179    const LOCAL_PORT_1: NonZeroU16 = NonZeroU16::new(1234).unwrap();
180    const LOCAL_PORT_2: NonZeroU16 = NonZeroU16::new(5678).unwrap();
181    const LOCAL_PORT_3: NonZeroU16 = NonZeroU16::new(4321).unwrap();
182
183    const REMOTE_PORT_1: NonZeroU16 = NonZeroU16::new(100).unwrap();
184    const REMOTE_PORT_2: NonZeroU16 = NonZeroU16::new(200).unwrap();
185
186    const MARK: u32 = 0x10;
187    const MARK_MASK: u32 = !0;
188
189    #[ip_test(I)]
190    fn diagnostics_match_ip_version<I: TestIpExt>() {
191        set_logger_for_test();
192
193        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
194        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
195
196        let socket = api.create();
197        api.listen(&socket, None, Some(LOCAL_PORT_1)).expect("listen should succeed");
198
199        let mut results = Vec::new();
200        api.bound_sockets_diagnostics(&IpSocketMatcher::Family(I::VERSION), &mut results);
201        assert_eq!(
202            results,
203            vec![UdpSocketDiagnostics {
204                state: UdpSocketDiagnosticTuple::Bound { src_addr: None, src_port: LOCAL_PORT_1 },
205                cookie: socket.socket_cookie(),
206                marks: Marks::default(),
207            }]
208        );
209
210        results.clear();
211        api.bound_sockets_diagnostics(
212            &IpSocketMatcher::Family(
213                <<I as netstack3_base::socket::DualStackIpExt>::OtherVersion as Ip>::VERSION,
214            ),
215            &mut results,
216        );
217        assert_eq!(results, Vec::new());
218    }
219
220    #[ip_test(I)]
221    fn diagnostics_match_src_addr<I: TestIpExt>() {
222        set_logger_for_test();
223
224        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
225        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
226
227        let socket = api.create();
228        api.listen(&socket, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_1))
229            .expect("listen should succeed");
230
231        let mut results = Vec::new();
232        let matcher = I::map_ip_in(
233            I::TEST_ADDRS.subnet,
234            |subnet| {
235                BoundAddressMatcherEither::Bound(AddressMatcherEither::V4(AddressMatcher {
236                    matcher: AddressMatcherType::Subnet(SubnetMatcher(subnet)),
237                    invert: false,
238                }))
239            },
240            |subnet| {
241                BoundAddressMatcherEither::Bound(AddressMatcherEither::V6(AddressMatcher {
242                    matcher: AddressMatcherType::Subnet(SubnetMatcher(subnet)),
243                    invert: false,
244                }))
245            },
246        );
247        api.bound_sockets_diagnostics(&IpSocketMatcher::SrcAddr(matcher), &mut results);
248        assert_eq!(
249            results,
250            vec![UdpSocketDiagnostics {
251                state: UdpSocketDiagnosticTuple::Bound {
252                    src_addr: Some(I::TEST_ADDRS.local_ip.get()),
253                    src_port: LOCAL_PORT_1
254                },
255                cookie: socket.socket_cookie(),
256                marks: Marks::default(),
257            }]
258        );
259
260        results.clear();
261        let matcher = I::map_ip_in(
262            I::TEST_ADDRS.remote_ip.get(),
263            |addr| {
264                BoundAddressMatcherEither::Bound(AddressMatcherEither::V4(AddressMatcher {
265                    matcher: AddressMatcherType::Subnet(SubnetMatcher(
266                        Subnet::new(addr, 32).unwrap(),
267                    )),
268                    invert: false,
269                }))
270            },
271            |addr| {
272                BoundAddressMatcherEither::Bound(AddressMatcherEither::V6(AddressMatcher {
273                    matcher: AddressMatcherType::Subnet(SubnetMatcher(
274                        Subnet::new(addr, 128).unwrap(),
275                    )),
276                    invert: false,
277                }))
278            },
279        );
280        api.bound_sockets_diagnostics(&IpSocketMatcher::SrcAddr(matcher), &mut results);
281        assert_eq!(results, Vec::new());
282    }
283
284    #[ip_test(I)]
285    fn diagnostics_match_src_addr_unbound<I: TestIpExt>() {
286        set_logger_for_test();
287
288        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
289        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
290
291        let socket1 = api.create();
292        api.listen(&socket1, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_1))
293            .expect("listen should succeed");
294
295        let socket2 = api.create();
296        api.listen(&socket2, None, Some(LOCAL_PORT_2)).expect("listen should succeed");
297
298        let mut results = Vec::new();
299        let matcher = match I::VERSION {
300            net_types::ip::IpVersion::V4 => BoundAddressMatcherEither::Unbound,
301            net_types::ip::IpVersion::V6 => BoundAddressMatcherEither::Unbound,
302        };
303        api.bound_sockets_diagnostics(&IpSocketMatcher::SrcAddr(matcher), &mut results);
304        assert_eq!(
305            results,
306            vec![UdpSocketDiagnostics {
307                state: UdpSocketDiagnosticTuple::Bound { src_addr: None, src_port: LOCAL_PORT_2 },
308                cookie: socket2.socket_cookie(),
309                marks: Marks::default(),
310            }]
311        );
312
313        results.clear();
314        let matcher = I::map_ip_in(
315            I::TEST_ADDRS.subnet,
316            |subnet| {
317                BoundAddressMatcherEither::Bound(AddressMatcherEither::V4(AddressMatcher {
318                    matcher: AddressMatcherType::Subnet(SubnetMatcher(subnet)),
319                    invert: false,
320                }))
321            },
322            |subnet| {
323                BoundAddressMatcherEither::Bound(AddressMatcherEither::V6(AddressMatcher {
324                    matcher: AddressMatcherType::Subnet(SubnetMatcher(subnet)),
325                    invert: false,
326                }))
327            },
328        );
329        api.bound_sockets_diagnostics(&IpSocketMatcher::SrcAddr(matcher), &mut results);
330        assert_eq!(
331            results,
332            vec![UdpSocketDiagnostics {
333                state: UdpSocketDiagnosticTuple::Bound {
334                    src_addr: Some(I::TEST_ADDRS.local_ip.get()),
335                    src_port: LOCAL_PORT_1
336                },
337                cookie: socket1.socket_cookie(),
338                marks: Marks::default(),
339            }]
340        );
341    }
342
343    #[ip_test(I)]
344    fn diagnostics_match_dst_addr<I: TestIpExt>() {
345        set_logger_for_test();
346
347        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
348        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
349
350        let socket = api.create();
351        api.listen(&socket, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_2))
352            .expect("listen should succeed");
353        api.connect(
354            &socket,
355            Some(ZonedAddr::Unzoned(I::TEST_ADDRS.remote_ip)),
356            UdpRemotePort::Set(LOCAL_PORT_1),
357        )
358        .expect("connect should succeed");
359
360        let mut results = Vec::new();
361        let matcher = I::map_ip_in(
362            I::TEST_ADDRS.subnet,
363            |subnet| {
364                BoundAddressMatcherEither::Bound(AddressMatcherEither::V4(AddressMatcher {
365                    matcher: AddressMatcherType::Subnet(SubnetMatcher(subnet)),
366                    invert: false,
367                }))
368            },
369            |subnet| {
370                BoundAddressMatcherEither::Bound(AddressMatcherEither::V6(AddressMatcher {
371                    matcher: AddressMatcherType::Subnet(SubnetMatcher(subnet)),
372                    invert: false,
373                }))
374            },
375        );
376        api.bound_sockets_diagnostics(&IpSocketMatcher::DstAddr(matcher), &mut results);
377        assert_eq!(
378            results,
379            vec![UdpSocketDiagnostics {
380                state: UdpSocketDiagnosticTuple::Connected {
381                    src_addr: I::TEST_ADDRS.local_ip.get(),
382                    src_port: LOCAL_PORT_2,
383                    dst_addr: I::TEST_ADDRS.remote_ip.get(),
384                    dst_port: LOCAL_PORT_1.get(),
385                },
386                cookie: socket.socket_cookie(),
387                marks: Marks::default(),
388            }]
389        );
390
391        results.clear();
392        let matcher = I::map_ip_in(
393            I::TEST_ADDRS.local_ip.get(),
394            |addr| {
395                BoundAddressMatcherEither::Bound(AddressMatcherEither::V4(AddressMatcher {
396                    matcher: AddressMatcherType::Subnet(SubnetMatcher(
397                        Subnet::new(addr, 32).unwrap(),
398                    )),
399                    invert: false,
400                }))
401            },
402            |addr| {
403                BoundAddressMatcherEither::Bound(AddressMatcherEither::V6(AddressMatcher {
404                    matcher: AddressMatcherType::Subnet(SubnetMatcher(
405                        Subnet::new(addr, 128).unwrap(),
406                    )),
407                    invert: false,
408                }))
409            },
410        );
411        api.bound_sockets_diagnostics(&IpSocketMatcher::DstAddr(matcher), &mut results);
412        assert_eq!(results, Vec::new());
413    }
414
415    #[ip_test(I)]
416    fn diagnostics_match_dst_addr_unbound<I: TestIpExt>() {
417        set_logger_for_test();
418
419        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
420        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
421
422        let socket1 = api.create();
423        api.listen(&socket1, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_1))
424            .expect("listen should succeed");
425        api.connect(
426            &socket1,
427            Some(ZonedAddr::Unzoned(I::TEST_ADDRS.remote_ip)),
428            UdpRemotePort::Set(REMOTE_PORT_1),
429        )
430        .expect("connect should succeed");
431
432        let socket2 = api.create();
433        api.listen(&socket2, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_2))
434            .expect("listen should succeed");
435
436        let mut results = Vec::new();
437        let matcher = match I::VERSION {
438            net_types::ip::IpVersion::V4 => BoundAddressMatcherEither::Unbound,
439            net_types::ip::IpVersion::V6 => BoundAddressMatcherEither::Unbound,
440        };
441        api.bound_sockets_diagnostics(&IpSocketMatcher::DstAddr(matcher), &mut results);
442        assert_eq!(
443            results,
444            vec![UdpSocketDiagnostics {
445                state: UdpSocketDiagnosticTuple::Bound {
446                    src_addr: Some(I::TEST_ADDRS.local_ip.get()),
447                    src_port: LOCAL_PORT_2
448                },
449                cookie: socket2.socket_cookie(),
450                marks: Marks::default(),
451            }]
452        );
453
454        results.clear();
455        let matcher = I::map_ip_in(
456            I::TEST_ADDRS.remote_ip.get(),
457            |addr| {
458                BoundAddressMatcherEither::Bound(AddressMatcherEither::V4(AddressMatcher {
459                    matcher: AddressMatcherType::Subnet(SubnetMatcher(
460                        Subnet::new(addr, 32).unwrap(),
461                    )),
462                    invert: false,
463                }))
464            },
465            |addr| {
466                BoundAddressMatcherEither::Bound(AddressMatcherEither::V6(AddressMatcher {
467                    matcher: AddressMatcherType::Subnet(SubnetMatcher(
468                        Subnet::new(addr, 128).unwrap(),
469                    )),
470                    invert: false,
471                }))
472            },
473        );
474        api.bound_sockets_diagnostics(&IpSocketMatcher::DstAddr(matcher), &mut results);
475        assert_eq!(
476            results,
477            vec![UdpSocketDiagnostics {
478                state: UdpSocketDiagnosticTuple::Connected {
479                    src_addr: I::TEST_ADDRS.local_ip.get(),
480                    src_port: LOCAL_PORT_1,
481                    dst_addr: I::TEST_ADDRS.remote_ip.get(),
482                    dst_port: REMOTE_PORT_1.get(),
483                },
484                cookie: socket1.socket_cookie(),
485                marks: Marks::default(),
486            }]
487        );
488    }
489
490    #[ip_test(I)]
491    fn diagnostics_match_proto<I: TestIpExt>() {
492        set_logger_for_test();
493
494        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
495        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
496
497        let socket = api.create();
498        api.listen(&socket, None, Some(LOCAL_PORT_1)).expect("listen should succeed");
499
500        let mut results = Vec::new();
501        api.bound_sockets_diagnostics(
502            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(UdpSocketMatcher::Empty)),
503            &mut results,
504        );
505        assert_eq!(
506            results,
507            vec![UdpSocketDiagnostics {
508                state: UdpSocketDiagnosticTuple::Bound { src_addr: None, src_port: LOCAL_PORT_1 },
509                cookie: socket.socket_cookie(),
510                marks: Marks::default(),
511            }]
512        );
513
514        results.clear();
515        api.bound_sockets_diagnostics(
516            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Tcp(TcpSocketMatcher::Empty)),
517            &mut results,
518        );
519        assert_eq!(results, Vec::new());
520    }
521
522    #[ip_test(I)]
523    fn diagnostics_match_src_port<I: TestIpExt>() {
524        set_logger_for_test();
525
526        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
527        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
528
529        let socket = api.create();
530        api.listen(&socket, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_1))
531            .expect("listen should succeed");
532
533        let mut results = Vec::new();
534        api.bound_sockets_diagnostics(
535            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(
536                UdpSocketMatcher::SrcPort(BoundPortMatcher::Bound(PortMatcher {
537                    range: LOCAL_PORT_1.get()..=LOCAL_PORT_1.get(),
538                    invert: false,
539                })),
540            )),
541            &mut results,
542        );
543        assert_eq!(
544            results,
545            vec![UdpSocketDiagnostics {
546                state: UdpSocketDiagnosticTuple::Bound {
547                    src_addr: Some(I::TEST_ADDRS.local_ip.get()),
548                    src_port: LOCAL_PORT_1
549                },
550                cookie: socket.socket_cookie(),
551                marks: Marks::default(),
552            }]
553        );
554
555        results.clear();
556        api.bound_sockets_diagnostics(
557            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(
558                UdpSocketMatcher::SrcPort(BoundPortMatcher::Bound(PortMatcher {
559                    range: (LOCAL_PORT_1.get() + 1)..=(LOCAL_PORT_1.get() + 1),
560                    invert: false,
561                })),
562            )),
563            &mut results,
564        );
565        assert_eq!(results, Vec::new());
566    }
567
568    #[ip_test(I)]
569    fn diagnostics_match_dst_port<I: TestIpExt>() {
570        set_logger_for_test();
571
572        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
573        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
574
575        let socket = api.create();
576        api.listen(&socket, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_2))
577            .expect("listen should succeed");
578        api.connect(
579            &socket,
580            Some(ZonedAddr::Unzoned(I::TEST_ADDRS.remote_ip)),
581            UdpRemotePort::Set(LOCAL_PORT_1),
582        )
583        .expect("connect should succeed");
584
585        let mut results = Vec::new();
586        api.bound_sockets_diagnostics(
587            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(
588                UdpSocketMatcher::DstPort(BoundPortMatcher::Bound(PortMatcher {
589                    range: LOCAL_PORT_1.get()..=LOCAL_PORT_1.get(),
590                    invert: false,
591                })),
592            )),
593            &mut results,
594        );
595        assert_eq!(
596            results,
597            vec![UdpSocketDiagnostics {
598                state: UdpSocketDiagnosticTuple::Connected {
599                    src_addr: I::TEST_ADDRS.local_ip.get(),
600                    src_port: LOCAL_PORT_2,
601                    dst_addr: I::TEST_ADDRS.remote_ip.get(),
602                    dst_port: LOCAL_PORT_1.get(),
603                },
604                cookie: socket.socket_cookie(),
605                marks: Marks::default(),
606            }]
607        );
608
609        results.clear();
610        api.bound_sockets_diagnostics(
611            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(
612                UdpSocketMatcher::DstPort(BoundPortMatcher::Bound(PortMatcher {
613                    range: (LOCAL_PORT_1.get() + 1)..=(LOCAL_PORT_1.get() + 1),
614                    invert: false,
615                })),
616            )),
617            &mut results,
618        );
619        assert_eq!(results, Vec::new());
620    }
621
622    #[ip_test(I)]
623    fn diagnostics_match_src_port_unbound<I: TestIpExt>() {
624        set_logger_for_test();
625
626        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
627        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
628
629        let socket = api.create();
630        api.listen(&socket, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_1))
631            .expect("listen should succeed");
632
633        let mut results = Vec::new();
634        // Source port is always present (bound) for sockets visible in diagnostics.
635        api.bound_sockets_diagnostics(
636            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(
637                UdpSocketMatcher::SrcPort(BoundPortMatcher::Unbound),
638            )),
639            &mut results,
640        );
641        assert_eq!(results, Vec::new());
642    }
643
644    #[ip_test(I)]
645    fn diagnostics_match_dst_port_unbound<I: TestIpExt>() {
646        set_logger_for_test();
647
648        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
649        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
650
651        // Unconnected socket (no destination port).
652        let socket1 = api.create();
653        api.listen(&socket1, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_1))
654            .expect("listen should succeed");
655
656        // Connected socket (has destination port).
657        let socket2 = api.create();
658        api.listen(&socket2, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_2))
659            .expect("listen should succeed");
660        api.connect(
661            &socket2,
662            Some(ZonedAddr::Unzoned(I::TEST_ADDRS.remote_ip)),
663            UdpRemotePort::Set(REMOTE_PORT_2),
664        )
665        .expect("connect should succeed");
666
667        let mut results = Vec::new();
668        api.bound_sockets_diagnostics(
669            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(
670                UdpSocketMatcher::DstPort(BoundPortMatcher::Unbound),
671            )),
672            &mut results,
673        );
674        assert_eq!(
675            results,
676            vec![UdpSocketDiagnostics {
677                state: UdpSocketDiagnosticTuple::Bound {
678                    src_addr: Some(I::TEST_ADDRS.local_ip.get()),
679                    src_port: LOCAL_PORT_1
680                },
681                cookie: socket1.socket_cookie(),
682                marks: Marks::default(),
683            }]
684        );
685
686        results.clear();
687        api.bound_sockets_diagnostics(
688            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(
689                UdpSocketMatcher::DstPort(BoundPortMatcher::Unbound),
690            )),
691            &mut results,
692        );
693        assert_eq!(
694            results,
695            vec![UdpSocketDiagnostics {
696                state: UdpSocketDiagnosticTuple::Bound {
697                    src_addr: Some(I::TEST_ADDRS.local_ip.get()),
698                    src_port: LOCAL_PORT_1,
699                },
700                cookie: socket1.socket_cookie(),
701                marks: Marks::default(),
702            }]
703        );
704    }
705
706    #[ip_test(I)]
707    fn diagnostics_match_state<I: TestIpExt>() {
708        set_logger_for_test();
709
710        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
711        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
712
713        let socket_1 = api.create();
714        api.listen(&socket_1, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_1))
715            .expect("listen should succeed");
716
717        let socket_2 = api.create();
718        api.listen(&socket_2, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_2))
719            .expect("listen should succeed");
720        api.connect(
721            &socket_2,
722            Some(ZonedAddr::Unzoned(I::TEST_ADDRS.remote_ip)),
723            UdpRemotePort::Set(LOCAL_PORT_3),
724        )
725        .expect("connect should succeed");
726
727        let mut results = Vec::new();
728        api.bound_sockets_diagnostics(
729            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(UdpSocketMatcher::State(
730                UdpStateMatcher::BOUND,
731            ))),
732            &mut results,
733        );
734        assert_eq!(
735            results,
736            vec![UdpSocketDiagnostics {
737                state: UdpSocketDiagnosticTuple::Bound {
738                    src_addr: Some(I::TEST_ADDRS.local_ip.get()),
739                    src_port: LOCAL_PORT_1
740                },
741                cookie: socket_1.socket_cookie(),
742                marks: Marks::default(),
743            }]
744        );
745
746        results.clear();
747        api.bound_sockets_diagnostics(
748            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(UdpSocketMatcher::State(
749                UdpStateMatcher::CONNECTED,
750            ))),
751            &mut results,
752        );
753        assert_eq!(
754            results,
755            vec![UdpSocketDiagnostics {
756                state: UdpSocketDiagnosticTuple::Connected {
757                    src_addr: I::TEST_ADDRS.local_ip.get(),
758                    src_port: LOCAL_PORT_2,
759                    dst_addr: I::TEST_ADDRS.remote_ip.get(),
760                    dst_port: LOCAL_PORT_3.get(),
761                },
762                cookie: socket_2.socket_cookie(),
763                marks: Marks::default(),
764            }]
765        );
766    }
767
768    #[ip_test(I)]
769    fn diagnostics_match_device<I: TestIpExt>() {
770        set_logger_for_test();
771
772        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
773        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
774
775        let socket = api.create();
776        api.set_device(&socket, Some(&FakeDeviceId)).expect("set device should succeed");
777        api.listen(&socket, None, Some(LOCAL_PORT_1)).expect("listen should succeed");
778
779        let mut results = Vec::new();
780        api.bound_sockets_diagnostics(
781            &IpSocketMatcher::BoundInterface(BoundInterfaceMatcher::Bound(InterfaceMatcher::Name(
782                FakeDeviceId::FAKE_NAME.to_string(),
783            ))),
784            &mut results,
785        );
786        assert_eq!(
787            results,
788            vec![UdpSocketDiagnostics {
789                state: UdpSocketDiagnosticTuple::Bound { src_addr: None, src_port: LOCAL_PORT_1 },
790                cookie: socket.socket_cookie(),
791                marks: Marks::default(),
792            }]
793        );
794
795        results.clear();
796        api.bound_sockets_diagnostics(
797            &IpSocketMatcher::BoundInterface(BoundInterfaceMatcher::Unbound),
798            &mut results,
799        );
800        assert_eq!(results, Vec::new());
801    }
802
803    #[ip_test(I)]
804    fn diagnostics_match_cookie<I: TestIpExt>() {
805        set_logger_for_test();
806
807        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
808        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
809
810        let socket = api.create();
811        api.listen(&socket, None, Some(LOCAL_PORT_1)).expect("listen should succeed");
812
813        let mut results = Vec::new();
814        api.bound_sockets_diagnostics(
815            &IpSocketMatcher::Cookie(SocketCookieMatcher {
816                cookie: socket.socket_cookie().export_value(),
817                invert: false,
818            }),
819            &mut results,
820        );
821        assert_eq!(
822            results,
823            vec![UdpSocketDiagnostics {
824                state: UdpSocketDiagnosticTuple::Bound { src_addr: None, src_port: LOCAL_PORT_1 },
825                cookie: socket.socket_cookie(),
826                marks: Marks::default(),
827            }]
828        );
829
830        results.clear();
831        api.bound_sockets_diagnostics(
832            &IpSocketMatcher::Cookie(SocketCookieMatcher {
833                cookie: socket.socket_cookie().export_value() + 1,
834                invert: false,
835            }),
836            &mut results,
837        );
838        assert_eq!(results, Vec::new());
839    }
840
841    #[ip_test(I)]
842    #[test_case::test_case(MarkDomain::Mark1; "mark_1")]
843    #[test_case::test_case(MarkDomain::Mark2; "mark_2")]
844    fn diagnostics_match_mark<I: TestIpExt>(domain: MarkDomain) {
845        set_logger_for_test();
846
847        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
848        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
849
850        let socket = api.create();
851        api.listen(&socket, None, Some(LOCAL_PORT_1)).expect("listen should succeed");
852
853        api.set_mark(&socket, domain, Mark(Some(MARK)));
854
855        let mut results = Vec::new();
856        let matcher = |query_mark| {
857            IpSocketMatcher::Mark(netstack3_base::MarkInDomainMatcher {
858                domain,
859                matcher: MarkMatcher::Marked {
860                    mask: MARK_MASK,
861                    start: query_mark,
862                    end: query_mark,
863                    invert: false,
864                },
865            })
866        };
867        api.bound_sockets_diagnostics(&matcher(MARK), &mut results);
868        assert_eq!(
869            results,
870            vec![UdpSocketDiagnostics {
871                state: UdpSocketDiagnosticTuple::Bound { src_addr: None, src_port: LOCAL_PORT_1 },
872                cookie: socket.socket_cookie(),
873                marks: netstack3_base::MarkStorage::new([(domain, MARK)]),
874            }]
875        );
876
877        results.clear();
878        api.bound_sockets_diagnostics(&matcher(MARK + 1), &mut results);
879        assert_eq!(results, Vec::new());
880    }
881
882    /// Create three sockets, two of which target the same remote port, and make
883    /// sure that multiple matching sockets are returned.
884    #[ip_test(I)]
885    fn diagnostics_match_multiple<I: TestIpExt>() {
886        set_logger_for_test();
887
888        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
889        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
890
891        let socket_1 = api.create();
892        api.listen(&socket_1, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_1))
893            .expect("listen should succeed");
894        api.connect(
895            &socket_1,
896            Some(ZonedAddr::Unzoned(I::TEST_ADDRS.remote_ip)),
897            UdpRemotePort::Set(REMOTE_PORT_1),
898        )
899        .expect("connect should succeed");
900
901        let socket_2 = api.create();
902        api.listen(&socket_2, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_2))
903            .expect("listen should succeed");
904        api.connect(
905            &socket_2,
906            Some(ZonedAddr::Unzoned(I::TEST_ADDRS.remote_ip)),
907            UdpRemotePort::Set(REMOTE_PORT_1),
908        )
909        .expect("connect should succeed");
910
911        let socket_3 = api.create();
912        api.listen(&socket_3, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_3))
913            .expect("listen should succeed");
914        api.connect(
915            &socket_3,
916            Some(ZonedAddr::Unzoned(I::TEST_ADDRS.remote_ip)),
917            UdpRemotePort::Set(REMOTE_PORT_2),
918        )
919        .expect("connect should succeed");
920
921        let mut results = Vec::new();
922        api.bound_sockets_diagnostics(
923            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(
924                UdpSocketMatcher::DstPort(BoundPortMatcher::Bound(PortMatcher {
925                    range: REMOTE_PORT_1.get()..=REMOTE_PORT_1.get(),
926                    invert: false,
927                })),
928            )),
929            &mut results,
930        );
931
932        results.sort_by(|a, b| a.cookie.cmp(&b.cookie));
933        let mut expected = vec![
934            UdpSocketDiagnostics {
935                state: UdpSocketDiagnosticTuple::Connected {
936                    src_addr: I::TEST_ADDRS.local_ip.get(),
937                    src_port: LOCAL_PORT_1,
938                    dst_addr: I::TEST_ADDRS.remote_ip.get(),
939                    dst_port: REMOTE_PORT_1.get(),
940                },
941                cookie: socket_1.socket_cookie(),
942                marks: Marks::default(),
943            },
944            UdpSocketDiagnostics {
945                state: UdpSocketDiagnosticTuple::Connected {
946                    src_addr: I::TEST_ADDRS.local_ip.get(),
947                    src_port: LOCAL_PORT_2,
948                    dst_addr: I::TEST_ADDRS.remote_ip.get(),
949                    dst_port: REMOTE_PORT_1.get(),
950                },
951                cookie: socket_2.socket_cookie(),
952                marks: Marks::default(),
953            },
954        ];
955        expected.sort_by(|a, b| a.cookie.cmp(&b.cookie));
956        assert_eq!(results, expected);
957
958        results.clear();
959        api.bound_sockets_diagnostics(
960            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(
961                UdpSocketMatcher::DstPort(BoundPortMatcher::Bound(PortMatcher {
962                    range: REMOTE_PORT_2.get()..=REMOTE_PORT_2.get(),
963                    invert: false,
964                })),
965            )),
966            &mut results,
967        );
968        assert_eq!(
969            results,
970            vec![UdpSocketDiagnostics {
971                state: UdpSocketDiagnosticTuple::Connected {
972                    src_addr: I::TEST_ADDRS.local_ip.get(),
973                    src_port: LOCAL_PORT_3,
974                    dst_addr: I::TEST_ADDRS.remote_ip.get(),
975                    dst_port: REMOTE_PORT_2.get(),
976                },
977                cookie: socket_3.socket_cookie(),
978                marks: Marks::default(),
979            }]
980        );
981    }
982}