alpine_protocol_sdk/
discovery.rs

1use std::{
2    fmt, io,
3    net::{IpAddr, SocketAddr, UdpSocket},
4    time::Duration,
5};
6
7use alpine::messages::{DiscoveryReply, DiscoveryRequest};
8use rand::{rngs::OsRng, RngCore};
9use serde_cbor;
10use tracing::trace;
11
12const DEFAULT_MULTICAST_IPV4: &str = "239.255.255.250:5555";
13const DEFAULT_MULTICAST_IPV6: &str = "[ff12::1]:5555";
14const DEFAULT_BROADCAST_IPV4: &str = "255.255.255.255:5555";
15
16/// Options used to configure the blocking discovery helper.
17pub struct DiscoveryClientOptions {
18    pub remote_addr: SocketAddr,
19    pub local_addr: SocketAddr,
20    pub timeout: Duration,
21    pub prefer_multicast: bool,
22}
23
24impl DiscoveryClientOptions {
25    /// Creates options with the provided remote socket and a default timeout.
26    pub fn new(remote_addr: SocketAddr, local_addr: SocketAddr, timeout: Duration) -> Self {
27        Self {
28            remote_addr,
29            local_addr,
30            timeout,
31            prefer_multicast: true,
32        }
33    }
34
35    pub fn disable_multicast(mut self) -> Self {
36        self.prefer_multicast = false;
37        self
38    }
39}
40
41/// Errors that can happen while sending or receiving discovery payloads.
42#[derive(Debug)]
43pub enum DiscoveryError {
44    Io(io::Error),
45    Decode(serde_cbor::Error),
46    Timeout,
47    PermissionDenied,
48    MulticastUnavailable,
49    BroadcastBlocked,
50}
51
52impl fmt::Display for DiscoveryError {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            DiscoveryError::Io(err) => write!(f, "io error: {}", err),
56            DiscoveryError::Decode(err) => write!(f, "cbors serialization error: {}", err),
57            DiscoveryError::Timeout => write!(f, "discovery timed out"),
58            DiscoveryError::PermissionDenied => {
59                write!(f, "discovery channel permission denied")
60            }
61            DiscoveryError::MulticastUnavailable => {
62                write!(f, "multicast discovery unavailable")
63            }
64            DiscoveryError::BroadcastBlocked => write!(f, "broadcast discovery blocked"),
65        }
66    }
67}
68
69impl std::error::Error for DiscoveryError {}
70
71impl From<io::Error> for DiscoveryError {
72    fn from(err: io::Error) -> Self {
73        match err.kind() {
74            io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock => DiscoveryError::Timeout,
75            _ => DiscoveryError::Io(err),
76        }
77    }
78}
79
80impl From<serde_cbor::Error> for DiscoveryError {
81    fn from(err: serde_cbor::Error) -> Self {
82        DiscoveryError::Decode(err)
83    }
84}
85
86/// The outcome of a discovery request.
87pub struct DiscoveryOutcome {
88    pub reply: DiscoveryReply,
89    pub peer: SocketAddr,
90}
91
92/// Stateless discovery helper that wraps the protocol request/response models.
93pub struct DiscoveryClient {
94    socket: UdpSocket,
95    remote_addr: SocketAddr,
96    prefer_multicast: bool,
97}
98
99impl DiscoveryClient {
100    /// Creates a client that will send discovery packets to `remote_addr`.
101    pub fn new(options: DiscoveryClientOptions) -> Result<Self, DiscoveryError> {
102        let socket = UdpSocket::bind(options.local_addr)?;
103        socket.set_broadcast(true)?;
104        socket.set_read_timeout(Some(options.timeout))?;
105        Ok(Self {
106            socket,
107            remote_addr: options.remote_addr,
108            prefer_multicast: options.prefer_multicast,
109        })
110    }
111
112    /// Sends a discovery payload with the requested capability names and waits for a reply.
113    pub fn discover(&self, requested: &[String]) -> Result<DiscoveryOutcome, DiscoveryError> {
114        let mut nonce = vec![0u8; 32];
115        OsRng.fill_bytes(&mut nonce);
116        let request = DiscoveryRequest::new(requested.to_vec(), nonce.clone());
117        let payload = serde_cbor::to_vec(&request)?;
118
119        let mut send_error: Option<DiscoveryError> = None;
120        let mut sent = false;
121        for target in self.discovery_targets() {
122            match self.socket.send_to(&payload, target) {
123                Ok(_) => {
124                    trace!(target = %target, "discovery payload sent");
125                    sent = true;
126                    break;
127                }
128            Err(err) => {
129                let kind = err.kind();
130                let mapped = self.map_send_error(err, target);
131                trace!(target = %target, error = %mapped, "discovery send failed");
132                send_error = Some(mapped);
133                if !self.should_continue_after_error(&kind) {
134                    return Err(send_error.unwrap());
135                }
136            }
137            }
138        }
139
140        if !sent {
141            return Err(send_error.unwrap_or_else(|| DiscoveryError::PermissionDenied));
142        }
143
144        let mut buf = vec![0u8; 2048];
145        let (len, peer) = match self.socket.recv_from(&mut buf) {
146            Ok(res) => res,
147            Err(err) => return Err(self.map_recv_error(err)),
148        };
149        let reply: DiscoveryReply = serde_cbor::from_slice(&buf[..len])?;
150        Ok(DiscoveryOutcome { reply, peer })
151    }
152
153    fn discovery_targets(&self) -> Vec<SocketAddr> {
154        let mut targets = Vec::new();
155
156        if self.prefer_multicast {
157            if let Ok(addr) = DEFAULT_MULTICAST_IPV4.parse() {
158                targets.push(addr);
159            }
160            if let Ok(addr) = DEFAULT_MULTICAST_IPV6.parse() {
161                targets.push(addr);
162            }
163        }
164
165        if !targets.contains(&self.remote_addr) {
166            targets.push(self.remote_addr);
167        }
168
169        if self.remote_addr.ip().is_ipv4() {
170            if let Ok(addr) = DEFAULT_BROADCAST_IPV4.parse() {
171                if !targets.contains(&addr) {
172                    targets.push(addr);
173                }
174            }
175        }
176
177        targets
178    }
179
180    fn map_send_error(&self, err: io::Error, target: SocketAddr) -> DiscoveryError {
181        match err.kind() {
182            io::ErrorKind::PermissionDenied => {
183                if target.ip().is_multicast() {
184                    DiscoveryError::MulticastUnavailable
185                } else if is_broadcast_addr(target.ip()) {
186                    DiscoveryError::BroadcastBlocked
187                } else {
188                    DiscoveryError::PermissionDenied
189                }
190            }
191            io::ErrorKind::ConnectionReset | io::ErrorKind::WouldBlock => {
192                DiscoveryError::PermissionDenied
193            }
194            _ => DiscoveryError::Io(err),
195        }
196    }
197
198    fn map_recv_error(&self, err: io::Error) -> DiscoveryError {
199        match err.kind() {
200            io::ErrorKind::TimedOut => DiscoveryError::Timeout,
201            io::ErrorKind::PermissionDenied
202            | io::ErrorKind::ConnectionReset
203            | io::ErrorKind::WouldBlock => DiscoveryError::PermissionDenied,
204            _ => DiscoveryError::Io(err),
205        }
206    }
207
208    fn should_continue_after_error(&self, kind: &io::ErrorKind) -> bool {
209        matches!(
210            kind,
211            io::ErrorKind::PermissionDenied
212                | io::ErrorKind::WouldBlock
213                | io::ErrorKind::ConnectionReset
214        )
215    }
216}
217
218fn is_broadcast_addr(ip: IpAddr) -> bool {
219    matches!(ip, IpAddr::V4(addr) if addr.is_broadcast())
220}