blob: f2c5d78e48224191a065323051726da7f8c92df1 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::discovery::{TargetFinder, TargetFinderConfig};
use crate::target::*;
use futures::channel::mpsc;
use std::io;
#[cfg(target_os = "linux")]
use {self::linux::MdnsTargetFinderLinuxExt, std::net::UdpSocket};
#[cfg(target_os = "linux")]
#[derive(Debug)]
pub struct MdnsTargetFinder {
listener_sockets: Vec<UdpSocket>,
// TODO(awdavies): Might need to periodically check to see if a new iface
// has come up, like in the event of a TAP interface, for example.
sender_sockets: Vec<UdpSocket>,
config: TargetFinderConfig,
}
#[cfg(not(target_os = "linux"))]
#[derive(Debug)]
pub struct MdnsTargetFinder {}
impl TargetFinder for MdnsTargetFinder {
#[cfg(target_os = "linux")]
fn new(config: &TargetFinderConfig) -> io::Result<Self> {
Self::linux_new(config)
}
#[cfg(target_os = "linux")]
fn start(&self, s: &mpsc::UnboundedSender<Target>) -> io::Result<()> {
self.linux_start(s)
}
//// Non-linux trait impl
#[cfg(not(target_os = "linux"))]
fn new(_config: &TargetFinderConfig) -> io::Result<Self> {
unimplemented!()
}
#[cfg(not(target_os = "linux"))]
fn start(&self, _s: &mpsc::UnboundedSender<Target>) -> io::Result<()> {
unimplemented!()
}
}
// TODO(fxb/44855): This needs to be e2e tested.
#[cfg(target_os = "linux")]
mod linux {
use super::*;
use std::collections::HashSet;
use std::fmt::Write;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV6, UdpSocket};
use std::thread;
use crate::net;
use ::mdns::protocol as dns;
use chrono::Utc;
use futures::lock::Mutex;
use net2;
use net2::unix::UnixUdpBuilderExt;
use net2::UdpSocketExt;
use packet::{InnerPacketBuilder, ParseBuffer, Serializer};
use zerocopy::ByteSlice;
const MDNS_MCAST_V4: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 251);
const MDNS_MCAST_V6: Ipv6Addr = Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 0x00fb);
const MDNS_PORT: u16 = 5353;
#[derive(Debug, Eq, PartialEq)]
pub enum MdnsConvertError {
NodenameMissing,
}
pub trait MdnsTargetFinderLinuxExt: TargetFinder {
fn linux_new(c: &TargetFinderConfig) -> io::Result<Self>;
fn linux_start(&self, s: &mpsc::UnboundedSender<Target>) -> io::Result<()>;
}
impl MdnsTargetFinderLinuxExt for MdnsTargetFinder {
fn linux_new(config: &TargetFinderConfig) -> io::Result<Self> {
let mut listener_sockets: Vec<UdpSocket> = Vec::new();
listener_sockets.push(make_listen_socket((MDNS_MCAST_V4, MDNS_PORT).into())?);
listener_sockets.push(make_listen_socket((MDNS_MCAST_V6, MDNS_PORT).into())?);
let mut sender_sockets: Vec<UdpSocket> = Vec::new();
for iface in unsafe { net::linux::get_mcast_interfaces()? } {
for addr in iface.addrs.iter() {
let (src, dst): (SocketAddr, SocketAddr) = match addr {
IpAddr::V4(a) => {
((*a, MDNS_PORT).into(), (MDNS_MCAST_V4, MDNS_PORT).into())
}
// This needs to include the interface or else bind() will crash.
// Flow label is zero as this is just a UDP datagram.
IpAddr::V6(a) => (
SocketAddrV6::new(*a, MDNS_PORT, 0, iface.id).into(),
(MDNS_MCAST_V6, MDNS_PORT).into(),
),
};
sender_sockets.push(make_sender_socket(src, dst, config.mdns_ttl)?);
}
}
Ok(Self { listener_sockets, sender_sockets, config: config.clone() })
}
fn linux_start(&self, s: &mpsc::UnboundedSender<Target>) -> io::Result<()> {
self.start_listeners(s)?;
self.start_query_loop()?;
Ok(())
}
}
fn is_fuchsia_response<B: zerocopy::ByteSlice + Clone>(m: &dns::Message<B>) -> bool {
m.answers.len() >= 1 && m.answers[0].domain == "_fuchsia._udp.local"
}
impl<B: ByteSlice + Clone> TryIntoTarget for dns::Message<B> {
type Error = MdnsConvertError;
fn try_into_target(self, src: SocketAddr) -> Result<Target, Self::Error> {
let mut nodename = String::new();
let addrs: HashSet<TargetAddr> = [src.into()].iter().cloned().collect();
for record in self.additional.iter() {
if record.rtype != dns::Type::A && record.rtype != dns::Type::Aaaa {
continue;
}
if nodename.len() == 0 {
write!(nodename, "{}", record.domain).unwrap();
nodename = nodename.trim_end_matches(".local").into();
}
// The records here also have the IP addresses of
// the machine, however these could be different if behind a NAT
// (as with QEMU). Later it might be useful to store them in the
// Target struct.
}
if nodename.len() == 0 {
return Err(MdnsConvertError::NodenameMissing);
}
let time = Utc::now();
let addrs = Mutex::new(addrs);
let last_response = Mutex::new(time);
let state = Mutex::new(TargetState::new());
Ok(Target { nodename, addrs, last_response, state })
}
}
impl MdnsTargetFinder {
fn start_listeners(&self, s: &mpsc::UnboundedSender<Target>) -> io::Result<()> {
// Listen on all sockets in the event that unicast packets are returned.
for l in self.sender_sockets.iter().chain(self.listener_sockets.iter()) {
let sender = s.clone();
let sock = l.try_clone()?;
thread::spawn(move || {
let mut buf = [0; 1500];
loop {
let (amt, src) = sock.recv_from(&mut buf).unwrap();
let mut buf = &mut buf[..amt];
match buf.parse::<dns::Message<_>>() {
Ok(m) => {
if is_fuchsia_response(&m) {
match m.try_into_target(src) {
Ok(target) => sender.unbounded_send(target).unwrap(),
_ => (),
}
}
}
_ => (),
}
}
});
}
Ok(())
}
fn start_query_loop(&self) -> io::Result<()> {
for s in self.sender_sockets.iter() {
let sock = s.try_clone()?;
let config = self.config.clone();
thread::spawn(move || {
// MCast question.
let question = dns::QuestionBuilder::new(
dns::DomainBuilder::from_str("_fuchsia._udp.local").unwrap(),
dns::Type::Ptr,
dns::Class::In,
false,
);
let mut message = dns::MessageBuilder::new(0, true);
message.add_question(question);
let message_bytes = message
.into_serializer()
.serialize_vec_outer()
.unwrap_or_else(|_| panic!("Failed to serialize msg"))
.unwrap_b();
loop {
sock.send(&message_bytes.as_ref()).unwrap();
std::thread::sleep(config.broadcast_interval);
}
});
}
Ok(())
}
}
fn make_listen_socket(s: SocketAddr) -> io::Result<UdpSocket> {
match s {
SocketAddr::V4(addr) => {
let socket = net2::UdpBuilder::new_v4()?
.reuse_address(true)?
.reuse_port(true)?
.bind((Ipv4Addr::UNSPECIFIED, s.port()))?;
socket.set_multicast_loop_v4(false)?;
socket.join_multicast_v4(&addr.ip(), &Ipv4Addr::UNSPECIFIED)?;
Ok(socket)
}
SocketAddr::V6(addr) => {
let socket = net2::UdpBuilder::new_v6()?
.only_v6(true)?
.reuse_address(true)?
.reuse_port(true)?
.bind((Ipv6Addr::UNSPECIFIED, s.port()))?;
socket.set_multicast_loop_v6(false)?;
// Presuming that this is a multicast address, we need to join
// on every interface.
for iface in unsafe { net::linux::get_mcast_interfaces()? } {
// If the iface doesn't have a link local IPv6 address,
// this will panic.
if iface.addrs.iter().any(|addr| addr.is_ipv6()) {
socket.join_multicast_v6(&addr.ip(), iface.id)?;
}
}
Ok(socket)
}
}
}
fn make_sender_socket(s: SocketAddr, d: SocketAddr, ttl: u8) -> io::Result<UdpSocket> {
let socket = match s {
SocketAddr::V4(saddr) => {
let socket = net2::UdpBuilder::new_v4()?
.reuse_address(true)?
.reuse_port(true)?
.bind(saddr)?;
socket.set_multicast_ttl_v4(ttl.into())?;
socket
}
SocketAddr::V6(saddr) => {
let socket = net2::UdpBuilder::new_v6()?
.reuse_address(true)?
.reuse_port(true)?
.bind(saddr)?;
socket.set_multicast_hops_v6(ttl.into())?;
socket
}
};
socket.connect(d)?;
Ok(socket)
}
}