#![doc = include_str!("../README.md")]
mod guardian;
mod receiver;
mod sender;
mod socket;
mod updater;
use acto::{AcTokio, ActoHandle, ActoRef, ActoRuntime, SupervisionRef, TokioJoinHandle};
use hickory_proto::rr::Name;
use socket::{SocketError, Sockets};
use std::{
collections::BTreeMap,
fmt::Display,
net::{IpAddr, Ipv4Addr},
str::FromStr,
time::{Duration, Instant},
};
use thiserror::Error;
use tokio::runtime::Handle;
type Callback = Box<dyn FnMut(&str, &Peer) + Send + 'static>;
pub(crate) type TxtData = BTreeMap<String, Option<String>>;
#[derive(Debug, Error)]
pub enum SpawnError {
#[error(transparent)]
Sockets {
#[from]
source: SocketError,
},
#[error("Cannot construct service name from name '{name}' and protocol '{protocol}'")]
ServiceName {
#[source]
source: hickory_proto::ProtoError,
name: String,
protocol: Protocol,
},
#[error("Cannot construct name from peer ID {peer_id}")]
NameFromPeerId {
#[source]
source: hickory_proto::ProtoError,
peer_id: String,
},
#[error("Cannot append service name '{service_name}' to peer ID")]
AppendServiceName {
#[source]
source: hickory_proto::ProtoError,
service_name: Name,
},
}
#[derive(Debug, Error)]
pub enum TxtAttributeError {
#[error("Key may not be empty")]
EmptyKey,
#[error("Key-value pair is too long, must be shorter than 254 bytes")]
TooLong,
}
pub struct Discoverer {
name: String,
protocol: Protocol,
peer_id: String,
peers: BTreeMap<String, Peer>,
callback: Callback,
tau: Duration,
phi: f32,
class: IpClass,
multicast_interfaces: Vec<Ipv4Addr>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Peer {
addrs: Vec<(IpAddr, u16)>,
last_seen: Instant,
txt: TxtData,
}
impl Peer {
pub(crate) fn new() -> Self {
Peer {
addrs: Default::default(),
last_seen: Instant::now(),
txt: Default::default(),
}
}
pub fn addrs(&self) -> &[(IpAddr, u16)] {
&self.addrs
}
pub fn is_expiry(&self) -> bool {
self.addrs.is_empty()
}
pub fn age(&self) -> Duration {
self.last_seen.elapsed()
}
pub fn txt_attributes(&self) -> impl Iterator<Item = (&str, Option<&str>)> + '_ {
self.txt
.iter()
.map(|(k, v)| (k.as_str(), v.as_ref().map(|v| v.as_str())))
}
pub fn txt_attribute(&self, name: &str) -> Option<Option<&str>> {
self.txt.get(name).map(|x| x.as_deref())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum IpClass {
V4Only,
V6Only,
V4AndV6,
#[default]
Auto,
}
impl IpClass {
pub fn has_v4(self) -> bool {
matches!(self, Self::V4Only | Self::V4AndV6)
}
pub fn has_v6(self) -> bool {
matches!(self, Self::V6Only | Self::V4AndV6)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Protocol {
#[default]
Udp,
Tcp,
}
impl Display for Protocol {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Protocol::Udp => write!(f, "_udp"),
Protocol::Tcp => write!(f, "_tcp"),
}
}
}
impl Discoverer {
pub fn new(name: String, peer_id: String) -> Self {
Self {
name,
protocol: Protocol::default(),
peer_id,
peers: BTreeMap::new(),
callback: Box::new(|_, _| {}),
tau: Duration::from_secs(10),
phi: 1.0,
class: IpClass::default(),
multicast_interfaces: Vec::new(),
}
}
pub fn new_interactive(name: String, peer_id: String) -> Self {
Self::new(name, peer_id)
.with_cadence(Duration::from_millis(700))
.with_response_rate(2.5)
}
pub fn with_protocol(mut self, protocol: Protocol) -> Self {
self.protocol = protocol;
self
}
pub fn with_addrs(mut self, port: u16, addrs: impl IntoIterator<Item = IpAddr>) -> Self {
let me = self
.peers
.entry(self.peer_id.clone())
.or_insert_with(Peer::new);
me.addrs.extend(addrs.into_iter().map(|addr| (addr, port)));
me.addrs.sort_unstable();
me.addrs.dedup();
self
}
pub fn with_txt_attributes(
mut self,
attributes: impl IntoIterator<Item = (String, Option<String>)>,
) -> Result<Self, TxtAttributeError> {
let me = self
.peers
.entry(self.peer_id.clone())
.or_insert_with(Peer::new);
for (key, value) in attributes.into_iter() {
validate_txt_attribute(&key, value.as_deref())?;
me.txt.insert(key, value);
}
Ok(self)
}
pub fn with_callback(mut self, callback: impl FnMut(&str, &Peer) + Send + 'static) -> Self {
self.callback = Box::new(callback);
self
}
pub fn with_cadence(mut self, tau: Duration) -> Self {
self.tau = tau;
self
}
pub fn with_response_rate(mut self, phi: f32) -> Self {
self.phi = phi;
self
}
pub fn with_ip_class(mut self, class: IpClass) -> Self {
self.class = class;
self
}
pub fn with_multicast_interfaces_v4(mut self, interfaces: Vec<Ipv4Addr>) -> Self {
self.multicast_interfaces = interfaces;
self
}
pub fn spawn(self, handle: &Handle) -> Result<DropGuard, SpawnError> {
let _entered = handle.enter();
let sockets = Sockets::new(self.class, self.multicast_interfaces.clone())?;
tracing::trace!(?sockets, "created new sockets");
let service_name = Name::from_str(&format!("_{}.{}.local.", self.name, self.protocol))
.map_err(|source| SpawnError::ServiceName {
source,
name: self.name.clone(),
protocol: self.protocol,
})?;
Name::from_str(&self.peer_id)
.map_err(|source| SpawnError::NameFromPeerId {
source,
peer_id: self.peer_id.clone(),
})?
.append_domain(&service_name)
.map_err(|source| SpawnError::AppendServiceName {
source,
service_name: service_name.clone(),
})?;
let rt = AcTokio::from_handle("swarm-discovery", handle.clone());
let SupervisionRef { me, handle } = rt.spawn_actor("guardian", move |ctx| {
guardian::guardian(ctx, self, sockets, service_name)
});
Ok(DropGuard {
task: Some(handle),
aref: me,
_rt: rt,
})
}
}
#[must_use = "dropping this value will stop the mDNS discovery"]
pub struct DropGuard {
task: Option<TokioJoinHandle<()>>,
aref: ActoRef<guardian::Input>,
_rt: AcTokio,
}
impl DropGuard {
pub fn remove_all(&self) {
self.aref.send(guardian::Input::RemoveAll);
}
pub fn remove_port(&self, port: u16) {
self.aref.send(guardian::Input::RemovePort(port));
}
pub fn remove_addr(&self, addr: IpAddr) {
self.aref.send(guardian::Input::RemoveAddr(addr));
}
pub fn add(&self, port: u16, addrs: Vec<IpAddr>) {
self.aref.send(guardian::Input::AddAddr(port, addrs));
}
pub fn set_txt_attribute(
&self,
key: String,
value: Option<String>,
) -> Result<(), TxtAttributeError> {
validate_txt_attribute(&key, value.as_deref())?;
self.aref.send(guardian::Input::SetTxt(key, value));
Ok(())
}
pub fn remove_txt_attribute(&self, key: String) {
self.aref.send(guardian::Input::RemoveTxt(key));
}
pub fn add_interface_v4(&self, interface: Ipv4Addr) {
self.aref
.send(guardian::Input::AddInterface(IpAddr::V4(interface)));
}
pub fn remove_interface_v4(&self, interface: Ipv4Addr) {
self.aref
.send(guardian::Input::RemoveInterface(IpAddr::V4(interface)));
}
}
impl Drop for DropGuard {
fn drop(&mut self) {
self.task.take().unwrap().abort();
}
}
fn validate_txt_attribute(key: &str, value: Option<&str>) -> Result<(), TxtAttributeError> {
if key.is_empty() {
Err(TxtAttributeError::EmptyKey)
} else if key.len() + value.as_ref().map(|v| v.len()).unwrap_or_default() > 254 {
Err(TxtAttributeError::TooLong)
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{Ipv4Addr, Ipv6Addr};
use tokio::sync::mpsc;
#[tokio::test]
async fn test_change_addresses() {
let handle = tokio::runtime::Handle::current();
let peer_id1 = "test_peer1".to_string();
let peer_id2 = "test_peer2".to_string();
let (tx, mut rx) = mpsc::channel(10);
let discoverer1 = Discoverer::new("test_service".to_string(), peer_id1.clone())
.with_addrs(8000, vec![IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))])
.with_multicast_interfaces_v4(vec![Ipv4Addr::new(127, 0, 0, 1)])
.with_cadence(Duration::from_secs(1))
.with_response_rate(1.0);
let guard1 = discoverer1
.spawn(&handle)
.expect("Failed to spawn discoverer1");
let discoverer2 = Discoverer::new("test_service".to_string(), peer_id2)
.with_multicast_interfaces_v4(vec![Ipv4Addr::new(127, 0, 0, 1)])
.with_callback(move |id, peer| {
if id == peer_id1 {
tx.try_send(peer.clone()).ok();
}
});
let _guard2 = discoverer2
.spawn(&handle)
.expect("Failed to spawn discoverer2");
let initial_peer = tokio::time::timeout(Duration::from_secs(2), rx.recv())
.await
.expect("Timeout waiting for initial peer")
.expect("Failed to receive initial peer");
assert_eq!(initial_peer.addrs().len(), 1);
assert_eq!(
initial_peer.addrs()[0],
(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8000)
);
guard1.add(
9000,
vec![IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))],
);
guard1.remove_port(8000);
let updated_peer = tokio::time::timeout(Duration::from_secs(5), async {
loop {
if let Some(peer) = rx.recv().await {
if peer.addrs().len() == 1 && peer.addrs()[0].1 == 9000 {
return Ok(peer);
}
} else {
return Err("Failed to receive updated peer");
}
}
})
.await
.expect("Timeout waiting for updated peer")
.expect("Failed to receive updated peer");
assert_eq!(updated_peer.addrs().len(), 1);
assert_eq!(
updated_peer.addrs()[0],
(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 9000)
);
drop(guard1);
}
}