blob: 45a1e011a61c8e700d3eaf48d1922c5964538966 [file] [log] [blame]
// Copyright 2021 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 {
addr::TargetAddr,
anyhow::{anyhow, bail, Context as _, Result},
async_io::Async,
async_net::UdpSocket,
ffx_daemon_core::events,
ffx_daemon_events::{DaemonEvent, TargetInfo, TryIntoTargetInfo, WireTrafficType},
fuchsia_async::{Task, Timer},
netext::{get_mcast_interfaces, IsLocalAddr},
netsvc_proto::netboot::{
NetbootPacket, NetbootPacketBuilder, Opcode, ADVERT_PORT, SERVER_PORT,
},
packet::{Buf, FragmentedBuffer, InnerPacketBuilder, ParseBuffer, Serializer},
std::collections::HashSet,
std::net::{IpAddr, Ipv6Addr, SocketAddr},
std::num::NonZeroU16,
std::sync::{Arc, Weak},
std::time::Duration,
zerocopy::ByteSlice,
};
const ZEDBOOT_MCAST_V6: Ipv6Addr = Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 1);
const ZEDBOOT_REDISCOVERY_INTERFACE_INTERVAL: Duration = Duration::from_secs(5);
pub async fn zedboot_discovery(e: events::Queue<DaemonEvent>) -> Result<Task<()>> {
let port = port().await?;
Ok(Task::local(interface_discovery(port, e, ZEDBOOT_REDISCOVERY_INTERFACE_INTERVAL)))
}
async fn port() -> Result<NonZeroU16> {
ffx_config::get("discovery.zedboot.advert_port")
.await
.map(|port| {
NonZeroU16::new(port).ok_or_else(|| anyhow::anyhow!("advert port must be nonzero"))
})
.unwrap_or(Ok(ADVERT_PORT))
}
// interface_discovery iterates over all multicast interfaces
pub async fn interface_discovery(
port: NonZeroU16,
e: events::Queue<DaemonEvent>,
discovery_interval: Duration,
) {
// See fxbug.dev/62617#c10 for details. A macOS system can end up in
// a situation where the default routes for protocols are on
// non-functional interfaces, and under such conditions the wildcard
// listen socket binds will fail. We will repeat attempting to bind
// them, as newly added interfaces later may unstick the issue, if
// they introduce new routes. These boolean flags are used to
// suppress the production of a log output in every interface
// iteration.
// In order to manually reproduce these conditions on a macOS
// system, open Network.prefpane, and for each connection in the
// list select Advanced... > TCP/IP > Configure IPv6 > Link-local
// only. Click apply, then restart the ffx daemon.
let mut should_log_v6_listen_error = true;
let mut v6_listen_socket: Weak<UdpSocket> = Weak::new();
if ffx_config::get("discovery.zedboot.enabled").await.unwrap_or(false) {
log::debug!("Starting Zedboot discovery loop");
};
loop {
// fxb/90219 - disabled by default
let is_enabled: bool = ffx_config::get("discovery.zedboot.enabled").await.unwrap_or(false);
if is_enabled {
if v6_listen_socket.upgrade().is_none() {
match make_listen_socket((ZEDBOOT_MCAST_V6, port.get()).into())
.context("make_listen_socket for IPv6")
{
Ok(sock) => {
let sock = Arc::new(sock);
v6_listen_socket = Arc::downgrade(&sock);
Task::local(recv_loop(sock.clone(), e.clone())).detach();
should_log_v6_listen_error = true;
}
Err(err) => {
if should_log_v6_listen_error {
log::error!(
"unable to bind IPv6 listen socket: {}. Discovery may fail.",
err
);
should_log_v6_listen_error = false;
}
}
}
}
for iface in get_mcast_interfaces().unwrap_or(Vec::new()) {
match iface.id() {
Ok(id) => {
if let Some(sock) = v6_listen_socket.upgrade() {
let _ = sock.join_multicast_v6(&ZEDBOOT_MCAST_V6, id);
}
}
Err(err) => {
log::warn!("{}", err);
}
}
}
}
Timer::new(discovery_interval).await;
}
}
// recv_loop reads packets from sock. If the packet is a Fuchsia zedboot packet, a
// corresponding zedboot event is published to the queue. All other packets are
// silently discarded.
async fn recv_loop(sock: Arc<UdpSocket>, e: events::Queue<DaemonEvent>) {
loop {
// fxb/90219 - disabled by default
let is_enabled: bool = ffx_config::get("discovery.zedboot.enabled").await.unwrap_or(false);
if !is_enabled {
return;
}
let mut buf = &mut [0u8; 1500][..];
let addr = match sock.recv_from(&mut buf).await {
Ok((sz, addr)) => {
buf = &mut buf[..sz];
addr
}
Err(err) => {
log::info!("listen socket recv error: {}, zedboot listener closed", err);
return;
}
};
let msg = match buf.parse::<NetbootPacket<_>>() {
Ok(msg) => msg,
Err(e) => {
log::error!("failed to parse netboot packet {:?}", e);
continue;
}
};
// Note: important, otherwise non-local responders could add themselves.
if !addr.ip().is_local_addr() {
continue;
}
match ZedbootPacket(msg).try_into_target_info(addr) {
Ok(info) => {
log::trace!(
"zedboot packet from {} ({}) on {}",
addr,
info.nodename.clone().unwrap_or("<unknown>".to_string()),
sock.local_addr().unwrap()
);
e.push(DaemonEvent::WireTraffic(WireTrafficType::Zedboot(info))).unwrap_or_else(
|err| log::debug!("zedboot discovery was unable to publish: {}", err),
);
}
Err(e) => log::error!("failed to extract zedboot target info {:?}", e),
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub enum ZedbootConvertError {
NodenameMissing,
}
/// A newtype for NetbootPacket so we can implement traits for it respecting the
/// orphan rules.
struct ZedbootPacket<B: ByteSlice>(NetbootPacket<B>);
impl<B: ByteSlice> TryIntoTargetInfo for ZedbootPacket<B> {
type Error = ZedbootConvertError;
fn try_into_target_info(self, src: SocketAddr) -> Result<TargetInfo, Self::Error> {
let Self(packet) = self;
let mut nodename = None;
let msg = std::str::from_utf8(packet.payload().as_ref())
.map_err(|_| ZedbootConvertError::NodenameMissing)?;
for data in msg.split(';') {
let entry: Vec<&str> = data.split('=').collect();
if entry.len() == 2 {
if entry[0] == "nodename" {
nodename.replace(entry[1].trim_matches(char::from(0)));
break;
}
}
}
let nodename = nodename.ok_or(ZedbootConvertError::NodenameMissing)?;
let mut addrs: HashSet<TargetAddr> = [src.into()].iter().cloned().collect();
Ok(TargetInfo {
nodename: Some(nodename.to_string()),
addresses: addrs.drain().collect(),
..Default::default()
})
}
}
fn make_listen_socket(listen_addr: SocketAddr) -> Result<UdpSocket> {
let socket: std::net::UdpSocket = match listen_addr {
SocketAddr::V4(_) => bail!("Zedboot only supports IPv6"),
SocketAddr::V6(_) => {
let socket = socket2::Socket::new(
socket2::Domain::IPV6,
socket2::Type::DGRAM,
Some(socket2::Protocol::UDP),
)
.context("construct datagram socket")?;
socket.set_only_v6(true).context("set_only_v6")?;
socket.set_multicast_loop_v6(false).context("set_multicast_loop_v6")?;
socket.set_reuse_address(true).context("set_reuse_address")?;
socket.set_reuse_port(true).context("set_reuse_port")?;
socket
.bind(
&SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), listen_addr.port()).into(),
)
.context("bind")?;
socket
}
}
.into();
Ok(Async::new(socket)?.into())
}
async fn make_sender_socket(addr: SocketAddr) -> Result<UdpSocket> {
let socket: std::net::UdpSocket = {
let socket = socket2::Socket::new(
socket2::Domain::IPV6,
socket2::Type::DGRAM,
Some(socket2::Protocol::UDP),
)
.context("construct datagram socket")?;
socket.set_only_v6(true).context("set_only_v6")?;
socket.set_reuse_address(true).context("set_reuse_address")?;
socket.set_reuse_port(true).context("set_reuse_port")?;
socket
}
.into();
let result: UdpSocket = Async::new(socket)?.into();
result.connect(addr).await.context("connect to remote address")?;
Ok(result)
}
async fn send(opcode: Opcode, body: &str, to_addr: TargetAddr) -> Result<()> {
const BUFFER_SIZE: usize = 512;
const COOKIE: u32 = 1;
const ARG: u32 = 0;
let msg = (body.as_bytes())
.into_serializer_with(Buf::new([0u8; BUFFER_SIZE], ..))
.serialize_no_alloc(NetbootPacketBuilder::new(opcode.into(), COOKIE, ARG))
.expect("failed to serialize");
let mut to_sock: SocketAddr = to_addr.into();
to_sock.set_port(SERVER_PORT.get());
log::info!("Sending {:?} {} to {}", opcode, body, to_sock);
let sock = make_sender_socket(to_sock).await?;
sock.send(msg.as_ref()).await.map_err(|e| anyhow!("Sending error: {}", e)).and_then(|sent| {
if sent == msg.len() {
Ok(())
} else {
Err(anyhow::anyhow!("partial send {} of {} bytes", sent, msg.len()))
}
})
}
pub async fn reboot(to_addr: TargetAddr) -> Result<()> {
send(Opcode::Reboot, "", to_addr).await
}
pub async fn reboot_to_bootloader(to_addr: TargetAddr) -> Result<()> {
send(Opcode::ShellCmd, "dm reboot-bootloader\0", to_addr).await
}
pub async fn reboot_to_recovery(to_addr: TargetAddr) -> Result<()> {
send(Opcode::ShellCmd, "dm reboot-recovery\0", to_addr).await
}