cat_dev/mion/
discovery.rs

1//! APIs for discovering Cat-Dev Bridge's, and more specifically their MION
2//! boards.
3//!
4//! There are two main groups of methods for attempting to find MIONs:
5//!
6//! 1. [`discover_bridges`], [`discover_bridges_with_logging_hooks`],
7//!    [`discover_and_collect_bridges`], and
8//!    [`discover_and_collect_bridges_with_logging_hooks`] incase you
9//!    want to output values as you discover mions (processing them in a
10//!    stream), or if you want to collect all the values in a single vector
11//!    at the very end.
12//! 2. [`find_mion`] which finds a specific MION board based on one of the
13//!    identifiers we know how to search for. *NOTE: in some cases this can
14//!    lead to a full scan. See the API information for details.*
15//!
16//! It should be noted you can only find bridges that are on the same broadcast
17//! domain within your local network. In general this means under the same
18//! Subnet, and VLAN (unless your repeating broadcast packets across VLANs).
19//!
20//! If you are in different VLANs/Subnets and you do have the ability to run
21//! a repeater heading in BOTH directions (both are required for all bits of
22//! functionality!), you want to broadcast the port
23//! [`crate::mion::proto::DEFAULT_MION_CONTROL_PORT`] aka 7974. Otherwise things
24//! will not work. You may also need to broadcast whatever your configured ATAPI
25//! port is (by default this is also 7974, so not a worry.)
26
27use crate::{
28	errors::{CatBridgeError, NetworkError},
29	mion::proto::{
30		control::{MionIdentity, MionIdentityAnnouncement},
31		DEFAULT_MION_CONTROL_PORT, MION_ANNOUNCE_TIMEOUT_SECONDS,
32	},
33};
34use bytes::{Bytes, BytesMut};
35use fnv::FnvHashSet;
36use futures::stream::{unfold, StreamExt};
37use mac_address::MacAddress;
38use network_interface::{Addr, NetworkInterface, NetworkInterfaceConfig};
39use std::{
40	fmt::{Display, Formatter, Result as FmtResult},
41	hash::BuildHasherDefault,
42	net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4},
43};
44use tokio::{
45	net::UdpSocket,
46	sync::mpsc::{unbounded_channel, UnboundedReceiver},
47	task::JoinSet,
48	time::{sleep, Duration, Instant},
49};
50use tracing::{debug, error, warn};
51
52/// A small wrapper around [`discover_bridges`] that collects all the results
53/// into a list for you to parse through.
54///
55/// This will in general be slower than the `findbridge` cli tool, or even
56/// `bridgectl` because it will attempt to wait for the maximum amount of time
57/// in order to let any slow MIONs respond. Where as `bridgectl` (by default),
58/// and `findbridge` will attempt to exit early if they're not seeing a lot of
59/// responses on the network. To replicate their speed, and behaviour you can
60/// pass an early timeout of 3 seconds.
61///
62/// *note: you probably do not want to set `control_port`, we have not seen
63/// a mion respond on a separate port to this day, but certain tools do try
64/// to query other ports (We believe it's an unintentional bug, however, we
65/// expose it, just incase).
66///
67/// ## Errors
68///
69/// See the error notes for [`discover_bridges`].
70pub async fn discover_and_collect_bridges(
71	fetch_detailed_info: bool,
72	early_timeout: Option<Duration>,
73	override_control_port: Option<u16>,
74) -> Result<Vec<MionIdentity>, CatBridgeError> {
75	discover_and_collect_bridges_with_logging_hooks(
76		fetch_detailed_info,
77		early_timeout,
78		override_control_port,
79		noop_logger_interface,
80	)
81	.await
82}
83
84/// A small wrapper around [`discover_bridges`] that collects all the results
85/// into a list for you to parse through with extra logging handlers.
86///
87/// You ***probably*** don't want to call this directly, and instead call
88/// [`discover_bridges`] this can mainly be used for folks who need to create
89/// CLI tools with hyper-specific `println!`'s that don't use the normal
90/// [`tracing`] crate, or need some custom hooks to process data.
91///
92/// This will in general be slower than the `findbridge` cli tool, or even
93/// `bridgectl` because it will attempt to wait for the maximum amount of time
94/// in order to let any slow MIONs respond. Where as `bridgectl` (by default),
95/// and `findbridge` will attempt to exit early if they're not seeing a lot of
96/// responses on the network. To replicate their speed, and behaviour you can
97/// pass an early timeout of 3 seconds.
98///
99/// *note: you probably do not want to set `control_port`, we have not seen
100/// a mion respond on a separate port to this day, but certain tools do try
101/// to query other ports (We believe it's an unintentional bug, however, we
102/// expose it, just incase).
103///
104/// ## Errors
105///
106/// See the error notes for [`discover_bridges`].
107pub async fn discover_and_collect_bridges_with_logging_hooks<InterfaceLoggingHook>(
108	fetch_detailed_info: bool,
109	early_timeout: Option<Duration>,
110	override_control_port: Option<u16>,
111	interface_logging_hook: InterfaceLoggingHook,
112) -> Result<Vec<MionIdentity>, CatBridgeError>
113where
114	InterfaceLoggingHook: Fn(&'_ Addr) + Clone + Send + 'static,
115{
116	let mut recv_channel = discover_bridges_with_logging_hooks(
117		fetch_detailed_info,
118		override_control_port,
119		interface_logging_hook,
120	)
121	.await?;
122
123	let mut results = Vec::new();
124	loop {
125		tokio::select! {
126			opt = recv_channel.recv() => {
127				let Some(identity) = opt else {
128					// No more identities being received.
129					break;
130				};
131				// The same identity could be broadcast multiple times!
132				if !results.contains(&identity) {
133					results.push(identity);
134				}
135			}
136			() = sleep(early_timeout.unwrap_or(Duration::from_secs(MION_ANNOUNCE_TIMEOUT_SECONDS * 2))) => {
137				break;
138			}
139		}
140	}
141	Ok(results)
142}
143
144/// Discover all the Cat-Dev Bridges actively on the network.
145///
146/// NOTE: This will only find MIONs within the time window of
147///   [`crate::mion::proto::MION_ANNOUNCE_TIMEOUT_SECONDS`].
148///   To stop scanning for broadcasts early, simply close the receiving end of
149///   the channel.
150///
151/// This is what most users will actually want to mess with, as it simply logs
152/// using tracing, and returns the stream, AS devices are discovered. You might
153/// also want to use the api: [`discover_and_collect_bridges`]. Which handles
154/// all the scanning for you for 10 seconds, and then gives you the full list
155/// of discovered MIONs. Which also accepts an optional early timeout so you
156/// don't gotta wait the full seconds if you know you're not having some
157/// respond slowly.
158///
159/// *note: if you have multiple interfaces on the same network it is possible
160/// with this function to receive the same interface multiple times. you should
161/// handle any de-duping on your side!*
162///
163/// There are also two sister functions [`discover_bridges_with_logging_hooks`]
164/// and [`discover_and_collect_bridges_with_logging_hooks`]. Which are used by
165/// the command line tool `findbridge` in order to match the output of the
166/// original tools EXACTLY. For most users you probably don't want those
167/// logging hooks, as they ALREADY get piped through [`tracing`].
168///
169/// *note: you probably do not want to set `control_port`, we have not seen
170/// a mion respond on a separate port to this day, but certain tools do try
171/// to query other ports (We believe it's an unintentional bug, however, we
172/// expose it, just incase).
173///
174/// ## Errors
175///
176/// - If we fail to spawn a task to concurrently look up the MIONs.
177/// - If any background-task fails to create a socket, and broadcast on that
178///   socket.
179///
180/// They will also silently ignore any interfaces that are not up, if there is
181/// no IPv4 Address on the NIC, if we receive a packet from an IPv6 address, or
182/// finally if the broadcast packet is not a MION Identity response.
183pub async fn discover_bridges(
184	fetch_detailed_info: bool,
185	override_control_port: Option<u16>,
186) -> Result<UnboundedReceiver<MionIdentity>, CatBridgeError> {
187	discover_bridges_with_logging_hooks(
188		fetch_detailed_info,
189		override_control_port,
190		noop_logger_interface,
191	)
192	.await
193}
194
195/// Discover all the Cat-Dev Bridges actively on the network.
196///
197/// This is the function that allows you to specify EXTRA logging hooks (e.g.
198/// those that aren't written to [`tracing`], for like when you need to manually
199/// recreate a CLI with old hacky `println!`).
200///
201/// You probably want [`discover_bridges`].
202///
203/// *note: you probably do not want to set `control_port`, we have not seen
204/// a mion respond on a separate port to this day, but certain tools do try
205/// to query other ports (We believe it's an unintentional bug, however, we
206/// expose it, just incase).
207///
208/// ## Errors
209///
210/// See the error notes for [`discover_bridges`].
211pub async fn discover_bridges_with_logging_hooks<InterfaceLoggingHook>(
212	fetch_detailed_info: bool,
213	override_control_port: Option<u16>,
214	interface_logging_hook: InterfaceLoggingHook,
215) -> Result<UnboundedReceiver<MionIdentity>, CatBridgeError>
216where
217	InterfaceLoggingHook: Fn(&'_ Addr) + Clone + Send + 'static,
218{
219	let to_broadcast = Bytes::from(MionIdentityAnnouncement::new(fetch_detailed_info));
220	let mut tasks = JoinSet::new();
221
222	for (interface_addr, interface_ipv4) in get_all_broadcast_addresses()? {
223		let broadcast_messaged_cloned = to_broadcast.clone();
224		let cloned_iface_hook = interface_logging_hook.clone();
225		tasks
226			.build_task()
227			.name(&format!("cat_dev::discover_mion::{interface_ipv4}"))
228			.spawn(async move {
229				broadcast_to_mions_on_interface(
230					override_control_port,
231					broadcast_messaged_cloned,
232					interface_addr,
233					interface_ipv4,
234					cloned_iface_hook,
235				)
236				.await
237			})
238			.map_err(CatBridgeError::SpawnFailure)?;
239	}
240
241	let mut listening_sockets = Vec::with_capacity(tasks.len());
242	while let Some(joined) = tasks.join_next().await {
243		let joined_result = match joined {
244			Ok(data) => data,
245			Err(cause) => {
246				tasks.abort_all();
247				return Err(CatBridgeError::JoinFailure(cause));
248			}
249		};
250		let mut opt_socket = match joined_result {
251			Ok(optional_socket) => optional_socket,
252			Err(cause) => {
253				tasks.abort_all();
254				return Err(cause.into());
255			}
256		};
257		if let Some(socket) = opt_socket.take() {
258			listening_sockets.push(socket);
259		}
260	}
261
262	let mut our_addresses = FnvHashSet::with_capacity_and_hasher(
263		listening_sockets.len(),
264		BuildHasherDefault::default(),
265	);
266	for sock in &listening_sockets {
267		if let Ok(our_addr) = sock.local_addr() {
268			our_addresses.insert(our_addr.ip());
269		}
270	}
271
272	let streams = listening_sockets
273		.into_iter()
274		.map(|socket| Box::pin(unfold(socket, unfold_socket)))
275		.collect::<Vec<_>>();
276	// Combine every single socket receive into a single receive stream.
277	let mut single_stream = futures::stream::select_all(streams);
278	let timeout_at = Instant::now() + Duration::from_secs(MION_ANNOUNCE_TIMEOUT_SECONDS);
279	let (send, recv) = unbounded_channel::<MionIdentity>();
280
281	tokio::task::spawn(async move {
282		loop {
283			tokio::select! {
284				opt = single_stream.next() => {
285					let Some((read_data_len, from, mut buff)) = opt else {
286						continue;
287					};
288					buff.truncate(read_data_len);
289					let frozen = buff.freeze();
290
291					let from_ip = from.ip();
292					if our_addresses.contains(&from_ip) {
293						debug!("broadcast saw our own message");
294						continue;
295					}
296					let ip_address = match from_ip {
297						IpAddr::V4(v4) => v4,
298						IpAddr::V6(v6) => {
299							debug!(%v6, "broadcast packet from IPv6, ignoring, can't be announcement");
300							continue;
301						},
302					};
303
304					let Ok(identity) = MionIdentity::try_from((ip_address, frozen.clone())) else {
305						warn!(%from, packet = %format!("{frozen:02x?}"), "could not parse packet from MION");
306						continue;
307					};
308					if let Err(_closed) = send.send(identity) {
309						break;
310					}
311				}
312				() = tokio::time::sleep_until(timeout_at) => {
313					break;
314				}
315			}
316		}
317	});
318
319	Ok(recv)
320}
321
322/// Attempt to find a specific MION by searching for a specific field.
323///
324/// This _may_ cause a full discovery search to run, or may send a direct
325/// packet to the device itself.
326///
327/// *note: you probably do not want to set `control_port`, we have not seen
328/// a mion respond on a separate port to this day, but certain tools do try
329/// to query other ports (We believe it's an unintentional bug, however, we
330/// expose it, just incase).
331///
332/// ## Errors
333///
334/// - If we fail to spawn a task to concurrently look up the MIONs, and we need
335///   to do a full discovery search.
336/// - If any task fails to create a socket, and broadcast on that socket.
337pub async fn find_mion(
338	find_by: MIONFindBy,
339	find_detailed: bool,
340	early_scan_timeout: Option<Duration>,
341	override_control_port: Option<u16>,
342) -> Result<Option<MionIdentity>, CatBridgeError> {
343	find_mion_with_logging_hooks(
344		find_by,
345		find_detailed,
346		early_scan_timeout,
347		override_control_port,
348		noop_logger_interface,
349	)
350	.await
351}
352
353/// Attempt to find a specific MION by searching for a specific field.
354///
355/// This _may_ cause a full discovery search to run, or may send a packet
356/// directly to the device itself.
357///
358/// You probably want [`find_mion`] without logging hooks. Again logs still get
359/// generated through the [`tracing`] crate. This is purely for those who need some
360/// extra manual logging, say because you're implementing a broken CLI.
361///
362/// It should also be noted YOU MAY NOT get logging callbacks, if we don't
363/// need to do a full scan. You can call [`MIONFindBy::will_cause_full_scan`]
364/// in order to determine if you'll get logging callbacks.
365///
366/// *note: you probably do not want to set `control_port`, we have not seen
367/// a mion respond on a separate port to this day, but certain tools do try
368/// to query other ports (We believe it's an unintentional bug, however, we
369/// expose it, just incase).
370///
371/// ## Errors
372///
373/// - If we fail to spawn a task to concurrently look up the MIONs, and we need
374///   to do a full discovery search.
375/// - If any task fails to create a socket, and broadcast on that socket.
376pub async fn find_mion_with_logging_hooks<InterfaceLoggingHook>(
377	find_by: MIONFindBy,
378	find_detailed_info: bool,
379	early_scan_timeout: Option<Duration>,
380	override_control_port: Option<u16>,
381	interface_logging_hook: InterfaceLoggingHook,
382) -> Result<Option<MionIdentity>, CatBridgeError>
383where
384	InterfaceLoggingHook: Fn(&'_ Addr) + Clone + Send + 'static,
385{
386	let port = override_control_port.unwrap_or(DEFAULT_MION_CONTROL_PORT);
387	let (find_by_mac, find_by_name) = match find_by {
388		MIONFindBy::Ip(ipv4) => {
389			let local_socket = UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port))
390				.await
391				.map_err(|_| NetworkError::BindFailure)?;
392			local_socket
393				.connect(SocketAddrV4::new(ipv4, port))
394				.await
395				.map_err(NetworkError::IO)?;
396			local_socket
397				.send(&Bytes::from(MionIdentityAnnouncement::new(
398					find_detailed_info,
399				)))
400				.await
401				.map_err(NetworkError::IO)?;
402
403			let mut buff = BytesMut::zeroed(8192);
404			tokio::select! {
405				result = local_socket.recv(&mut buff) => {
406					let actual_size = result.map_err(NetworkError::IO)?;
407					buff.truncate(actual_size);
408				}
409				() = sleep(Duration::from_secs(MION_ANNOUNCE_TIMEOUT_SECONDS)) => {
410					return Ok(None);
411				}
412			}
413			return Ok(Some(MionIdentity::try_from((ipv4, buff.freeze()))?));
414		}
415		MIONFindBy::MacAddress(mac) => (Some(mac), None),
416		MIONFindBy::Name(name) => (None, Some(name)),
417	};
418
419	let mut recv_channel = discover_bridges_with_logging_hooks(
420		find_detailed_info,
421		override_control_port,
422		interface_logging_hook,
423	)
424	.await?;
425	loop {
426		tokio::select! {
427			opt = recv_channel.recv() => {
428				let Some(identity) = opt else {
429					// No more identities being received.
430					break;
431				};
432
433				if let Some(filter_mac) = find_by_mac.as_ref() {
434					if *filter_mac == identity.mac_address() {
435						return Ok(Some(identity));
436					}
437				}
438				if let Some(filter_name) = find_by_name.as_ref() {
439					if filter_name == identity.name() {
440						return Ok(Some(identity));
441					}
442				}
443			}
444			() = sleep(early_scan_timeout.unwrap_or(Duration::from_secs(MION_ANNOUNCE_TIMEOUT_SECONDS * 2))) => {
445				break;
446			}
447		}
448	}
449
450	Ok(None)
451}
452
453/// A way to search for a single MION board.
454///
455/// Some of these can end up causing a full discovery broadcast, some of them
456/// cause just a single packet to a single ip address. You can parse these from
457/// a string or from one of the associated types.
458#[derive(Clone, Debug, Eq, Hash, PartialEq)]
459pub enum MIONFindBy {
460	/// Search by a specific IP Address.
461	///
462	/// The IP Address has to be a V4 address, as the MIONs do not actually
463	/// support being on an IPv6 address, and using `DHCPv6`.
464	///
465	/// This searching type will only send a specific request to the specific
466	/// IPv4 address that you've specified here. IT WILL NOT cause a full
467	/// broadcast to happen.
468	Ip(Ipv4Addr),
469	/// Search by a mac address coming from a specific device.
470	///
471	/// This searching type will cause a FULL Broadcast to happen. Meaning we
472	/// will receive potentially many mac addresses that we have to ignore. We
473	/// could in theory avoid this by using RARP (aka reverse arp) requests.
474	/// However, that requires running as an administrator on many OS's to issue
475	/// full RARP's requests. In theory we could parse things like
476	/// `/proc/net/arp`, but that requires doing things like sending
477	/// pings/broadcasts first which isn't always possible, especially because we
478	/// don't know the IP Address before hand.
479	///
480	/// Maybe one day it would be possible to use RARP requests.
481	MacAddress(MacAddress),
482	/// Search by the name of a Cat-Dev Bridge.
483	///
484	/// This searching type will cause a FULL Broadcast to happen. Meaning we
485	/// will receive potentially many broadcast responses that we might have to
486	/// ignore. There isn't really a way to avoid this without keeping a cache
487	/// somewhere. In theory a user could still this, and just pass in find by
488	/// ip with their own cache.
489	Name(String),
490}
491impl MIONFindBy {
492	/// Techincally the name can collide with a mac address, and even techincally
493	/// an IP.
494	///
495	/// To help provide similar APIs to the CLIs we offer
496	/// `MIONFindBy::Name(value)`, and `MIONFindBy::from_name_or_ip(value)`,
497	/// and finally `MIONFindBy::from(value)` to let you choose which collisions
498	/// if any you're okay with.
499	#[must_use]
500	pub fn from_name_or_ip(value: String) -> Self {
501		if let Ok(ipv4) = value.as_str().parse::<Ipv4Addr>() {
502			Self::Ip(ipv4)
503		} else {
504			Self::Name(value)
505		}
506	}
507
508	/// Determine if the scanning method you're actively using will cause a full
509	/// scan of the network.
510	#[must_use]
511	pub const fn will_cause_full_scan(&self) -> bool {
512		match self {
513			Self::Ip(ref _ip) => false,
514			Self::MacAddress(ref _mac) => true,
515			Self::Name(ref _name) => true,
516		}
517	}
518}
519impl From<String> for MIONFindBy {
520	fn from(value: String) -> Self {
521		// First we try parsing a mac address, then an ipv4, before we just give up
522		// and use a name.
523		if let Ok(mac) = MacAddress::try_from(value.as_str()) {
524			Self::MacAddress(mac)
525		} else {
526			Self::from_name_or_ip(value)
527		}
528	}
529}
530impl Display for MIONFindBy {
531	fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
532		match self {
533			Self::Ip(ref ip) => write!(fmt, "{ip}"),
534			Self::MacAddress(ref mac) => write!(fmt, "{mac}"),
535			Self::Name(ref name) => write!(fmt, "{name}"),
536		}
537	}
538}
539
540/// Get a list of all the network interfaces to actively scanning on.
541///
542/// NOTE: this doesn't actually fetch all the broadcast addresses, just the
543/// potential ones we MIGHT be able to scan on. This is specifically required
544/// for implementing the broken behavior of `findbridge`, which DOES NOT
545/// actually ensure that a broadcast address could be made at all.
546///
547/// Thanks `findbridge`.
548///
549/// ## Errors
550///
551/// - If we cannot list all the network interfaces present on the system.
552pub fn get_all_broadcast_addresses() -> Result<Vec<(Addr, Ipv4Addr)>, NetworkError> {
553	Ok(NetworkInterface::show()
554		.map_err(|cause| {
555			error!(?cause, "could not list network interfaces on this device");
556			NetworkError::ListInterfacesFailure(cause)
557		})?
558		.into_iter()
559		.fold(Vec::<(Addr, Ipv4Addr)>::new(), |mut accum, iface| {
560			for local_address in &iface.addr {
561				let ip = match local_address.ip() {
562					IpAddr::V4(ref v4) => {
563						if !v4.is_private() && !v4.is_link_local() {
564							debug!(?iface, ?local_address, "will not broadcast to public ips");
565							continue;
566						}
567
568						*v4
569					}
570					IpAddr::V6(_) => {
571						debug!(?iface, ?local_address, "cannot broadcast to IPv6 addresses");
572						continue;
573					}
574				};
575
576				accum.push((*local_address, ip));
577			}
578
579			accum
580		}))
581}
582
583/// Broadcast to all the MIONs on a particular network interface.
584///
585/// This doesn't actually read the values (we want to queue up all the reads
586/// so we can read from them all concurrently with a timeout that applies to
587/// all of them).
588async fn broadcast_to_mions_on_interface<InterfaceLoggingHook>(
589	override_control_port: Option<u16>,
590	body_to_broadcast: Bytes,
591	interface_addr: Addr,
592	interface_ipv4: Ipv4Addr,
593	interface_hook: InterfaceLoggingHook,
594) -> Result<Option<UdpSocket>, NetworkError>
595where
596	InterfaceLoggingHook: Fn(&'_ Addr),
597{
598	// Nintendo just blindly prints this even if there is no broadcast address
599	// and IT WILL fail.
600	interface_hook(&interface_addr);
601	let Some(broadcast_address) = interface_addr.broadcast() else {
602		debug!(
603			?interface_addr,
604			?interface_ipv4,
605			"failed to get broadcast address"
606		);
607		return Ok(None);
608	};
609
610	debug!(
611		?interface_addr,
612		?interface_ipv4,
613		"actually broadcasting to interface"
614	);
615
616	let local_socket = UdpSocket::bind(SocketAddr::V4(SocketAddrV4::new(
617		interface_ipv4,
618		override_control_port.unwrap_or(DEFAULT_MION_CONTROL_PORT),
619	)))
620	.await
621	.map_err(|_| NetworkError::BindFailure)?;
622	local_socket
623		.set_broadcast(true)
624		.map_err(|_| NetworkError::SetBroadcastFailure)?;
625	local_socket
626		.send_to(
627			&body_to_broadcast,
628			SocketAddr::new(
629				broadcast_address,
630				override_control_port.unwrap_or(DEFAULT_MION_CONTROL_PORT),
631			),
632		)
633		.await
634		.map_err(NetworkError::IO)?;
635	Ok(Some(local_socket))
636}
637
638/// Unfold sockets goal is to turn reading from a socket over & over into a
639/// stream.
640///
641/// When the stream has produced a value, and gets polled again,
642/// it queues up another read, and so on.
643async fn unfold_socket(sock: UdpSocket) -> Option<((usize, SocketAddr, BytesMut), UdpSocket)> {
644	let mut buff = BytesMut::zeroed(1024);
645	let Ok((len, addr)) = sock.recv_from(&mut buff).await else {
646		warn!("failed to receive data from broadcast socket");
647		return None;
648	};
649	Some(((len, addr, buff), sock))
650}
651
652/// A logger to use when we don't have another logger passed in.
653#[inline]
654fn noop_logger_interface(_: &Addr) {}
655
656#[cfg(test)]
657mod unit_tests {
658	use super::*;
659
660	#[test]
661	pub fn can_list_at_least_one_interface() {
662		assert!(
663			!get_all_broadcast_addresses().expect("Failed to list all broadcast addresses!").is_empty(),
664			"Failed to list all broadcast addresses... for some reason your PC isn't compatible to scan devices... perhaps you don't have a private IPv4 address?",
665		);
666	}
667
668	/// Although we can't actually scan for a real device, as not everyone will
669	/// have that device on their network.
670	///
671	/// However, we can scan for a device that we know is guaranteed to not
672	/// exist, so we look for a device with a name that is non-ascii, as device
673	/// names have to be ascii.
674	#[tokio::test]
675	pub async fn cant_find_nonexisting_device() {
676		assert!(
677			find_mion(MIONFindBy::Name("𩸽".to_owned()), false, None, None)
678				.await
679				.expect("Failed to scan to find a specific mion")
680				.is_none(),
681			"Somehow found a MION that can't exist?"
682		);
683		assert!(
684			find_mion(MIONFindBy::Name("𩸽".to_owned()), true, None, None)
685				.await
686				.expect("Failed to scan to find a specific mion")
687				.is_none(),
688			"Somehow found a MION that can't exist?"
689		);
690		assert!(
691			find_mion(
692				MIONFindBy::Name("𩸽".to_owned()),
693				true,
694				Some(Duration::from_secs(3)),
695				None,
696			)
697			.await
698			.expect("Failed to scan to find a specific mion")
699			.is_none(),
700			"Somehow found a MION that can't exist?"
701		);
702	}
703}