[mdns] Split response packets if necessary. (#1877)

* [mdns] Split response packets.

Prevent MDNS response packets becoming too large by creating
multi-packet responses. Also skip addresses that don't fit
into a TXT record or contain invalid characters.

* Update protocols/mdns/src/dns.rs

Co-authored-by: Max Inden <mail@max-inden.de>

* Refactor response packet construction.

* Update mdns changelog.

Co-authored-by: Max Inden <mail@max-inden.de>
This commit is contained in:
Roman Borschel
2020-12-08 11:47:35 +01:00
committed by GitHub
parent e665a818d7
commit 4c1657ea0f
5 changed files with 142 additions and 93 deletions

View File

@ -1,5 +1,9 @@
# 0.26.0 [unreleased] # 0.26.0 [unreleased]
- Create multiple multicast response packets as required to avoid
hitting the limit of 9000 bytes per MDNS packet.
[PR 1877](https://github.com/libp2p/rust-libp2p/pull/1877).
- Detect interface changes and join the MDNS multicast - Detect interface changes and join the MDNS multicast
group on all interfaces as they become available. group on all interfaces as they become available.
[PR 1830](https://github.com/libp2p/rust-libp2p/pull/1830). [PR 1830](https://github.com/libp2p/rust-libp2p/pull/1830).

View File

@ -199,13 +199,14 @@ impl NetworkBehaviour for Mdns {
MdnsPacket::Query(query) => { MdnsPacket::Query(query) => {
// MaybeBusyMdnsService should always be Free. // MaybeBusyMdnsService should always be Free.
if let MdnsBusyWrapper::Free(ref mut service) = self.service { if let MdnsBusyWrapper::Free(ref mut service) = self.service {
let resp = build_query_response( for packet in build_query_response(
query.query_id(), query.query_id(),
params.local_peer_id().clone(), params.local_peer_id().clone(),
params.listened_addresses().into_iter(), params.listened_addresses().into_iter(),
MDNS_RESPONSE_TTL, MDNS_RESPONSE_TTL,
); ) {
service.enqueue_response(resp.unwrap()); service.enqueue_response(packet)
}
} else { debug_assert!(false); } } else { debug_assert!(false); }
}, },
MdnsPacket::Response(response) => { MdnsPacket::Response(response) => {

View File

@ -18,16 +18,37 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE. // DEALINGS IN THE SOFTWARE.
//! Contains methods that handle the DNS encoding and decoding capabilities not available in the //! (M)DNS encoding and decoding on top of the `dns_parser` library.
//! `dns_parser` library.
use crate::{META_QUERY_SERVICE, SERVICE_NAME}; use crate::{META_QUERY_SERVICE, SERVICE_NAME};
use libp2p_core::{Multiaddr, PeerId}; use libp2p_core::{Multiaddr, PeerId};
use std::{borrow::Cow, cmp, error, fmt, str, time::Duration}; use std::{borrow::Cow, cmp, error, fmt, str, time::Duration};
/// Maximum size of a DNS label as per RFC1035 /// Maximum size of a DNS label as per RFC1035.
const MAX_LABEL_LENGTH: usize = 63; const MAX_LABEL_LENGTH: usize = 63;
/// DNS TXT records can have up to 255 characters as a single string value.
///
/// Current values are usually around 170-190 bytes long, varying primarily
/// with the length of the contained `Multiaddr`.
const MAX_TXT_VALUE_LENGTH: usize = 255;
/// A conservative maximum size (in bytes) of a complete TXT record,
/// as encoded by [`append_txt_record`].
const MAX_TXT_RECORD_SIZE: usize = MAX_TXT_VALUE_LENGTH + 45;
/// The maximum DNS packet size is 9000 bytes less the maximum
/// sizes of the IP (60) and UDP (8) headers.
const MAX_PACKET_SIZE: usize = 9000 - 68;
/// A conservative maximum number of records that can be packed into
/// a single DNS UDP packet, allowing up to 100 bytes of MDNS packet
/// header data to be added by [`query_response_packet()`].
const MAX_RECORDS_PER_PACKET: usize = (MAX_PACKET_SIZE - 100) / MAX_TXT_RECORD_SIZE;
/// An encoded MDNS packet.
pub type MdnsPacket = Vec<u8>;
/// Decodes a `<character-string>` (as defined by RFC1035) into a `Vec` of ASCII characters. /// Decodes a `<character-string>` (as defined by RFC1035) into a `Vec` of ASCII characters.
// TODO: better error type? // TODO: better error type?
pub fn decode_character_string(mut from: &[u8]) -> Result<Cow<'_, [u8]>, ()> { pub fn decode_character_string(mut from: &[u8]) -> Result<Cow<'_, [u8]>, ()> {
@ -49,7 +70,7 @@ pub fn decode_character_string(mut from: &[u8]) -> Result<Cow<'_, [u8]>, ()> {
} }
/// Builds the binary representation of a DNS query to send on the network. /// Builds the binary representation of a DNS query to send on the network.
pub fn build_query() -> Vec<u8> { pub fn build_query() -> MdnsPacket {
let mut out = Vec::with_capacity(33); let mut out = Vec::with_capacity(33);
// Program-generated transaction ID; unused by our implementation. // Program-generated transaction ID; unused by our implementation.
@ -80,7 +101,7 @@ pub fn build_query() -> Vec<u8> {
out out
} }
/// Builds the response to the DNS query. /// Builds the response to an address discovery DNS query.
/// ///
/// If there are more than 2^16-1 addresses, ignores the rest. /// If there are more than 2^16-1 addresses, ignores the rest.
pub fn build_query_response( pub fn build_query_response(
@ -88,60 +109,59 @@ pub fn build_query_response(
peer_id: PeerId, peer_id: PeerId,
addresses: impl ExactSizeIterator<Item = Multiaddr>, addresses: impl ExactSizeIterator<Item = Multiaddr>,
ttl: Duration, ttl: Duration,
) -> Result<Vec<u8>, MdnsResponseError> { ) -> Vec<MdnsPacket> {
// Convert the TTL into seconds. // Convert the TTL into seconds.
let ttl = duration_to_secs(ttl); let ttl = duration_to_secs(ttl);
// Add a limit to 2^16-1 addresses, as the protocol limits to this number. // Add a limit to 2^16-1 addresses, as the protocol limits to this number.
let addresses = addresses.take(65535); let mut addresses = addresses.take(65535);
// This capacity was determined empirically and is a reasonable upper limit.
let mut out = Vec::with_capacity(320);
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, addresses.len() as u16);
// Our single answer.
// The name.
append_qname(&mut out, SERVICE_NAME);
// Flags.
append_u16(&mut out, 0x000c);
append_u16(&mut out, 0x0001);
// TTL for the answer
append_u32(&mut out, ttl);
// Peer Id.
let peer_id_bytes = encode_peer_id(&peer_id); let peer_id_bytes = encode_peer_id(&peer_id);
debug_assert!(peer_id_bytes.len() <= 0xffff); debug_assert!(peer_id_bytes.len() <= 0xffff);
append_u16(&mut out, peer_id_bytes.len() as u16);
out.extend_from_slice(&peer_id_bytes);
// The TXT records for answers. // The accumulated response packets.
for addr in addresses { let mut packets = Vec::new();
// The records accumulated per response packet.
let mut records = Vec::with_capacity(addresses.len() * MAX_TXT_RECORD_SIZE);
// Encode the addresses as TXT records, and multiple TXT records into a
// response packet.
while let Some(addr) = addresses.next() {
let txt_to_send = format!("dnsaddr={}/p2p/{}", addr.to_string(), peer_id.to_base58()); let txt_to_send = format!("dnsaddr={}/p2p/{}", addr.to_string(), peer_id.to_base58());
let mut txt_to_send_bytes = Vec::with_capacity(txt_to_send.len()); let mut txt_record = Vec::with_capacity(txt_to_send.len());
append_character_string(&mut txt_to_send_bytes, txt_to_send.as_bytes())?; match append_txt_record(&mut txt_record, &peer_id_bytes, ttl, &txt_to_send) {
append_txt_record(&mut out, &peer_id_bytes, ttl, Some(&txt_to_send_bytes[..]))?; Ok(()) => {
records.push(txt_record);
}
Err(e) => {
log::warn!("Excluding address {} from response: {:?}", addr, e);
}
}
if records.len() == MAX_RECORDS_PER_PACKET {
packets.push(query_response_packet(id, &peer_id_bytes, &records, ttl));
records.clear();
}
} }
// The DNS specs specify that the maximum allowed size is 9000 bytes. // If there are still unpacked records, i.e. if the number of records is not
if out.len() > 9000 { // a multiple of `MAX_RECORDS_PER_PACKET`, create a final packet.
return Err(MdnsResponseError::ResponseTooLong); if !records.is_empty() {
packets.push(query_response_packet(id, &peer_id_bytes, &records, ttl));
} }
Ok(out) // If no packets have been built at all, because `addresses` is empty,
// construct an empty response packet.
if packets.is_empty() {
packets.push(query_response_packet(id, &peer_id_bytes, &Vec::new(), ttl));
}
packets
} }
/// Builds the response to the DNS query. /// Builds the response to a service discovery DNS query.
pub fn build_service_discovery_response(id: u16, ttl: Duration) -> Vec<u8> { pub fn build_service_discovery_response(id: u16, ttl: Duration) -> MdnsPacket {
// Convert the TTL into seconds. // Convert the TTL into seconds.
let ttl = duration_to_secs(ttl); let ttl = duration_to_secs(ttl);
@ -182,6 +202,42 @@ pub fn build_service_discovery_response(id: u16, ttl: Duration) -> Vec<u8> {
out out
} }
/// Constructs an MDNS query response packet for an address lookup.
fn query_response_packet(id: u16, peer_id: &Vec<u8>, records: &Vec<Vec<u8>>, ttl: u32) -> MdnsPacket {
let mut out = Vec::with_capacity(records.len() * MAX_TXT_RECORD_SIZE);
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, records.len() as u16);
// Our single answer.
// The name.
append_qname(&mut out, SERVICE_NAME);
// Flags.
append_u16(&mut out, 0x000c);
append_u16(&mut out, 0x0001);
// TTL for the answer
append_u32(&mut out, ttl);
// Peer Id.
append_u16(&mut out, peer_id.len() as u16);
out.extend_from_slice(&peer_id);
// The TXT records.
for record in records {
out.extend_from_slice(&record);
}
out
}
/// Returns the number of secs of a duration. /// Returns the number of secs of a duration.
fn duration_to_secs(duration: Duration) -> u32 { fn duration_to_secs(duration: Duration) -> u32 {
let secs = duration let secs = duration
@ -262,21 +318,19 @@ fn append_qname(out: &mut Vec<u8>, name: &[u8]) {
} }
/// Appends a `<character-string>` (as defined by RFC1035) to the `Vec`. /// Appends a `<character-string>` (as defined by RFC1035) to the `Vec`.
fn append_character_string(out: &mut Vec<u8>, ascii_str: &[u8]) -> Result<(), MdnsResponseError> { fn append_character_string(out: &mut Vec<u8>, ascii_str: &str) -> Result<(), MdnsResponseError> {
if !ascii_str.is_ascii() { if !ascii_str.is_ascii() {
return Err(MdnsResponseError::NonAsciiMultiaddr); return Err(MdnsResponseError::NonAsciiMultiaddr);
} }
if !ascii_str.iter().any(|&c| c == b' ') { if !ascii_str.bytes().any(|c| c == b' ') {
for &chr in ascii_str.iter() { out.extend_from_slice(ascii_str.as_bytes());
out.push(chr);
}
return Ok(()); return Ok(());
} }
out.push(b'"'); out.push(b'"');
for &chr in ascii_str.iter() { for &chr in ascii_str.as_bytes() {
if chr == b'\\' { if chr == b'\\' {
out.push(b'\\'); out.push(b'\\');
out.push(b'\\'); out.push(b'\\');
@ -292,19 +346,19 @@ fn append_character_string(out: &mut Vec<u8>, ascii_str: &[u8]) -> Result<(), Md
Ok(()) Ok(())
} }
/// Appends a TXT record to the answer in `out`. /// Appends a TXT record to `out`.
fn append_txt_record<'a>( fn append_txt_record<'a>(
out: &mut Vec<u8>, out: &mut Vec<u8>,
name: &[u8], name: &[u8],
ttl_secs: u32, ttl_secs: u32,
entries: impl IntoIterator<Item = &'a [u8]>, value: &str,
) -> Result<(), MdnsResponseError> { ) -> Result<(), MdnsResponseError> {
// The name. // The name.
out.extend_from_slice(name); out.extend_from_slice(name);
// Flags. // Flags.
out.push(0x00); out.push(0x00);
out.push(0x10); // TXT record. out.push(0x10); // TXT record.
out.push(0x80); out.push(0x80);
out.push(0x01); out.push(0x01);
@ -312,35 +366,23 @@ fn append_txt_record<'a>(
append_u32(out, ttl_secs); append_u32(out, ttl_secs);
// Add the strings. // Add the strings.
let mut buffer = Vec::new(); if value.len() > MAX_TXT_VALUE_LENGTH {
for entry in entries {
if entry.len() > u8::max_value() as usize {
return Err(MdnsResponseError::TxtRecordTooLong);
}
buffer.push(entry.len() as u8);
buffer.extend_from_slice(entry);
}
// It is illegal to have an empty TXT record, but we can have one zero-bytes entry, which does
// the same.
if buffer.is_empty() {
buffer.push(0);
}
if buffer.len() > u16::max_value() as usize {
return Err(MdnsResponseError::TxtRecordTooLong); return Err(MdnsResponseError::TxtRecordTooLong);
} }
let mut buffer = Vec::new();
buffer.push(value.len() as u8);
append_character_string(&mut buffer, value)?;
append_u16(out, buffer.len() as u16); append_u16(out, buffer.len() as u16);
out.extend_from_slice(&buffer); out.extend_from_slice(&buffer);
Ok(()) Ok(())
} }
/// Error that can happen when producing a DNS response. /// Errors that can occur on encoding an MDNS response.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug)]
pub enum MdnsResponseError { enum MdnsResponseError {
TxtRecordTooLong, TxtRecordTooLong,
NonAsciiMultiaddr, NonAsciiMultiaddr,
ResponseTooLong,
} }
impl fmt::Display for MdnsResponseError { impl fmt::Display for MdnsResponseError {
@ -349,11 +391,8 @@ impl fmt::Display for MdnsResponseError {
MdnsResponseError::TxtRecordTooLong => { MdnsResponseError::TxtRecordTooLong => {
write!(f, "TXT record invalid because it is too long") write!(f, "TXT record invalid because it is too long")
} }
MdnsResponseError::NonAsciiMultiaddr => write!( MdnsResponseError::NonAsciiMultiaddr =>
f, write!(f, "A multiaddr contains non-ASCII characters when serialized"),
"A multiaddr contains non-ASCII characters when serializd"
),
MdnsResponseError::ResponseTooLong => write!(f, "DNS response is too long"),
} }
} }
} }
@ -378,14 +417,15 @@ mod tests {
let my_peer_id = identity::Keypair::generate_ed25519().public().into_peer_id(); let my_peer_id = identity::Keypair::generate_ed25519().public().into_peer_id();
let addr1 = "/ip4/1.2.3.4/tcp/5000".parse().unwrap(); let addr1 = "/ip4/1.2.3.4/tcp/5000".parse().unwrap();
let addr2 = "/ip6/::1/udp/10000".parse().unwrap(); let addr2 = "/ip6/::1/udp/10000".parse().unwrap();
let query = build_query_response( let packets = build_query_response(
0xf8f8, 0xf8f8,
my_peer_id, my_peer_id,
vec![addr1, addr2].into_iter(), vec![addr1, addr2].into_iter(),
Duration::from_secs(60), Duration::from_secs(60),
) );
.unwrap(); for packet in packets {
assert!(Packet::parse(&query).is_ok()); assert!(Packet::parse(&packet).is_ok());
}
} }
#[test] #[test]

View File

@ -30,9 +30,9 @@
//! struct will automatically discover other libp2p nodes on the local network. //! struct will automatically discover other libp2p nodes on the local network.
//! //!
/// Hardcoded name of the mDNS service. Part of the mDNS libp2p specifications. /// The DNS service name for all libp2p peers used to query for addresses.
const SERVICE_NAME: &[u8] = b"_p2p._udp.local"; const SERVICE_NAME: &[u8] = b"_p2p._udp.local";
/// Hardcoded name of the service used for DNS-SD. /// The meta query for looking up the `SERVICE_NAME`.
const META_QUERY_SERVICE: &[u8] = b"_services._dns-sd._udp.local"; const META_QUERY_SERVICE: &[u8] = b"_services._dns-sd._udp.local";
pub use crate::{ pub use crate::{

View File

@ -29,7 +29,7 @@ use log::warn;
use socket2::{Socket, Domain, Type}; use socket2::{Socket, Domain, Type};
use std::{convert::TryFrom, fmt, io, net::{IpAddr, Ipv4Addr, UdpSocket, SocketAddr}, str, time::{Duration, Instant}}; use std::{convert::TryFrom, fmt, io, net::{IpAddr, Ipv4Addr, UdpSocket, SocketAddr}, str, time::{Duration, Instant}};
pub use dns::{MdnsResponseError, build_query_response, build_service_discovery_response}; pub use dns::{build_query_response, build_service_discovery_response};
lazy_static! { lazy_static! {
static ref IPV4_MDNS_MULTICAST_ADDRESS: SocketAddr = SocketAddr::from(( static ref IPV4_MDNS_MULTICAST_ADDRESS: SocketAddr = SocketAddr::from((
@ -76,13 +76,15 @@ lazy_static! {
/// match packet { /// match packet {
/// MdnsPacket::Query(query) => { /// MdnsPacket::Query(query) => {
/// println!("Query from {:?}", query.remote_addr()); /// println!("Query from {:?}", query.remote_addr());
/// let resp = build_query_response( /// let packets = build_query_response(
/// query.query_id(), /// query.query_id(),
/// my_peer_id.clone(), /// my_peer_id.clone(),
/// vec![].into_iter(), /// vec![].into_iter(),
/// Duration::from_secs(120), /// Duration::from_secs(120),
/// ).unwrap(); /// );
/// service.enqueue_response(resp); /// for packet in packets {
/// service.enqueue_response(packet);
/// }
/// } /// }
/// MdnsPacket::Response(response) => { /// MdnsPacket::Response(response) => {
/// for peer in response.discovered_peers() { /// for peer in response.discovered_peers() {
@ -609,8 +611,10 @@ mod tests {
peer_id.clone(), peer_id.clone(),
vec![].into_iter(), vec![].into_iter(),
Duration::from_secs(120), Duration::from_secs(120),
).unwrap(); );
service.enqueue_response(resp); for r in resp {
service.enqueue_response(r);
}
} }
MdnsPacket::Response(response) => { MdnsPacket::Response(response) => {
for peer in response.discovered_peers() { for peer in response.discovered_peers() {