solana_net_utils/
sockets.rs

1#[cfg(feature = "dev-context-only-utils")]
2use tokio::net::UdpSocket as TokioUdpSocket;
3use {
4    crate::PortRange,
5    log::warn,
6    socket2::{Domain, SockAddr, Socket, Type},
7    std::{
8        io,
9        net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener, UdpSocket},
10        ops::Range,
11        sync::atomic::{AtomicU16, Ordering},
12    },
13};
14// base port for deconflicted allocations
15pub(crate) const UNIQUE_ALLOC_BASE_PORT: u16 = 2000;
16// how much to allocate per individual process.
17// we expect to have at most 64 concurrent tests in CI at any moment on a given host.
18const SLICE_PER_PROCESS: u16 = (u16::MAX - UNIQUE_ALLOC_BASE_PORT) / 64;
19/// When running under nextest, this will try to provide
20/// a unique slice of port numbers (assuming no other nextest processes
21/// are running on the same host) based on NEXTEST_TEST_GLOBAL_SLOT variable
22/// The port ranges will be reused following nextest logic.
23///
24/// When running without nextest, this will only bump an atomic and eventually
25/// panic when it runs out of port numbers to assign.
26#[allow(clippy::arithmetic_side_effects)]
27pub fn unique_port_range_for_tests(size: u16) -> Range<u16> {
28    static SLICE: AtomicU16 = AtomicU16::new(0);
29    let offset = SLICE.fetch_add(size, Ordering::SeqCst);
30    let start = offset
31        + match std::env::var("NEXTEST_TEST_GLOBAL_SLOT") {
32            Ok(slot) => {
33                let slot: u16 = slot.parse().unwrap();
34                assert!(
35                    offset < SLICE_PER_PROCESS,
36                    "Overrunning into the port range of another test! Consider using fewer ports \
37                     per test."
38                );
39                UNIQUE_ALLOC_BASE_PORT + slot * SLICE_PER_PROCESS
40            }
41            Err(_) => UNIQUE_ALLOC_BASE_PORT,
42        };
43    assert!(start < u16::MAX - size, "Ran out of port numbers!");
44    start..start + size
45}
46
47/// Retrieve a free 25-port slice for unit tests
48///
49/// When running under nextest, this will try to provide
50/// a unique slice of port numbers (assuming no other nextest processes
51/// are running on the same host) based on NEXTEST_TEST_GLOBAL_SLOT variable
52/// The port ranges will be reused following nextest logic.
53///
54/// When running without nextest, this will only bump an atomic and eventually
55/// panic when it runs out of port numbers to assign.
56pub fn localhost_port_range_for_tests() -> (u16, u16) {
57    let pr = unique_port_range_for_tests(25);
58    (pr.start, pr.end)
59}
60
61/// Bind a `UdpSocket` to a unique port.
62pub fn bind_to_localhost_unique() -> io::Result<UdpSocket> {
63    bind_to(
64        IpAddr::V4(Ipv4Addr::LOCALHOST),
65        unique_port_range_for_tests(1).start,
66    )
67}
68
69pub fn bind_gossip_port_in_range(
70    gossip_addr: &SocketAddr,
71    port_range: PortRange,
72    bind_ip_addr: IpAddr,
73) -> (u16, (UdpSocket, TcpListener)) {
74    let config = SocketConfiguration::default();
75    if gossip_addr.port() != 0 {
76        (
77            gossip_addr.port(),
78            bind_common_with_config(bind_ip_addr, gossip_addr.port(), config).unwrap_or_else(|e| {
79                panic!("gossip_addr bind_to port {}: {}", gossip_addr.port(), e)
80            }),
81        )
82    } else {
83        bind_common_in_range_with_config(bind_ip_addr, port_range, config).expect("Failed to bind")
84    }
85}
86
87/// True on platforms that support advanced socket configuration
88pub(crate) const PLATFORM_SUPPORTS_SOCKET_CONFIGS: bool =
89    cfg!(not(any(windows, target_os = "ios")));
90
91#[derive(Clone, Copy, Debug, Default)]
92pub struct SocketConfiguration {
93    reuseport: bool, // controls SO_REUSEPORT, this is not intended to be set explicitly
94    recv_buffer_size: Option<usize>,
95    send_buffer_size: Option<usize>,
96    non_blocking: bool,
97}
98
99impl SocketConfiguration {
100    /// Sets the receive buffer size for the socket (no effect on windows/ios).
101    ///
102    /// **Note:** On Linux the kernel will double the value you specify.
103    /// For example, if you specify `16MB`, the kernel will configure the
104    /// socket to use `32MB`.
105    /// See: https://man7.org/linux/man-pages/man7/socket.7.html: SO_RCVBUF
106    pub fn recv_buffer_size(mut self, size: usize) -> Self {
107        self.recv_buffer_size = Some(size);
108        self
109    }
110
111    /// Sets the send buffer size for the socket (no effect on windows/ios)
112    ///
113    /// **Note:** On Linux the kernel will double the value you specify.
114    /// For example, if you specify `16MB`, the kernel will configure the
115    /// socket to use `32MB`.
116    /// See: https://man7.org/linux/man-pages/man7/socket.7.html: SO_SNDBUF
117    pub fn send_buffer_size(mut self, size: usize) -> Self {
118        self.send_buffer_size = Some(size);
119        self
120    }
121
122    /// Configure the socket for non-blocking IO
123    pub fn set_non_blocking(mut self, non_blocking: bool) -> Self {
124        self.non_blocking = non_blocking;
125        self
126    }
127}
128
129#[allow(deprecated)]
130impl From<crate::SocketConfig> for SocketConfiguration {
131    fn from(value: crate::SocketConfig) -> Self {
132        Self {
133            reuseport: value.reuseport,
134            recv_buffer_size: value.recv_buffer_size,
135            send_buffer_size: value.send_buffer_size,
136            non_blocking: false,
137        }
138    }
139}
140
141#[cfg(any(windows, target_os = "ios"))]
142fn set_reuse_port<T>(_socket: &T) -> io::Result<()> {
143    Ok(())
144}
145
146/// Sets SO_REUSEPORT on platforms that support it.
147#[cfg(not(any(windows, target_os = "ios")))]
148fn set_reuse_port<T>(socket: &T) -> io::Result<()>
149where
150    T: std::os::fd::AsFd,
151{
152    use nix::sys::socket::{setsockopt, sockopt::ReusePort};
153    setsockopt(socket, ReusePort, &true).map_err(io::Error::from)
154}
155
156pub(crate) fn udp_socket_with_config(config: SocketConfiguration) -> io::Result<Socket> {
157    let SocketConfiguration {
158        reuseport,
159        recv_buffer_size,
160        send_buffer_size,
161        non_blocking,
162    } = config;
163    let sock = Socket::new(Domain::IPV4, Type::DGRAM, None)?;
164    if PLATFORM_SUPPORTS_SOCKET_CONFIGS {
165        // Set buffer sizes
166        if let Some(recv_buffer_size) = recv_buffer_size {
167            sock.set_recv_buffer_size(recv_buffer_size)?;
168        }
169        if let Some(send_buffer_size) = send_buffer_size {
170            sock.set_send_buffer_size(send_buffer_size)?;
171        }
172
173        if reuseport {
174            set_reuse_port(&sock)?;
175        }
176    }
177    sock.set_nonblocking(non_blocking)?;
178    Ok(sock)
179}
180
181/// Find a port in the given range with a socket config that is available for both TCP and UDP
182pub fn bind_common_in_range_with_config(
183    ip_addr: IpAddr,
184    range: PortRange,
185    config: SocketConfiguration,
186) -> io::Result<(u16, (UdpSocket, TcpListener))> {
187    for port in range.0..range.1 {
188        if let Ok((sock, listener)) = bind_common_with_config(ip_addr, port, config) {
189            return Result::Ok((sock.local_addr().unwrap().port(), (sock, listener)));
190        }
191    }
192
193    Err(io::Error::other(format!(
194        "No available TCP/UDP ports in {range:?}"
195    )))
196}
197
198pub fn bind_in_range_with_config(
199    ip_addr: IpAddr,
200    range: PortRange,
201    config: SocketConfiguration,
202) -> io::Result<(u16, UdpSocket)> {
203    let socket = udp_socket_with_config(config)?;
204
205    for port in range.0..range.1 {
206        let addr = SocketAddr::new(ip_addr, port);
207
208        if socket.bind(&SockAddr::from(addr)).is_ok() {
209            let udp_socket: UdpSocket = socket.into();
210            return Result::Ok((udp_socket.local_addr().unwrap().port(), udp_socket));
211        }
212    }
213
214    Err(io::Error::other(format!(
215        "No available UDP ports in {range:?}"
216    )))
217}
218
219#[deprecated(since = "3.0.0", note = "Please bind to specific ports instead")]
220#[allow(deprecated)]
221pub fn bind_with_any_port_with_config(
222    ip_addr: IpAddr,
223    config: SocketConfiguration,
224) -> io::Result<UdpSocket> {
225    _bind_with_any_port_with_config(ip_addr, config)
226}
227
228// this private method works around a cargo bug involving nested deprecations
229// remove it with the above deprecated public method
230fn _bind_with_any_port_with_config(
231    ip_addr: IpAddr,
232    config: SocketConfiguration,
233) -> io::Result<UdpSocket> {
234    let sock = udp_socket_with_config(config)?;
235    let addr = SocketAddr::new(ip_addr, 0);
236    let bind = sock.bind(&SockAddr::from(addr));
237    match bind {
238        Ok(_) => Result::Ok(sock.into()),
239        Err(err) => Err(io::Error::other(format!("No available UDP port: {err}"))),
240    }
241}
242
243/// binds num sockets to the same port in a range with config
244pub fn multi_bind_in_range_with_config(
245    ip_addr: IpAddr,
246    range: PortRange,
247    config: SocketConfiguration,
248    mut num: usize,
249) -> io::Result<(u16, Vec<UdpSocket>)> {
250    if !PLATFORM_SUPPORTS_SOCKET_CONFIGS && num != 1 {
251        // See https://github.com/solana-labs/solana/issues/4607
252        warn!(
253            "multi_bind_in_range_with_config() only supports 1 socket on this platform ({num} \
254             requested)"
255        );
256        num = 1;
257    }
258    let (port, socket) = bind_in_range_with_config(ip_addr, range, config)?;
259    let sockets = bind_more_with_config(socket, num, config)?;
260    Ok((port, sockets))
261}
262
263pub fn bind_to(ip_addr: IpAddr, port: u16) -> io::Result<UdpSocket> {
264    let config = SocketConfiguration {
265        ..Default::default()
266    };
267    bind_to_with_config(ip_addr, port, config)
268}
269
270#[cfg(feature = "dev-context-only-utils")]
271pub async fn bind_to_async(ip_addr: IpAddr, port: u16) -> io::Result<TokioUdpSocket> {
272    let config = SocketConfiguration {
273        non_blocking: true,
274        ..Default::default()
275    };
276    let socket = bind_to_with_config(ip_addr, port, config)?;
277    TokioUdpSocket::from_std(socket)
278}
279
280#[cfg(feature = "dev-context-only-utils")]
281pub async fn bind_to_localhost_async() -> io::Result<TokioUdpSocket> {
282    let port = unique_port_range_for_tests(1).start;
283    bind_to_async(IpAddr::V4(Ipv4Addr::LOCALHOST), port).await
284}
285
286#[cfg(feature = "dev-context-only-utils")]
287pub async fn bind_to_unspecified_async() -> io::Result<TokioUdpSocket> {
288    let port = unique_port_range_for_tests(1).start;
289    bind_to_async(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port).await
290}
291
292pub fn bind_to_with_config(
293    ip_addr: IpAddr,
294    port: u16,
295    config: SocketConfiguration,
296) -> io::Result<UdpSocket> {
297    let sock = udp_socket_with_config(config)?;
298
299    let addr = SocketAddr::new(ip_addr, port);
300
301    sock.bind(&SockAddr::from(addr)).map(|_| sock.into())
302}
303
304/// binds both a UdpSocket and a TcpListener on the same port
305pub fn bind_common_with_config(
306    ip_addr: IpAddr,
307    port: u16,
308    config: SocketConfiguration,
309) -> io::Result<(UdpSocket, TcpListener)> {
310    let sock = udp_socket_with_config(config)?;
311
312    let addr = SocketAddr::new(ip_addr, port);
313    let sock_addr = SockAddr::from(addr);
314    sock.bind(&sock_addr)
315        .and_then(|_| TcpListener::bind(addr).map(|listener| (sock.into(), listener)))
316}
317
318pub fn bind_two_in_range_with_offset_and_config(
319    ip_addr: IpAddr,
320    range: PortRange,
321    offset: u16,
322    sock1_config: SocketConfiguration,
323    sock2_config: SocketConfiguration,
324) -> io::Result<((u16, UdpSocket), (u16, UdpSocket))> {
325    if range.1.saturating_sub(range.0) < offset {
326        return Err(io::Error::other(
327            "range too small to find two ports with the correct offset".to_string(),
328        ));
329    }
330
331    let max_start_port = range.1.saturating_sub(offset);
332    for port in range.0..=max_start_port {
333        let first_bind_result = bind_to_with_config(ip_addr, port, sock1_config);
334        if let Ok(first_bind) = first_bind_result {
335            let second_port = port.saturating_add(offset);
336            let second_bind_result = bind_to_with_config(ip_addr, second_port, sock2_config);
337            if let Ok(second_bind) = second_bind_result {
338                return Ok((
339                    (first_bind.local_addr().unwrap().port(), first_bind),
340                    (second_bind.local_addr().unwrap().port(), second_bind),
341                ));
342            }
343        }
344    }
345    Err(io::Error::other(
346        "couldn't find two ports with the correct offset in range".to_string(),
347    ))
348}
349
350pub fn bind_more_with_config(
351    socket: UdpSocket,
352    num: usize,
353    mut config: SocketConfiguration,
354) -> io::Result<Vec<UdpSocket>> {
355    if !PLATFORM_SUPPORTS_SOCKET_CONFIGS {
356        if num > 1 {
357            warn!(
358                "bind_more_with_config() only supports 1 socket on this platform ({num} requested)"
359            );
360        }
361        Ok(vec![socket])
362    } else {
363        set_reuse_port(&socket)?;
364        config.reuseport = true;
365        let addr = socket.local_addr().unwrap();
366        let ip = addr.ip();
367        let port = addr.port();
368        std::iter::once(Ok(socket))
369            .chain((1..num).map(|_| bind_to_with_config(ip, port, config)))
370            .collect()
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use {
377        super::*,
378        crate::{
379            bind_in_range, get_cluster_shred_version, get_public_ip_addr_with_binding,
380            ip_echo_client, ip_echo_server, parse_host,
381            sockets::{localhost_port_range_for_tests, unique_port_range_for_tests},
382            verify_all_reachable_tcp, verify_all_reachable_udp, DEFAULT_IP_ECHO_SERVER_THREADS,
383            MAX_PORT_VERIFY_THREADS,
384        },
385        itertools::Itertools,
386        std::{net::Ipv4Addr, time::Duration},
387        tokio::runtime::Runtime,
388    };
389
390    fn runtime() -> Runtime {
391        tokio::runtime::Builder::new_current_thread()
392            .enable_all()
393            .build()
394            .expect("Can not create a runtime")
395    }
396
397    #[test]
398    fn test_bind() {
399        let (pr_s, pr_e) = localhost_port_range_for_tests();
400        let ip_addr = IpAddr::V4(Ipv4Addr::LOCALHOST);
401        let config = SocketConfiguration::default();
402        let s = bind_in_range(ip_addr, (pr_s, pr_e)).unwrap();
403        assert_eq!(s.0, pr_s, "bind_in_range should use first available port");
404        let ip_addr = IpAddr::V4(Ipv4Addr::LOCALHOST);
405        let x = bind_to_with_config(ip_addr, pr_s + 1, config).unwrap();
406        let y = bind_more_with_config(x, 2, config).unwrap();
407        assert_eq!(
408            y[0].local_addr().unwrap().port(),
409            y[1].local_addr().unwrap().port()
410        );
411        bind_to_with_config(ip_addr, pr_s, SocketConfiguration::default()).unwrap_err();
412        bind_in_range(ip_addr, (pr_s, pr_s + 2)).unwrap_err();
413
414        let (port, v) =
415            multi_bind_in_range_with_config(ip_addr, (pr_s + 5, pr_e), config, 10).unwrap();
416        for sock in &v {
417            assert_eq!(port, sock.local_addr().unwrap().port());
418        }
419    }
420
421    #[test]
422    fn test_bind_with_any_port() {
423        let x = bind_to_localhost_unique().unwrap();
424        let y = bind_to_localhost_unique().unwrap();
425        assert_ne!(
426            x.local_addr().unwrap().port(),
427            y.local_addr().unwrap().port()
428        );
429    }
430
431    #[test]
432    fn test_bind_in_range_nil() {
433        let ip_addr = IpAddr::V4(Ipv4Addr::LOCALHOST);
434        let range = unique_port_range_for_tests(2);
435        bind_in_range(ip_addr, (range.end, range.end)).unwrap_err();
436        bind_in_range(ip_addr, (range.end, range.start)).unwrap_err();
437    }
438
439    #[test]
440    fn test_bind_on_top() {
441        let config = SocketConfiguration::default();
442        let localhost = IpAddr::V4(Ipv4Addr::LOCALHOST);
443        let port_range = localhost_port_range_for_tests();
444        let (_p, s) = bind_in_range_with_config(localhost, port_range, config).unwrap();
445        let _socks = bind_more_with_config(s, 8, config).unwrap();
446
447        let _socks2 = multi_bind_in_range_with_config(localhost, port_range, config, 8).unwrap();
448    }
449
450    #[test]
451    fn test_bind_common_in_range() {
452        let ip_addr = IpAddr::V4(Ipv4Addr::LOCALHOST);
453        let range = unique_port_range_for_tests(5);
454        let config = SocketConfiguration::default();
455        let (port, _sockets) =
456            bind_common_in_range_with_config(ip_addr, (range.start, range.end), config).unwrap();
457        assert!(range.contains(&port));
458        bind_common_in_range_with_config(ip_addr, (port, port + 1), config).unwrap_err();
459    }
460
461    #[test]
462    fn test_bind_two_in_range_with_offset() {
463        agave_logger::setup();
464        let config = SocketConfiguration::default();
465        let ip_addr = IpAddr::V4(Ipv4Addr::LOCALHOST);
466        let offset = 6;
467        let port_range = unique_port_range_for_tests(10);
468        if let Ok(((port1, _), (port2, _))) = bind_two_in_range_with_offset_and_config(
469            ip_addr,
470            (port_range.start, port_range.end),
471            offset,
472            config,
473            config,
474        ) {
475            assert!(port2 == port1 + offset);
476        }
477        let offset = 42;
478        if let Ok(((port1, _), (port2, _))) = bind_two_in_range_with_offset_and_config(
479            ip_addr,
480            (port_range.start, port_range.end),
481            offset,
482            config,
483            config,
484        ) {
485            assert!(port2 == port1 + offset);
486        }
487        assert!(bind_two_in_range_with_offset_and_config(
488            ip_addr,
489            (port_range.start, port_range.start + 5),
490            offset,
491            config,
492            config
493        )
494        .is_err());
495    }
496
497    #[test]
498    fn test_get_public_ip_addr_none() {
499        agave_logger::setup();
500        let ip_addr = IpAddr::V4(Ipv4Addr::LOCALHOST);
501        let (pr_s, pr_e) = localhost_port_range_for_tests();
502        let config = SocketConfiguration::default();
503        let (_server_port, (server_udp_socket, server_tcp_listener)) =
504            bind_common_in_range_with_config(ip_addr, (pr_s, pr_e), config).unwrap();
505
506        let _runtime = ip_echo_server(
507            server_tcp_listener,
508            DEFAULT_IP_ECHO_SERVER_THREADS,
509            /*shred_version=*/ Some(42),
510        );
511
512        let server_ip_echo_addr = server_udp_socket.local_addr().unwrap();
513        assert_eq!(
514            get_public_ip_addr_with_binding(
515                &server_ip_echo_addr,
516                IpAddr::V4(Ipv4Addr::UNSPECIFIED)
517            )
518            .unwrap(),
519            parse_host("127.0.0.1").unwrap(),
520        );
521        assert_eq!(get_cluster_shred_version(&server_ip_echo_addr).unwrap(), 42);
522        assert!(verify_all_reachable_tcp(&server_ip_echo_addr, vec![],));
523        assert!(verify_all_reachable_udp(&server_ip_echo_addr, &[],));
524    }
525
526    #[test]
527    fn test_get_public_ip_addr_reachable() {
528        agave_logger::setup();
529        let ip_addr = IpAddr::V4(Ipv4Addr::LOCALHOST);
530        let port_range = localhost_port_range_for_tests();
531        let config = SocketConfiguration::default();
532        let (_server_port, (server_udp_socket, server_tcp_listener)) =
533            bind_common_in_range_with_config(ip_addr, port_range, config).unwrap();
534        let (_client_port, (client_udp_socket, client_tcp_listener)) =
535            bind_common_in_range_with_config(ip_addr, port_range, config).unwrap();
536
537        let _runtime = ip_echo_server(
538            server_tcp_listener,
539            DEFAULT_IP_ECHO_SERVER_THREADS,
540            /*shred_version=*/ Some(65535),
541        );
542
543        let ip_echo_server_addr = server_udp_socket.local_addr().unwrap();
544        assert_eq!(
545            get_public_ip_addr_with_binding(
546                &ip_echo_server_addr,
547                IpAddr::V4(Ipv4Addr::UNSPECIFIED)
548            )
549            .unwrap(),
550            parse_host("127.0.0.1").unwrap(),
551        );
552        assert_eq!(
553            get_cluster_shred_version(&ip_echo_server_addr).unwrap(),
554            65535
555        );
556        assert!(verify_all_reachable_tcp(
557            &ip_echo_server_addr,
558            vec![client_tcp_listener],
559        ));
560        assert!(verify_all_reachable_udp(
561            &ip_echo_server_addr,
562            &[&client_udp_socket],
563        ));
564    }
565
566    #[test]
567    fn test_verify_ports_tcp_unreachable() {
568        agave_logger::setup();
569        let ip_addr = IpAddr::V4(Ipv4Addr::LOCALHOST);
570        let port_range = localhost_port_range_for_tests();
571        let config = SocketConfiguration::default();
572        let (_server_port, (server_udp_socket, _server_tcp_listener)) =
573            bind_common_in_range_with_config(ip_addr, port_range, config).unwrap();
574
575        // make the socket unreachable by not running the ip echo server!
576        let server_ip_echo_addr = server_udp_socket.local_addr().unwrap();
577
578        let (_, (_client_udp_socket, client_tcp_listener)) =
579            bind_common_in_range_with_config(ip_addr, port_range, config).unwrap();
580
581        let rt = runtime();
582        assert!(!rt.block_on(ip_echo_client::verify_all_reachable_tcp(
583            server_ip_echo_addr,
584            vec![client_tcp_listener],
585            Duration::from_secs(2),
586        )));
587    }
588
589    #[test]
590    fn test_verify_ports_udp_unreachable() {
591        agave_logger::setup();
592        let ip_addr = IpAddr::V4(Ipv4Addr::LOCALHOST);
593        let port_range = unique_port_range_for_tests(2);
594        let config = SocketConfiguration::default();
595        let (_server_port, (server_udp_socket, _server_tcp_listener)) =
596            bind_common_in_range_with_config(ip_addr, (port_range.start, port_range.end), config)
597                .unwrap();
598
599        // make the socket unreachable by not running the ip echo server!
600        let server_ip_echo_addr = server_udp_socket.local_addr().unwrap();
601
602        let (_correct_client_port, (client_udp_socket, _client_tcp_listener)) =
603            bind_common_in_range_with_config(ip_addr, (port_range.start, port_range.end), config)
604                .unwrap();
605
606        let rt = runtime();
607        assert!(!rt.block_on(ip_echo_client::verify_all_reachable_udp(
608            server_ip_echo_addr,
609            &[&client_udp_socket],
610            Duration::from_secs(2),
611            3,
612        )));
613    }
614
615    #[test]
616    fn test_verify_many_ports_reachable() {
617        agave_logger::setup();
618        let ip_addr = IpAddr::V4(Ipv4Addr::LOCALHOST);
619        let config = SocketConfiguration::default();
620        let mut tcp_listeners = vec![];
621        let mut udp_sockets = vec![];
622
623        let port_range = unique_port_range_for_tests(1);
624        let (_server_port, (_, server_tcp_listener)) =
625            bind_common_in_range_with_config(ip_addr, (port_range.start, port_range.end), config)
626                .unwrap();
627        for _ in 0..MAX_PORT_VERIFY_THREADS * 2 {
628            let port_range = unique_port_range_for_tests(1);
629            let (_client_port, (client_udp_socket, client_tcp_listener)) =
630                bind_common_in_range_with_config(
631                    ip_addr,
632                    (port_range.start, port_range.end),
633                    config,
634                )
635                .unwrap();
636            tcp_listeners.push(client_tcp_listener);
637            udp_sockets.push(client_udp_socket);
638        }
639
640        let ip_echo_server_addr = server_tcp_listener.local_addr().unwrap();
641
642        let _runtime = ip_echo_server(
643            server_tcp_listener,
644            DEFAULT_IP_ECHO_SERVER_THREADS,
645            Some(65535),
646        );
647
648        assert_eq!(
649            get_public_ip_addr_with_binding(
650                &ip_echo_server_addr,
651                IpAddr::V4(Ipv4Addr::UNSPECIFIED)
652            )
653            .unwrap(),
654            parse_host("127.0.0.1").unwrap(),
655        );
656
657        let socket_refs = udp_sockets.iter().collect_vec();
658        assert!(verify_all_reachable_tcp(
659            &ip_echo_server_addr,
660            tcp_listeners,
661        ));
662        assert!(verify_all_reachable_udp(&ip_echo_server_addr, &socket_refs));
663    }
664
665    // This test is gated for non-macOS platforms because it requires binding to 127.0.0.2,
666    // which is not supported on macOS by default.
667    #[cfg(not(target_os = "macos"))]
668    #[test]
669    fn test_verify_udp_multiple_ips_reachable() {
670        agave_logger::setup();
671        let config = SocketConfiguration::default();
672        let ip_a = IpAddr::V4(Ipv4Addr::LOCALHOST);
673        let ip_b = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2));
674
675        let port_range = localhost_port_range_for_tests();
676
677        let (_srv_udp_port, (srv_udp_sock, srv_tcp_listener)) =
678            bind_common_in_range_with_config(ip_a, port_range, config).unwrap();
679
680        let ip_echo_server_addr = srv_udp_sock.local_addr().unwrap();
681        let _runtime = ip_echo_server(
682            srv_tcp_listener,
683            DEFAULT_IP_ECHO_SERVER_THREADS,
684            /*shred_version=*/ Some(42),
685        );
686
687        let mut udp_sockets = Vec::new();
688        let (_p1, (sock_a, _tl_a)) =
689            bind_common_in_range_with_config(ip_a, port_range, config).unwrap();
690        let (_p2, (sock_b, _tl_b)) =
691            bind_common_in_range_with_config(ip_b, port_range, config).unwrap();
692
693        udp_sockets.push(sock_a);
694        udp_sockets.push(sock_b);
695
696        let socket_refs: Vec<&UdpSocket> = udp_sockets.iter().collect();
697
698        assert!(
699            verify_all_reachable_udp(&ip_echo_server_addr, &socket_refs),
700            "all UDP ports on both 127.0.0.1 and 127.0.0.2 should be reachable"
701        );
702    }
703}