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}