diff --git a/Cargo.lock b/Cargo.lock index fdeada37500..3550a1d9a83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2725,7 +2725,7 @@ dependencies = [ [[package]] name = "libp2p-mdns" -version = "0.48.0" +version = "0.48.1" dependencies = [ "futures", "hickory-proto", diff --git a/Cargo.toml b/Cargo.toml index 9e65d1ab62f..698cc282cce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,7 +86,7 @@ libp2p-gossipsub = { version = "0.50.0", path = "protocols/gossipsub" } libp2p-identify = { version = "0.47.0", path = "protocols/identify" } libp2p-identity = { version = "0.2.12" } libp2p-kad = { version = "0.49.0", path = "protocols/kad" } -libp2p-mdns = { version = "0.48.0", path = "protocols/mdns" } +libp2p-mdns = { version = "0.48.1", path = "protocols/mdns" } libp2p-memory-connection-limits = { version = "0.5.0", path = "misc/memory-connection-limits" } libp2p-metrics = { version = "0.17.1", path = "misc/metrics" } libp2p-mplex = { version = "0.43.1", path = "muxers/mplex" } diff --git a/protocols/mdns/CHANGELOG.md b/protocols/mdns/CHANGELOG.md index a1d928c5658..6c10025fc1e 100644 --- a/protocols/mdns/CHANGELOG.md +++ b/protocols/mdns/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.48.1 + +- Implements support for querying individual peer SRV and TXT queries via dns-sd style queries. + ## 0.48.0 - Remove `async_std` dependency [PR 5958](https://github.com/libp2p/rust-libp2p/pull/5958) diff --git a/protocols/mdns/Cargo.toml b/protocols/mdns/Cargo.toml index 5f3f9dfeccf..2b8d0e424ca 100644 --- a/protocols/mdns/Cargo.toml +++ b/protocols/mdns/Cargo.toml @@ -2,7 +2,7 @@ name = "libp2p-mdns" edition.workspace = true rust-version = { workspace = true } -version = "0.48.0" +version = "0.48.1" description = "Implementation of the libp2p mDNS discovery method" authors = ["Parity Technologies "] license = "MIT" diff --git a/protocols/mdns/src/behaviour/iface.rs b/protocols/mdns/src/behaviour/iface.rs index 873bb8a307b..4db0b866167 100644 --- a/protocols/mdns/src/behaviour/iface.rs +++ b/protocols/mdns/src/behaviour/iface.rs @@ -33,7 +33,7 @@ use std::{ }; use futures::{channel::mpsc, SinkExt, StreamExt}; -use libp2p_core::Multiaddr; +use libp2p_core::{multiaddr::Protocol, Multiaddr}; use libp2p_identity::PeerId; use libp2p_swarm::ListenAddresses; use socket2::{Domain, Socket, Type}; @@ -43,7 +43,11 @@ use self::{ query::MdnsPacket, }; use crate::{ - behaviour::{socket::AsyncSocket, timer::Builder}, + behaviour::{ + iface::dns::{build_individual_query_srv_response, build_individual_query_txt_response}, + socket::AsyncSocket, + timer::Builder, + }, Config, }; @@ -315,6 +319,53 @@ where .push_back(build_service_discovery_response(disc.query_id(), this.ttl)); continue; } + Poll::Ready(Ok(Ok(Some(MdnsPacket::IndividualPeerResponse(response))))) => { + tracing::trace!( + address=%this.addr, + remote_address=%response.remote_addr(), + "received service discovery from remote address on address" + ); + + let buffer_bytes = match response.query_type() { + hickory_proto::rr::RecordType::SRV => { + let port = this + .listen_addresses + .read() + .unwrap_or_else(|e| e.into_inner()) + .iter() + .next() + .and_then(|addr| { + addr.iter().find_map(|proto| match proto { + Protocol::Tcp(port) => Some(port), + Protocol::Udp(port) => Some(port), + _ => None, + }) + }) + .unwrap_or(0); + + build_individual_query_srv_response( + response.query_id(), + response.peer_name(), + port, + this.ttl, + ) + } + hickory_proto::rr::RecordType::TXT => build_individual_query_txt_response( + response.query_id(), + response.peer_name(), + this.local_peer_id, + this.listen_addresses + .read() + .unwrap_or_else(|e| e.into_inner()) + .iter(), + this.ttl, + ), + _ => continue, + }; + + this.send_buffer.push_back(buffer_bytes); + continue; + } Poll::Ready(Err(err)) if err.kind() == std::io::ErrorKind::WouldBlock => { // No more bytes available on the socket to read continue; diff --git a/protocols/mdns/src/behaviour/iface/dns.rs b/protocols/mdns/src/behaviour/iface/dns.rs index 35cba44f4af..454886f2e93 100644 --- a/protocols/mdns/src/behaviour/iface/dns.rs +++ b/protocols/mdns/src/behaviour/iface/dns.rs @@ -207,6 +207,99 @@ pub(crate) fn build_service_discovery_response(id: u16, ttl: Duration) -> MdnsPa out } +/// Builds the response to an individual peer SRV query. +pub(crate) fn build_individual_query_srv_response( + id: u16, + peer_name: &str, + port: u16, + ttl: Duration, +) -> MdnsPacket { + let ttl = duration_to_secs(ttl); + let mut out = Vec::with_capacity(200); + + append_u16(&mut out, id); + // 0x84 flag for an answer. + append_u16(&mut out, 0x8400); + // Number of questions, answers, authorities, additionals. + append_u16(&mut out, 0x0); + append_u16(&mut out, 0x1); + append_u16(&mut out, 0x0); + append_u16(&mut out, 0x0); + + // Answer to the query: peer_name._p2p._udp.local. SRV record + let full_name = format!("{}._p2p._udp.local", peer_name); + append_qname(&mut out, full_name.as_bytes()); + // SRV type + append_u16(&mut out, 0x0021); + // Cache-flush bit (IN) + append_u16(&mut out, 0x8001); + + // TTL for the answer + append_u32(&mut out, ttl); + + // Calculate RDATA length + let mut rdata = Vec::new(); + // Priority + append_u16(&mut rdata, 0); + // Weight + append_u16(&mut rdata, 0); + append_u16(&mut rdata, port); + + // Use the peer name as hostname for mDNS + append_qname(&mut rdata, format!("{}.local", peer_name).as_bytes()); + + append_u16(&mut out, rdata.len() as u16); + out.extend_from_slice(&rdata); + + out +} + +/// Builds the response to an individual peer TXT query. +pub(crate) fn build_individual_query_txt_response<'a>( + id: u16, + peer_name: &str, + peer_id: PeerId, + addresses: impl Iterator, + ttl: Duration, +) -> MdnsPacket { + let ttl = duration_to_secs(ttl); + let mut out = Vec::with_capacity(500); + + append_u16(&mut out, id); + // 0x84 flag for an answer. + append_u16(&mut out, 0x8400); + // Number of questions, answers, authorities, additionals. + append_u16(&mut out, 0x0); + append_u16(&mut out, 0x1); + append_u16(&mut out, 0x0); + append_u16(&mut out, 0x0); + + // Answer: peer_name._p2p._udp.local. TXT record + let full_name = format!("{}._p2p._udp.local", peer_name); + append_qname(&mut out, full_name.as_bytes()); + + // TXT type + append_u16(&mut out, 0x0010); + // Cache-flush bit (IN) + append_u16(&mut out, 0x8001); + append_u32(&mut out, ttl); + + // Build TXT record data with all addresses + let mut txt_data = Vec::new(); + for addr in addresses { + let txt_value = format!("dnsaddr={}/p2p/{}", addr, peer_id.to_base58()); + if txt_value.len() <= MAX_TXT_VALUE_LENGTH { + txt_data.push(txt_value.len() as u8); + txt_data.extend_from_slice(txt_value.as_bytes()); + } + } + + append_u16(&mut out, txt_data.len() as u16); + out.extend_from_slice(&txt_data); + + out +} + /// Constructs an MDNS query response packet for an address lookup. fn query_response_packet(id: u16, peer_id: &[u8], records: &[Vec], ttl: u32) -> MdnsPacket { let mut out = Vec::with_capacity(records.len() * MAX_TXT_RECORD_SIZE); @@ -427,6 +520,33 @@ mod tests { assert!(Message::from_vec(&query).is_ok()); } + #[test] + fn build_individual_query_txt_response_correct() { + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let addr: Multiaddr = "/ip4/127.0.0.1/tcp/4001".parse().unwrap(); + + let packet = build_individual_query_txt_response( + 0x5678, + "testpeer", + peer_id, + std::iter::once(&addr), + Duration::from_secs(60), + ); + assert!(Message::from_vec(&packet).is_ok()); + } + + #[test] + fn build_individual_query_srv_response_correct() { + let packet = build_individual_query_srv_response( + 0x1234, + "my-peer-abc123", + 30333, + Duration::from_secs(120), + ); + + assert!(Message::from_vec(&packet).is_ok()); + } + #[test] fn test_random_string() { let varsize = thread_rng().gen_range(0..32); diff --git a/protocols/mdns/src/behaviour/iface/query.rs b/protocols/mdns/src/behaviour/iface/query.rs index a2a2c200b3b..132d1d3ceb7 100644 --- a/protocols/mdns/src/behaviour/iface/query.rs +++ b/protocols/mdns/src/behaviour/iface/query.rs @@ -45,6 +45,8 @@ pub(crate) enum MdnsPacket { Response(MdnsResponse), /// A request for service discovery. ServiceDiscovery(MdnsServiceDiscovery), + /// A query made by a remote for an individual peer. + IndividualPeerResponse(MdnsIndividualPeerQuery), } impl MdnsPacket { @@ -82,6 +84,45 @@ impl MdnsPacket { }))); } + for query in packet.queries() { + let query_name = query.name().to_utf8(); + + if query_name.ends_with("._p2p._udp.local.") || query_name.ends_with("._p2p._udp.local") + { + let peer_and_query_parts = query_name.splitn(2, '.').collect::>(); + if peer_and_query_parts.is_empty() { + continue; + } + + let peer_str = peer_and_query_parts[0]; + + // Check what type of record is being asked for + match query.query_type() { + hickory_proto::rr::RecordType::SRV => { + return Ok(Some(MdnsPacket::IndividualPeerResponse( + MdnsIndividualPeerQuery { + from, + query_id: packet.header().id(), + query_type: hickory_proto::rr::RecordType::SRV, + peer_name: peer_str.to_string(), + }, + ))); + } + hickory_proto::rr::RecordType::TXT => { + return Ok(Some(MdnsPacket::IndividualPeerResponse( + MdnsIndividualPeerQuery { + from, + query_id: packet.header().id(), + query_type: hickory_proto::rr::RecordType::TXT, + peer_name: peer_str.to_string(), + }, + ))) + } + _ => continue, + } + } + } + Ok(None) } } @@ -115,6 +156,46 @@ impl fmt::Debug for MdnsQuery { } } +pub(crate) struct MdnsIndividualPeerQuery { + /// Sender of the address. + from: SocketAddr, + /// Id of the received DNS query. We need to pass this ID back in the results. + query_id: u16, + /// The RecordType of the query + query_type: hickory_proto::rr::RecordType, + /// The name of the peer being queried + peer_name: String, +} + +impl fmt::Debug for MdnsIndividualPeerQuery { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MdnsIndividualPeerQuery") + .field("from", self.remote_addr()) + .field("query_id", &self.query_id()) + .field("query_type", self.query_type()) + .field("peer_name", self.peer_name()) + .finish() + } +} + +impl MdnsIndividualPeerQuery { + pub(crate) fn remote_addr(&self) -> &SocketAddr { + &self.from + } + + pub(crate) fn query_id(&self) -> u16 { + self.query_id + } + + pub(crate) fn query_type(&self) -> &hickory_proto::rr::RecordType { + &self.query_type + } + + pub(crate) fn peer_name(&self) -> &String { + &self.peer_name + } +} + /// A received mDNS service discovery query. pub(crate) struct MdnsServiceDiscovery { /// Sender of the address.