simpdiscoverylib/lib.rs
1#![deny(missing_docs)]
2#![warn(clippy::unwrap_used)]
3
4//! `simpdiscovery` library crate for simple UDP datagram-based discovery of services on a LAN
5//!
6//! # Example combining a BeaconSender and a BeaconListener
7//! ```
8//! use simpdiscoverylib::{BeaconSender, BeaconListener};
9//! use std::time::Duration;
10//! use portpicker::pick_unused_port;
11//!
12//! let service_port = pick_unused_port().expect("Could not get a free port");
13//! let broadcast_port = pick_unused_port().expect("Could not get a free port");
14//! let my_service_name = "_my_service._tcp.local".as_bytes();
15//! let beacon = BeaconSender::new(service_port, my_service_name, broadcast_port)
16//! .expect("Could not create sender");
17//! std::thread::spawn(move || {
18//! beacon.send_loop(Duration::from_secs(1)).expect("Could not run send_loop")
19//! });
20//!
21//! let listener = BeaconListener::new(my_service_name, broadcast_port)
22//! .expect("Could not create listener");
23//! let beacon = listener.wait(None).expect("Failed to receive beacon");
24//! assert_eq!(beacon.service_name, my_service_name, "Received service name doesn't match");
25//! assert_eq!(beacon.service_port, service_port, "Received service port doesn't match");
26//! ```
27
28use std::net::UdpSocket;
29use std::time::Duration;
30use log::{info, trace};
31use std::fmt::Formatter;
32use std::io;
33
34/// A broadcast address is always relative to a given network. When you have a network, you can
35/// compute its broadcast address by replacing all the host bits with 1s; simply put, the broadcast
36/// address is the highest numbered address you can have on the network, while the network address
37/// is the lowest one (with all host bits set to 0s); this is why you can't use either of them
38/// as actual host addresses: they are reserved for this use.
39///
40/// If your network is `192.168.1.0/24`, then your network address will be `192.168.1.0`
41/// and your broadcast address will be `192.168.1.255`
42///
43/// If your network is `192.168.0.0/16`, then your network address will be `192.168.0.0`
44/// and your broadcast address will be `192.168.255.255`
45///
46/// `255.255.255.255` is a special broadcast address, which means "this network".
47/// It lets you send a broadcast packet to the network you're connected to, without actually
48/// caring about its address.
49///
50/// See [wikipedia article](https://en.wikipedia.org/wiki/Broadcast_address) for more info
51const BROADCAST_ADDRESS : &str = "255.255.255.255";
52
53/// The address `0.0.0.0` is known as the "zero network", which in Internet Protocol standards
54/// stands for this network, i.e. the local network.
55const LISTENING_ADDRESS : &str = "0.0.0.0";
56
57const MAX_INCOMING_BEACON_SIZE : usize = 1024;
58const MAGIC_NUMBER: u16 = 0xbeef;
59
60/// `BeaconSender` is used to send UDP Datagram beacons to the Broadcast IP address on the LAN
61///
62/// # Example of using `BeaconSender`
63/// This example will just exit at the end and the thread above will die along with the process.
64///
65/// In your own code, either:
66/// * don't start a background thread and just loop forever sending beacons in main thread, or
67/// * have some other way to keep the process (and hence the sending thread) alive so
68/// beacons are actually sent before process ends
69///
70/// ```
71/// use simpdiscoverylib::{BeaconSender, BeaconListener};
72/// use std::time::Duration;
73/// use portpicker::pick_unused_port;
74///
75/// let service_port = pick_unused_port().expect("Could not get a free port");
76/// let broadcast_port = pick_unused_port().expect("Could not get a free port for broadcast");
77/// let my_service_name = "_my_service._tcp.local".as_bytes();
78/// let beacon = BeaconSender::new(service_port, my_service_name, broadcast_port)
79/// .expect("Could not create sender");
80/// std::thread::spawn(move || {
81/// beacon.send_loop(Duration::from_secs(1)).expect("Could not enter send_loop");
82/// });
83pub struct BeaconSender {
84 socket: UdpSocket,
85 beacon_payload: Vec<u8>,
86 broadcast_address: String,
87}
88
89fn u16_to_array_of_u8(x:u16) -> [u8;2] {
90 let b1 : u8 = ((x >> 8) & 0xff) as u8;
91 let b2 : u8 = (x & 0xff) as u8;
92 [b1, b2]
93}
94
95fn array_of_u8_to_u16(array: &[u8]) -> u16 {
96 let upper : u16 = (array[0] as u16) << 8;
97 let lower : u16 = array[1] as u16;
98 upper + lower
99}
100
101impl BeaconSender {
102 /// Create a new `BeaconSender` to send `Beacon`s for a service with name `service_name` that
103 /// should be contacted on the port `service_port`
104 pub fn new(service_port: u16, service_name: &[u8], broadcast_port: u16) -> io::Result<Self> {
105 // Setting the port to non-zero (or at least the same port used in listener) causes
106 // this to fail. I am not sure of the correct value to use. Docs on UDP says '0' is
107 // permitted, if you do not expect a response from the UDP Datagram sent.
108 let bind_address = format!("{LISTENING_ADDRESS}:0");
109 let socket:UdpSocket = UdpSocket::bind(&bind_address)
110 .map_err(|e|
111 io::Error::new(io::ErrorKind::AddrInUse,
112 format!("SimpDiscover::BeaconSender could not bind to UdpSocket {bind_address} ({e})")))?;
113 info!("Socket bound to: {}", bind_address);
114
115 socket.set_broadcast(true)?;
116 info!("Broadcast mode set to ON");
117
118 // Create payload with magic number, service_port number and service_name
119 let mut beacon_payload: Vec<u8> = u16_to_array_of_u8(MAGIC_NUMBER).to_vec();
120 beacon_payload.append(&mut u16_to_array_of_u8(service_port).to_vec());
121 beacon_payload.append(&mut service_name.to_vec());
122
123 let broadcast_address = format!("{BROADCAST_ADDRESS}:{broadcast_port}");
124
125 Ok(Self {
126 socket,
127 beacon_payload,
128 broadcast_address,
129 })
130 }
131
132 /// Enter an infinite loop sending `Beacon`s periodically
133 pub fn send_loop(&self, period: Duration) -> io::Result<()> {
134 loop {
135 self.send_one_beacon()?;
136 std::thread::sleep(period);
137 }
138 }
139
140 /// Send a single `Beacon` out
141 pub fn send_one_beacon(&self) -> io::Result<usize> {
142 trace!("Sending Beacon '{}' to: '{}'", String::from_utf8_lossy(&self.beacon_payload[4..]),
143 self.broadcast_address);
144 self.socket.send_to(&self.beacon_payload, &self.broadcast_address)
145 }
146}
147
148/// `Beacon` contains information about the beacon that was received by a `BeaconListener`
149pub struct Beacon {
150 /// The IP address and port the beacon was sent from
151 pub service_ip: String,
152 /// The port the service is running on
153 pub service_port: u16,
154 /// The name of the service sending the beacon
155 pub service_name: Vec<u8>
156}
157
158impl std::fmt::Display for Beacon {
159 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
160 let service_name = String::from_utf8(self.service_name.clone()).unwrap_or_else(|_| "Invalid UTF-8 String".into());
161 write!(f, "ServiceName: '{}', Service IP: {}, Service Port: {}", service_name, self.service_ip, self.service_port)
162 }
163}
164
165/// `BeaconListener` listens for new `Beacons` on the specified port
166///
167/// # Example of using `BeaconListener` with timeout
168/// ```
169/// use simpdiscoverylib::BeaconListener;
170/// use std::time::Duration;
171/// use portpicker::pick_unused_port;
172///
173/// let listening_port = pick_unused_port().expect("Could not get a free port to listen on");
174/// let listener = BeaconListener::new("_my_service._tcp.local".as_bytes(), listening_port)
175/// .expect("Could not create listener");
176///
177/// // Avoid blocking tests by setting a short timeout, expect an error, as there is no sender setup
178/// assert!(listener.wait(Some(Duration::from_millis(1))).is_err());
179/// ```
180pub struct BeaconListener {
181 socket: UdpSocket,
182 service_name: Vec<u8>,
183}
184
185impl BeaconListener {
186 /// Create a new `BeaconListener` on `port` with an option `filter` to be applied to incoming
187 /// beacons. This binds to address "0.0.0.0:listening_port"
188 pub fn new(service_name: &[u8], listening_port: u16) -> io::Result<Self> {
189 let listening_address = format!("{}:{}", LISTENING_ADDRESS, listening_port);
190 let socket = UdpSocket::bind(&listening_address)
191 .map_err(|e|
192 io::Error::new(io::ErrorKind::AddrInUse,
193 format!("SimpDiscover::BeaconListener could not bind to UdpSocket at {listening_address} ({e})")))?;
194 trace!("Socket bound to: {}", listening_address);
195 socket.set_broadcast(true)?;
196
197 Ok(Self {
198 socket,
199 service_name: service_name.to_vec(),
200 })
201 }
202
203 /// Wait for a `Beacon` on the port specified in `BeaconListener::new()`
204 /// If `timeout` is None, then it will block forever waiting for a beacon matching the optional
205 /// filter (if supplied) in `BeaconListener::new()`. If no `filter` was supplied it will block
206 /// waiting for any beacon to be received.
207 ///
208 /// If `timeout` is `Some(Duration)` then it will block for that duration on the reception of
209 /// each beacon. If the beacon does not match a supplied `filter` then it will loop (blocking
210 /// for `duration` each time until a matching beacon is found.
211 pub fn wait(&self, timeout: Option<Duration>) -> io::Result<Beacon> {
212 self.socket.set_read_timeout(timeout)?;
213 info!("Read timeout set to: {:?}", timeout);
214
215 info!("Waiting for beacon matching '{}'", String::from_utf8_lossy(&self.service_name));
216 loop {
217 let beacon = self.receive_one_beacon()?;
218
219 if beacon.service_name == self.service_name {
220 trace!("Beacon '{}' matches filter '{}': returning beacon",
221 String::from_utf8_lossy(&beacon.service_name), String::from_utf8_lossy(&self.service_name));
222 return Ok(beacon);
223 } else {
224 trace!("Beacon '{}' does not match filter '{}': ignoring",
225 String::from_utf8_lossy(&beacon.service_name), String::from_utf8_lossy(&self.service_name));
226 }
227 }
228 }
229
230 /*
231 Receive one beacon
232 */
233 fn receive_one_beacon(&self) -> io::Result<Beacon> {
234 let mut buffer = [0; MAX_INCOMING_BEACON_SIZE];
235
236 loop {
237 let (number_of_bytes, source_address) = self.socket.recv_from(&mut buffer)?;
238 let magic_number = array_of_u8_to_u16(&buffer[0..2]);
239 if magic_number == MAGIC_NUMBER {
240 let service_port = array_of_u8_to_u16(&buffer[2..4]);
241 let service_name = buffer[4..number_of_bytes].to_vec();
242
243 return Ok(Beacon {
244 service_ip: source_address.ip().to_string(),
245 service_port,
246 service_name
247 });
248 }
249 }
250 }
251}