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