Skip to main content

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 assert_matches::assert_matches;
162    use core::num::NonZeroU16;
163
164    use ip_test_macro::ip_test;
165    use net_types::ip::Subnet;
166    use net_types::{Witness as _, ZonedAddr};
167    use netstack3_base::testutil::{FakeDeviceId, set_logger_for_test};
168    use netstack3_base::{
169        AddressMatcher, AddressMatcherEither, AddressMatcherType, BoundAddressMatcherEither,
170        BoundInterfaceMatcher, BoundPortMatcher, InterfaceMatcher, IpSocketMatcher, Mark,
171        MarkDomain, MarkMatcher, PortMatcher, SocketCookieMatcher, SocketTransportProtocolMatcher,
172        SubnetMatcher, TcpSocketMatcher, UdpSocketMatcher, UdpStateMatcher,
173    };
174
175    use crate::internal::base::testutils::{FakeUdpCoreCtx, TestIpExt, UdpFakeDeviceCtx};
176    use crate::internal::base::{UdpApi, UdpRemotePort};
177    use netstack3_datagram::{ConnInfo, ListenerInfo};
178
179    use super::*;
180
181    const LOCAL_PORT_1: NonZeroU16 = NonZeroU16::new(1234).unwrap();
182    const LOCAL_PORT_2: NonZeroU16 = NonZeroU16::new(5678).unwrap();
183    const LOCAL_PORT_3: NonZeroU16 = NonZeroU16::new(4321).unwrap();
184
185    const REMOTE_PORT_1: NonZeroU16 = NonZeroU16::new(100).unwrap();
186    const REMOTE_PORT_2: NonZeroU16 = NonZeroU16::new(200).unwrap();
187
188    const MARK: u32 = 0x10;
189    const MARK_MASK: u32 = !0;
190
191    #[ip_test(I)]
192    fn diagnostics_match_ip_version<I: TestIpExt>() {
193        set_logger_for_test();
194
195        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
196        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
197
198        let socket = api.create();
199        api.listen(&socket, None, Some(LOCAL_PORT_1)).expect("listen should succeed");
200
201        let mut results = Vec::new();
202        api.bound_sockets_diagnostics(&IpSocketMatcher::Family(I::VERSION), &mut results);
203        assert_eq!(
204            results,
205            vec![UdpSocketDiagnostics {
206                state: UdpSocketDiagnosticTuple::Bound { src_addr: None, src_port: LOCAL_PORT_1 },
207                cookie: socket.socket_cookie(),
208                marks: Marks::default(),
209            }]
210        );
211
212        results.clear();
213        api.bound_sockets_diagnostics(
214            &IpSocketMatcher::Family(
215                <<I as netstack3_base::socket::DualStackIpExt>::OtherVersion as Ip>::VERSION,
216            ),
217            &mut results,
218        );
219        assert_eq!(results, Vec::new());
220    }
221
222    #[ip_test(I)]
223    fn diagnostics_match_src_addr<I: TestIpExt>() {
224        set_logger_for_test();
225
226        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
227        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
228
229        let socket = api.create();
230        api.listen(&socket, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_1))
231            .expect("listen should succeed");
232
233        let mut results = Vec::new();
234        let matcher = I::map_ip_in(
235            I::TEST_ADDRS.subnet,
236            |subnet| {
237                BoundAddressMatcherEither::Bound(AddressMatcherEither::V4(AddressMatcher {
238                    matcher: AddressMatcherType::Subnet(SubnetMatcher(subnet)),
239                    invert: false,
240                }))
241            },
242            |subnet| {
243                BoundAddressMatcherEither::Bound(AddressMatcherEither::V6(AddressMatcher {
244                    matcher: AddressMatcherType::Subnet(SubnetMatcher(subnet)),
245                    invert: false,
246                }))
247            },
248        );
249        api.bound_sockets_diagnostics(&IpSocketMatcher::SrcAddr(matcher), &mut results);
250        assert_eq!(
251            results,
252            vec![UdpSocketDiagnostics {
253                state: UdpSocketDiagnosticTuple::Bound {
254                    src_addr: Some(I::TEST_ADDRS.local_ip.get()),
255                    src_port: LOCAL_PORT_1
256                },
257                cookie: socket.socket_cookie(),
258                marks: Marks::default(),
259            }]
260        );
261
262        results.clear();
263        let matcher = I::map_ip_in(
264            I::TEST_ADDRS.remote_ip.get(),
265            |addr| {
266                BoundAddressMatcherEither::Bound(AddressMatcherEither::V4(AddressMatcher {
267                    matcher: AddressMatcherType::Subnet(SubnetMatcher(
268                        Subnet::new(addr, 32).unwrap(),
269                    )),
270                    invert: false,
271                }))
272            },
273            |addr| {
274                BoundAddressMatcherEither::Bound(AddressMatcherEither::V6(AddressMatcher {
275                    matcher: AddressMatcherType::Subnet(SubnetMatcher(
276                        Subnet::new(addr, 128).unwrap(),
277                    )),
278                    invert: false,
279                }))
280            },
281        );
282        api.bound_sockets_diagnostics(&IpSocketMatcher::SrcAddr(matcher), &mut results);
283        assert_eq!(results, Vec::new());
284    }
285
286    #[ip_test(I)]
287    fn diagnostics_match_src_addr_unbound<I: TestIpExt>() {
288        set_logger_for_test();
289
290        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
291        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
292
293        let socket1 = api.create();
294        api.listen(&socket1, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_1))
295            .expect("listen should succeed");
296
297        let socket2 = api.create();
298        api.listen(&socket2, None, Some(LOCAL_PORT_2)).expect("listen should succeed");
299
300        let mut results = Vec::new();
301        let matcher = match I::VERSION {
302            net_types::ip::IpVersion::V4 => BoundAddressMatcherEither::Unbound,
303            net_types::ip::IpVersion::V6 => BoundAddressMatcherEither::Unbound,
304        };
305        api.bound_sockets_diagnostics(&IpSocketMatcher::SrcAddr(matcher), &mut results);
306        assert_eq!(
307            results,
308            vec![UdpSocketDiagnostics {
309                state: UdpSocketDiagnosticTuple::Bound { src_addr: None, src_port: LOCAL_PORT_2 },
310                cookie: socket2.socket_cookie(),
311                marks: Marks::default(),
312            }]
313        );
314
315        results.clear();
316        let matcher = I::map_ip_in(
317            I::TEST_ADDRS.subnet,
318            |subnet| {
319                BoundAddressMatcherEither::Bound(AddressMatcherEither::V4(AddressMatcher {
320                    matcher: AddressMatcherType::Subnet(SubnetMatcher(subnet)),
321                    invert: false,
322                }))
323            },
324            |subnet| {
325                BoundAddressMatcherEither::Bound(AddressMatcherEither::V6(AddressMatcher {
326                    matcher: AddressMatcherType::Subnet(SubnetMatcher(subnet)),
327                    invert: false,
328                }))
329            },
330        );
331        api.bound_sockets_diagnostics(&IpSocketMatcher::SrcAddr(matcher), &mut results);
332        assert_eq!(
333            results,
334            vec![UdpSocketDiagnostics {
335                state: UdpSocketDiagnosticTuple::Bound {
336                    src_addr: Some(I::TEST_ADDRS.local_ip.get()),
337                    src_port: LOCAL_PORT_1
338                },
339                cookie: socket1.socket_cookie(),
340                marks: Marks::default(),
341            }]
342        );
343    }
344
345    #[ip_test(I)]
346    fn diagnostics_match_dst_addr<I: TestIpExt>() {
347        set_logger_for_test();
348
349        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
350        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
351
352        let socket = api.create();
353        api.listen(&socket, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_2))
354            .expect("listen should succeed");
355        api.connect(
356            &socket,
357            Some(ZonedAddr::Unzoned(I::TEST_ADDRS.remote_ip)),
358            UdpRemotePort::Set(LOCAL_PORT_1),
359        )
360        .expect("connect should succeed");
361
362        let mut results = Vec::new();
363        let matcher = I::map_ip_in(
364            I::TEST_ADDRS.subnet,
365            |subnet| {
366                BoundAddressMatcherEither::Bound(AddressMatcherEither::V4(AddressMatcher {
367                    matcher: AddressMatcherType::Subnet(SubnetMatcher(subnet)),
368                    invert: false,
369                }))
370            },
371            |subnet| {
372                BoundAddressMatcherEither::Bound(AddressMatcherEither::V6(AddressMatcher {
373                    matcher: AddressMatcherType::Subnet(SubnetMatcher(subnet)),
374                    invert: false,
375                }))
376            },
377        );
378        api.bound_sockets_diagnostics(&IpSocketMatcher::DstAddr(matcher), &mut results);
379        assert_eq!(
380            results,
381            vec![UdpSocketDiagnostics {
382                state: UdpSocketDiagnosticTuple::Connected {
383                    src_addr: I::TEST_ADDRS.local_ip.get(),
384                    src_port: LOCAL_PORT_2,
385                    dst_addr: I::TEST_ADDRS.remote_ip.get(),
386                    dst_port: LOCAL_PORT_1.get(),
387                },
388                cookie: socket.socket_cookie(),
389                marks: Marks::default(),
390            }]
391        );
392
393        results.clear();
394        let matcher = I::map_ip_in(
395            I::TEST_ADDRS.local_ip.get(),
396            |addr| {
397                BoundAddressMatcherEither::Bound(AddressMatcherEither::V4(AddressMatcher {
398                    matcher: AddressMatcherType::Subnet(SubnetMatcher(
399                        Subnet::new(addr, 32).unwrap(),
400                    )),
401                    invert: false,
402                }))
403            },
404            |addr| {
405                BoundAddressMatcherEither::Bound(AddressMatcherEither::V6(AddressMatcher {
406                    matcher: AddressMatcherType::Subnet(SubnetMatcher(
407                        Subnet::new(addr, 128).unwrap(),
408                    )),
409                    invert: false,
410                }))
411            },
412        );
413        api.bound_sockets_diagnostics(&IpSocketMatcher::DstAddr(matcher), &mut results);
414        assert_eq!(results, Vec::new());
415    }
416
417    #[ip_test(I)]
418    fn diagnostics_match_dst_addr_unbound<I: TestIpExt>() {
419        set_logger_for_test();
420
421        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
422        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
423
424        let socket1 = api.create();
425        api.listen(&socket1, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_1))
426            .expect("listen should succeed");
427        api.connect(
428            &socket1,
429            Some(ZonedAddr::Unzoned(I::TEST_ADDRS.remote_ip)),
430            UdpRemotePort::Set(REMOTE_PORT_1),
431        )
432        .expect("connect should succeed");
433
434        let socket2 = api.create();
435        api.listen(&socket2, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_2))
436            .expect("listen should succeed");
437
438        let mut results = Vec::new();
439        let matcher = match I::VERSION {
440            net_types::ip::IpVersion::V4 => BoundAddressMatcherEither::Unbound,
441            net_types::ip::IpVersion::V6 => BoundAddressMatcherEither::Unbound,
442        };
443        api.bound_sockets_diagnostics(&IpSocketMatcher::DstAddr(matcher), &mut results);
444        assert_eq!(
445            results,
446            vec![UdpSocketDiagnostics {
447                state: UdpSocketDiagnosticTuple::Bound {
448                    src_addr: Some(I::TEST_ADDRS.local_ip.get()),
449                    src_port: LOCAL_PORT_2
450                },
451                cookie: socket2.socket_cookie(),
452                marks: Marks::default(),
453            }]
454        );
455
456        results.clear();
457        let matcher = I::map_ip_in(
458            I::TEST_ADDRS.remote_ip.get(),
459            |addr| {
460                BoundAddressMatcherEither::Bound(AddressMatcherEither::V4(AddressMatcher {
461                    matcher: AddressMatcherType::Subnet(SubnetMatcher(
462                        Subnet::new(addr, 32).unwrap(),
463                    )),
464                    invert: false,
465                }))
466            },
467            |addr| {
468                BoundAddressMatcherEither::Bound(AddressMatcherEither::V6(AddressMatcher {
469                    matcher: AddressMatcherType::Subnet(SubnetMatcher(
470                        Subnet::new(addr, 128).unwrap(),
471                    )),
472                    invert: false,
473                }))
474            },
475        );
476        api.bound_sockets_diagnostics(&IpSocketMatcher::DstAddr(matcher), &mut results);
477        assert_eq!(
478            results,
479            vec![UdpSocketDiagnostics {
480                state: UdpSocketDiagnosticTuple::Connected {
481                    src_addr: I::TEST_ADDRS.local_ip.get(),
482                    src_port: LOCAL_PORT_1,
483                    dst_addr: I::TEST_ADDRS.remote_ip.get(),
484                    dst_port: REMOTE_PORT_1.get(),
485                },
486                cookie: socket1.socket_cookie(),
487                marks: Marks::default(),
488            }]
489        );
490    }
491
492    #[ip_test(I)]
493    fn diagnostics_match_proto<I: TestIpExt>() {
494        set_logger_for_test();
495
496        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
497        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
498
499        let socket = api.create();
500        api.listen(&socket, None, Some(LOCAL_PORT_1)).expect("listen should succeed");
501
502        let mut results = Vec::new();
503        api.bound_sockets_diagnostics(
504            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(UdpSocketMatcher::Empty)),
505            &mut results,
506        );
507        assert_eq!(
508            results,
509            vec![UdpSocketDiagnostics {
510                state: UdpSocketDiagnosticTuple::Bound { src_addr: None, src_port: LOCAL_PORT_1 },
511                cookie: socket.socket_cookie(),
512                marks: Marks::default(),
513            }]
514        );
515
516        results.clear();
517        api.bound_sockets_diagnostics(
518            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Tcp(TcpSocketMatcher::Empty)),
519            &mut results,
520        );
521        assert_eq!(results, Vec::new());
522    }
523
524    #[ip_test(I)]
525    fn diagnostics_match_src_port<I: TestIpExt>() {
526        set_logger_for_test();
527
528        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
529        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
530
531        let socket = api.create();
532        api.listen(&socket, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_1))
533            .expect("listen should succeed");
534
535        let mut results = Vec::new();
536        api.bound_sockets_diagnostics(
537            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(
538                UdpSocketMatcher::SrcPort(BoundPortMatcher::Bound(PortMatcher {
539                    range: LOCAL_PORT_1.get()..=LOCAL_PORT_1.get(),
540                    invert: false,
541                })),
542            )),
543            &mut results,
544        );
545        assert_eq!(
546            results,
547            vec![UdpSocketDiagnostics {
548                state: UdpSocketDiagnosticTuple::Bound {
549                    src_addr: Some(I::TEST_ADDRS.local_ip.get()),
550                    src_port: LOCAL_PORT_1
551                },
552                cookie: socket.socket_cookie(),
553                marks: Marks::default(),
554            }]
555        );
556
557        results.clear();
558        api.bound_sockets_diagnostics(
559            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(
560                UdpSocketMatcher::SrcPort(BoundPortMatcher::Bound(PortMatcher {
561                    range: (LOCAL_PORT_1.get() + 1)..=(LOCAL_PORT_1.get() + 1),
562                    invert: false,
563                })),
564            )),
565            &mut results,
566        );
567        assert_eq!(results, Vec::new());
568    }
569
570    #[ip_test(I)]
571    fn diagnostics_match_dst_port<I: TestIpExt>() {
572        set_logger_for_test();
573
574        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
575        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
576
577        let socket = api.create();
578        api.listen(&socket, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_2))
579            .expect("listen should succeed");
580        api.connect(
581            &socket,
582            Some(ZonedAddr::Unzoned(I::TEST_ADDRS.remote_ip)),
583            UdpRemotePort::Set(LOCAL_PORT_1),
584        )
585        .expect("connect should succeed");
586
587        let mut results = Vec::new();
588        api.bound_sockets_diagnostics(
589            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(
590                UdpSocketMatcher::DstPort(BoundPortMatcher::Bound(PortMatcher {
591                    range: LOCAL_PORT_1.get()..=LOCAL_PORT_1.get(),
592                    invert: false,
593                })),
594            )),
595            &mut results,
596        );
597        assert_eq!(
598            results,
599            vec![UdpSocketDiagnostics {
600                state: UdpSocketDiagnosticTuple::Connected {
601                    src_addr: I::TEST_ADDRS.local_ip.get(),
602                    src_port: LOCAL_PORT_2,
603                    dst_addr: I::TEST_ADDRS.remote_ip.get(),
604                    dst_port: LOCAL_PORT_1.get(),
605                },
606                cookie: socket.socket_cookie(),
607                marks: Marks::default(),
608            }]
609        );
610
611        results.clear();
612        api.bound_sockets_diagnostics(
613            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(
614                UdpSocketMatcher::DstPort(BoundPortMatcher::Bound(PortMatcher {
615                    range: (LOCAL_PORT_1.get() + 1)..=(LOCAL_PORT_1.get() + 1),
616                    invert: false,
617                })),
618            )),
619            &mut results,
620        );
621        assert_eq!(results, Vec::new());
622    }
623
624    #[ip_test(I)]
625    fn diagnostics_match_src_port_unbound<I: TestIpExt>() {
626        set_logger_for_test();
627
628        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
629        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
630
631        let socket = api.create();
632        api.listen(&socket, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_1))
633            .expect("listen should succeed");
634
635        let mut results = Vec::new();
636        // Source port is always present (bound) for sockets visible in diagnostics.
637        api.bound_sockets_diagnostics(
638            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(
639                UdpSocketMatcher::SrcPort(BoundPortMatcher::Unbound),
640            )),
641            &mut results,
642        );
643        assert_eq!(results, Vec::new());
644    }
645
646    #[ip_test(I)]
647    fn diagnostics_match_dst_port_unbound<I: TestIpExt>() {
648        set_logger_for_test();
649
650        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
651        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
652
653        // Unconnected socket (no destination port).
654        let socket1 = api.create();
655        api.listen(&socket1, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_1))
656            .expect("listen should succeed");
657
658        // Connected socket (has destination port).
659        let socket2 = api.create();
660        api.listen(&socket2, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_2))
661            .expect("listen should succeed");
662        api.connect(
663            &socket2,
664            Some(ZonedAddr::Unzoned(I::TEST_ADDRS.remote_ip)),
665            UdpRemotePort::Set(REMOTE_PORT_2),
666        )
667        .expect("connect should succeed");
668
669        let mut results = Vec::new();
670        api.bound_sockets_diagnostics(
671            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(
672                UdpSocketMatcher::DstPort(BoundPortMatcher::Unbound),
673            )),
674            &mut results,
675        );
676        assert_eq!(
677            results,
678            vec![UdpSocketDiagnostics {
679                state: UdpSocketDiagnosticTuple::Bound {
680                    src_addr: Some(I::TEST_ADDRS.local_ip.get()),
681                    src_port: LOCAL_PORT_1
682                },
683                cookie: socket1.socket_cookie(),
684                marks: Marks::default(),
685            }]
686        );
687
688        results.clear();
689        api.bound_sockets_diagnostics(
690            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(
691                UdpSocketMatcher::DstPort(BoundPortMatcher::Unbound),
692            )),
693            &mut results,
694        );
695        assert_eq!(
696            results,
697            vec![UdpSocketDiagnostics {
698                state: UdpSocketDiagnosticTuple::Bound {
699                    src_addr: Some(I::TEST_ADDRS.local_ip.get()),
700                    src_port: LOCAL_PORT_1,
701                },
702                cookie: socket1.socket_cookie(),
703                marks: Marks::default(),
704            }]
705        );
706    }
707
708    #[ip_test(I)]
709    fn diagnostics_match_state<I: TestIpExt>() {
710        set_logger_for_test();
711
712        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
713        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
714
715        let socket_1 = api.create();
716        api.listen(&socket_1, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_1))
717            .expect("listen should succeed");
718
719        let socket_2 = api.create();
720        api.listen(&socket_2, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_2))
721            .expect("listen should succeed");
722        api.connect(
723            &socket_2,
724            Some(ZonedAddr::Unzoned(I::TEST_ADDRS.remote_ip)),
725            UdpRemotePort::Set(LOCAL_PORT_3),
726        )
727        .expect("connect should succeed");
728
729        let mut results = Vec::new();
730        api.bound_sockets_diagnostics(
731            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(UdpSocketMatcher::State(
732                UdpStateMatcher::BOUND,
733            ))),
734            &mut results,
735        );
736        assert_eq!(
737            results,
738            vec![UdpSocketDiagnostics {
739                state: UdpSocketDiagnosticTuple::Bound {
740                    src_addr: Some(I::TEST_ADDRS.local_ip.get()),
741                    src_port: LOCAL_PORT_1
742                },
743                cookie: socket_1.socket_cookie(),
744                marks: Marks::default(),
745            }]
746        );
747
748        results.clear();
749        api.bound_sockets_diagnostics(
750            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(UdpSocketMatcher::State(
751                UdpStateMatcher::CONNECTED,
752            ))),
753            &mut results,
754        );
755        assert_eq!(
756            results,
757            vec![UdpSocketDiagnostics {
758                state: UdpSocketDiagnosticTuple::Connected {
759                    src_addr: I::TEST_ADDRS.local_ip.get(),
760                    src_port: LOCAL_PORT_2,
761                    dst_addr: I::TEST_ADDRS.remote_ip.get(),
762                    dst_port: LOCAL_PORT_3.get(),
763                },
764                cookie: socket_2.socket_cookie(),
765                marks: Marks::default(),
766            }]
767        );
768    }
769
770    #[ip_test(I)]
771    fn diagnostics_match_device<I: TestIpExt>() {
772        set_logger_for_test();
773
774        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
775        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
776
777        let socket = api.create();
778        api.set_device(&socket, Some(&FakeDeviceId)).expect("set device should succeed");
779        api.listen(&socket, None, Some(LOCAL_PORT_1)).expect("listen should succeed");
780
781        let mut results = Vec::new();
782        api.bound_sockets_diagnostics(
783            &IpSocketMatcher::BoundInterface(BoundInterfaceMatcher::Bound(InterfaceMatcher::Name(
784                FakeDeviceId::FAKE_NAME.to_string(),
785            ))),
786            &mut results,
787        );
788        assert_eq!(
789            results,
790            vec![UdpSocketDiagnostics {
791                state: UdpSocketDiagnosticTuple::Bound { src_addr: None, src_port: LOCAL_PORT_1 },
792                cookie: socket.socket_cookie(),
793                marks: Marks::default(),
794            }]
795        );
796
797        results.clear();
798        api.bound_sockets_diagnostics(
799            &IpSocketMatcher::BoundInterface(BoundInterfaceMatcher::Unbound),
800            &mut results,
801        );
802        assert_eq!(results, Vec::new());
803    }
804
805    #[ip_test(I)]
806    fn diagnostics_match_cookie<I: TestIpExt>() {
807        set_logger_for_test();
808
809        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
810        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
811
812        let socket = api.create();
813        api.listen(&socket, None, Some(LOCAL_PORT_1)).expect("listen should succeed");
814
815        let mut results = Vec::new();
816        api.bound_sockets_diagnostics(
817            &IpSocketMatcher::Cookie(SocketCookieMatcher {
818                cookie: socket.socket_cookie().export_value(),
819                invert: false,
820            }),
821            &mut results,
822        );
823        assert_eq!(
824            results,
825            vec![UdpSocketDiagnostics {
826                state: UdpSocketDiagnosticTuple::Bound { src_addr: None, src_port: LOCAL_PORT_1 },
827                cookie: socket.socket_cookie(),
828                marks: Marks::default(),
829            }]
830        );
831
832        results.clear();
833        api.bound_sockets_diagnostics(
834            &IpSocketMatcher::Cookie(SocketCookieMatcher {
835                cookie: socket.socket_cookie().export_value() + 1,
836                invert: false,
837            }),
838            &mut results,
839        );
840        assert_eq!(results, Vec::new());
841    }
842
843    #[ip_test(I, test = false)]
844    #[test_case::test_case(MarkDomain::Mark1; "mark_1")]
845    #[test_case::test_case(MarkDomain::Mark2; "mark_2")]
846    fn diagnostics_match_mark<I: TestIpExt>(domain: MarkDomain) {
847        set_logger_for_test();
848
849        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
850        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
851
852        let socket = api.create();
853        api.listen(&socket, None, Some(LOCAL_PORT_1)).expect("listen should succeed");
854
855        api.set_mark(&socket, domain, Mark(Some(MARK)));
856
857        let mut results = Vec::new();
858        let matcher = |query_mark| {
859            IpSocketMatcher::Mark(netstack3_base::MarkInDomainMatcher {
860                domain,
861                matcher: MarkMatcher::Marked {
862                    mask: MARK_MASK,
863                    start: query_mark,
864                    end: query_mark,
865                    invert: false,
866                },
867            })
868        };
869        api.bound_sockets_diagnostics(&matcher(MARK), &mut results);
870        assert_eq!(
871            results,
872            vec![UdpSocketDiagnostics {
873                state: UdpSocketDiagnosticTuple::Bound { src_addr: None, src_port: LOCAL_PORT_1 },
874                cookie: socket.socket_cookie(),
875                marks: netstack3_base::MarkStorage::new([(domain, MARK)]),
876            }]
877        );
878
879        results.clear();
880        api.bound_sockets_diagnostics(&matcher(MARK + 1), &mut results);
881        assert_eq!(results, Vec::new());
882    }
883
884    /// Create three sockets, two of which target the same remote port, and make
885    /// sure that multiple matching sockets are returned.
886    #[ip_test(I)]
887    fn diagnostics_match_multiple<I: TestIpExt>() {
888        set_logger_for_test();
889
890        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
891        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
892
893        let socket_1 = api.create();
894        api.listen(&socket_1, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_1))
895            .expect("listen should succeed");
896        api.connect(
897            &socket_1,
898            Some(ZonedAddr::Unzoned(I::TEST_ADDRS.remote_ip)),
899            UdpRemotePort::Set(REMOTE_PORT_1),
900        )
901        .expect("connect should succeed");
902
903        let socket_2 = api.create();
904        api.listen(&socket_2, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_2))
905            .expect("listen should succeed");
906        api.connect(
907            &socket_2,
908            Some(ZonedAddr::Unzoned(I::TEST_ADDRS.remote_ip)),
909            UdpRemotePort::Set(REMOTE_PORT_1),
910        )
911        .expect("connect should succeed");
912
913        let socket_3 = api.create();
914        api.listen(&socket_3, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_3))
915            .expect("listen should succeed");
916        api.connect(
917            &socket_3,
918            Some(ZonedAddr::Unzoned(I::TEST_ADDRS.remote_ip)),
919            UdpRemotePort::Set(REMOTE_PORT_2),
920        )
921        .expect("connect should succeed");
922
923        let mut results = Vec::new();
924        api.bound_sockets_diagnostics(
925            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(
926                UdpSocketMatcher::DstPort(BoundPortMatcher::Bound(PortMatcher {
927                    range: REMOTE_PORT_1.get()..=REMOTE_PORT_1.get(),
928                    invert: false,
929                })),
930            )),
931            &mut results,
932        );
933
934        results.sort_by(|a, b| a.cookie.cmp(&b.cookie));
935        let mut expected = vec![
936            UdpSocketDiagnostics {
937                state: UdpSocketDiagnosticTuple::Connected {
938                    src_addr: I::TEST_ADDRS.local_ip.get(),
939                    src_port: LOCAL_PORT_1,
940                    dst_addr: I::TEST_ADDRS.remote_ip.get(),
941                    dst_port: REMOTE_PORT_1.get(),
942                },
943                cookie: socket_1.socket_cookie(),
944                marks: Marks::default(),
945            },
946            UdpSocketDiagnostics {
947                state: UdpSocketDiagnosticTuple::Connected {
948                    src_addr: I::TEST_ADDRS.local_ip.get(),
949                    src_port: LOCAL_PORT_2,
950                    dst_addr: I::TEST_ADDRS.remote_ip.get(),
951                    dst_port: REMOTE_PORT_1.get(),
952                },
953                cookie: socket_2.socket_cookie(),
954                marks: Marks::default(),
955            },
956        ];
957        expected.sort_by(|a, b| a.cookie.cmp(&b.cookie));
958        assert_eq!(results, expected);
959
960        results.clear();
961        api.bound_sockets_diagnostics(
962            &IpSocketMatcher::Proto(SocketTransportProtocolMatcher::Udp(
963                UdpSocketMatcher::DstPort(BoundPortMatcher::Bound(PortMatcher {
964                    range: REMOTE_PORT_2.get()..=REMOTE_PORT_2.get(),
965                    invert: false,
966                })),
967            )),
968            &mut results,
969        );
970        assert_eq!(
971            results,
972            vec![UdpSocketDiagnostics {
973                state: UdpSocketDiagnosticTuple::Connected {
974                    src_addr: I::TEST_ADDRS.local_ip.get(),
975                    src_port: LOCAL_PORT_3,
976                    dst_addr: I::TEST_ADDRS.remote_ip.get(),
977                    dst_port: REMOTE_PORT_2.get(),
978                },
979                cookie: socket_3.socket_cookie(),
980                marks: Marks::default(),
981            }]
982        );
983    }
984
985    #[ip_test(I)]
986    fn disconnect_listener<I: TestIpExt>() {
987        set_logger_for_test();
988
989        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::new_fake_device::<I>());
990        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
991        let socket = api.create();
992        api.listen(&socket, None, Some(LOCAL_PORT_1)).unwrap();
993        assert_matches!(api.get_info(&socket), SocketInfo::Listener(ListenerInfo { .. }));
994
995        let count = api.disconnect_bound(&IpSocketMatcher::Cookie(SocketCookieMatcher {
996            cookie: socket.socket_cookie().export_value(),
997            invert: false,
998        }));
999        assert_eq!(count, 1);
1000        assert_matches!(api.get_info(&socket), SocketInfo::Unbound);
1001    }
1002
1003    #[ip_test(I)]
1004    fn disconnect_implicitly_bound<I: TestIpExt>() {
1005        set_logger_for_test();
1006
1007        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::with_local_remote_ip_addrs(
1008            vec![I::TEST_ADDRS.local_ip],
1009            vec![I::TEST_ADDRS.remote_ip],
1010        ));
1011        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
1012        let socket = api.create();
1013
1014        api.send_to(
1015            &socket,
1016            Some(ZonedAddr::Unzoned(I::TEST_ADDRS.remote_ip)),
1017            REMOTE_PORT_1.into(),
1018            packet::Buf::new(vec![], ..),
1019        )
1020        .expect("send failed");
1021        assert_matches!(api.get_info(&socket), SocketInfo::Listener(ListenerInfo { .. }));
1022
1023        let count = api.disconnect_bound(&IpSocketMatcher::Cookie(SocketCookieMatcher {
1024            cookie: socket.socket_cookie().export_value(),
1025            invert: false,
1026        }));
1027        assert_eq!(count, 1);
1028        assert_matches!(api.get_info(&socket), SocketInfo::Unbound);
1029    }
1030
1031    #[ip_test(I)]
1032    fn disconnect_connected_and_reuse<I: TestIpExt>() {
1033        set_logger_for_test();
1034
1035        let mut ctx = UdpFakeDeviceCtx::with_core_ctx(FakeUdpCoreCtx::with_local_remote_ip_addrs(
1036            vec![I::TEST_ADDRS.local_ip],
1037            vec![I::TEST_ADDRS.remote_ip],
1038        ));
1039        let mut api = UdpApi::<I, _>::new(ctx.as_mut());
1040        let socket = api.create();
1041
1042        api.listen(&socket, Some(ZonedAddr::Unzoned(I::TEST_ADDRS.local_ip)), Some(LOCAL_PORT_1))
1043            .unwrap();
1044        api.connect(
1045            &socket,
1046            Some(ZonedAddr::Unzoned(I::TEST_ADDRS.remote_ip)),
1047            REMOTE_PORT_1.into(),
1048        )
1049        .unwrap();
1050
1051        assert_matches!(api.get_info(&socket), SocketInfo::Connected(ConnInfo { .. }));
1052
1053        let count = api.disconnect_bound(&IpSocketMatcher::Cookie(SocketCookieMatcher {
1054            cookie: socket.socket_cookie().export_value(),
1055            invert: false,
1056        }));
1057        assert_eq!(count, 1);
1058        assert_matches!(api.get_info(&socket), SocketInfo::Unbound);
1059
1060        // Unlike TCP sockets, a UDP sockets that's been disconnected
1061        // can be reused (after putting it back in the right state).
1062        let new_remote_port = NonZeroU16::new(9999).unwrap();
1063        api.connect(
1064            &socket,
1065            Some(ZonedAddr::Unzoned(I::TEST_ADDRS.remote_ip)),
1066            new_remote_port.into(),
1067        )
1068        .unwrap();
1069
1070        let info = api.get_info(&socket);
1071        assert_matches!(info, SocketInfo::Connected(ConnInfo { .. }));
1072        if let SocketInfo::Connected(conn_info) = info {
1073            // Local port was reallocated, so no assert for that.
1074            assert_eq!(conn_info.remote_identifier, new_remote_port.get());
1075        }
1076    }
1077}