kaboodle/
networking.rs

1//! This utility module contains some network interface conveniences.
2
3use if_addrs::{IfAddr, Interface};
4use socket2::{Domain, Protocol, SockAddr, Socket, Type};
5use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4};
6use tokio::net::UdpSocket;
7
8use super::errors::KaboodleError;
9
10/// Returns the best available network interface. Only non-loopback interfaces are considered, and
11/// IPv6 interfaces are preferred.
12pub fn best_available_interface() -> Result<Interface, KaboodleError> {
13    // If no interface was provided, use the first IPv6 interface we find, and if there are
14    // no IPv6 interfaces, use the first IPv4 interface.
15    let non_loopbacks = non_loopback_interfaces();
16    let first_ipv6_interface = non_loopbacks
17        .iter()
18        .find(|xs| matches!(xs.addr, IfAddr::V6(_)));
19    first_ipv6_interface
20        .or_else(|| non_loopbacks.first())
21        .cloned()
22        .ok_or_else(|| KaboodleError::NoAvailableInterfaces)
23}
24
25/// Creates a pair of sockets for receiving and sending multicast messages on the given port with
26/// the given  interface.
27pub fn create_broadcast_sockets(
28    interface: &Interface,
29    broadcast_port: &u16,
30) -> Result<(UdpSocket, UdpSocket, SocketAddr), KaboodleError> {
31    match interface.ip() {
32        IpAddr::V4(_) => {
33            // IPv4:
34            // - for listening to broadcasts, we need to bind our socket to 0.0.0.0:<port>
35            // - for sending broadcasts, we need to send to 255.255.255.255:<port>.
36            let broadcast_inbound_addr = SocketAddr::V4(SocketAddrV4::new(
37                Ipv4Addr::new(0, 0, 0, 0),
38                *broadcast_port,
39            ));
40            let broadcast_outbound_addr = SocketAddr::V4(SocketAddrV4::new(
41                Ipv4Addr::new(255, 255, 255, 255),
42                *broadcast_port,
43            ));
44            let broadcast_raw_sock: std::net::UdpSocket = {
45                let broadcast_sock =
46                    Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)).unwrap();
47                broadcast_sock.set_broadcast(true).unwrap();
48                broadcast_sock.set_nonblocking(true).unwrap();
49                broadcast_sock.set_reuse_address(true).unwrap();
50                broadcast_sock.set_reuse_port(true).unwrap();
51                broadcast_sock
52                    .bind(&SockAddr::from(broadcast_inbound_addr))
53                    .expect("Failed to bind for broadcast");
54                broadcast_sock.into()
55            };
56
57            // Unlike IPv6, we can use a single socket for both sending and receiving broadcasts,
58            // but consistency for consuming code, it makes sense to return two here as well.
59            let broadcast_in_sock = UdpSocket::from_std(broadcast_raw_sock.try_clone()?)?;
60            let broadcast_out_sock = UdpSocket::from_std(broadcast_raw_sock)?;
61
62            Ok((
63                broadcast_in_sock,
64                broadcast_out_sock,
65                broadcast_outbound_addr,
66            ))
67        }
68        IpAddr::V6(_) => {
69            // IPv6:
70            // - for listening to broadcasts, we have to join a multicast IP address.
71            // - for sending broadcast messages, we have to explicitly tell the socket which
72            //   network interface to use, otherwise it won't have a route to the multicast IP.
73
74            let Some(interface_idx) = interface.index else {
75                return Err(KaboodleError::UnableToFindInterfaceNumber);
76            };
77
78            // All IPv6 IP addresses in the ff00::/8 prefix are multicast addresses. The first 16
79            // bits of the IP indicate the "multicast group", and the remaining 112 bits are the
80            // "group ID". A prefix of 0xff02 indicates link-local multicast, where "link-local"
81            // means the traffic won't pass beyond the local router.
82            // Broadly speaking, we can use whatever group ID we like, so long as it doesn't
83            // conflict with any well-known services.
84            // See https://www.ciscopress.com/articles/article.asp?p=2803866&seqNum=5 for more
85            // information about IPv6 multicast addresses.
86            let broadcast_ip_addr = Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0x1213, 0x1989);
87
88            let broadcast_socket_addr =
89                SocketAddr::new(IpAddr::V6(broadcast_ip_addr), *broadcast_port);
90            let broadcast_in_sock = {
91                let sock = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP))?;
92                sock.join_multicast_v6(&broadcast_ip_addr, interface_idx)?;
93                sock.set_nonblocking(true)?;
94                sock.set_only_v6(true)?;
95                sock.set_reuse_address(true)?;
96                sock.set_reuse_port(true)?;
97                sock.bind(&SockAddr::from(SocketAddr::new(
98                    Ipv6Addr::UNSPECIFIED.into(),
99                    *broadcast_port,
100                )))?;
101
102                UdpSocket::from_std(sock.into())?
103            };
104            let broadcast_out_sock = {
105                let sock = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP))?;
106                sock.set_multicast_if_v6(interface_idx)?;
107                sock.set_nonblocking(true)?;
108                sock.set_reuse_address(true)?;
109                sock.set_reuse_port(true)?;
110                sock.bind(&SockAddr::from(SocketAddr::new(
111                    Ipv6Addr::UNSPECIFIED.into(),
112                    0,
113                )))?;
114
115                UdpSocket::from_std(sock.into())?
116            };
117
118            Ok((broadcast_in_sock, broadcast_out_sock, broadcast_socket_addr))
119        }
120    }
121}
122
123/// Get all non-loopback interfaces for this host.
124pub fn non_loopback_interfaces() -> Vec<Interface> {
125    if_addrs::get_if_addrs()
126        .unwrap_or_default()
127        .into_iter()
128        .filter(|addr| !addr.is_loopback())
129        .collect()
130}