blob: 8e641b74ce6e35ebae31b23260ab0951b292c79d [file] [log] [blame]
// Copyright 2022 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.
#![allow(dead_code)]
use super::*;
use fidl::endpoints::create_endpoints;
use fidl_fuchsia_net_mdns::*;
use fuchsia_component::client::connect_to_protocol;
const BORDER_AGENT_SERVICE_TYPE: &str = "_meshcop._udp.";
// Port 9 is the old-school discard port.
const BORDER_AGENT_SERVICE_PLACEHOLDER_PORT: u16 = 9;
// These flags are ultimately defined by table 8-5 of the Thread v1.1.1 specification.
// Additional flags originate from the source code found [here][1].
//
// [1]: https://github.com/openthread/ot-br-posix/blob/36db8891576a6ed571ad319afca734c5288c4cd9/src/border_agent/border_agent.cpp#L86
bitflags::bitflags! {
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BorderAgentState : u32 {
const CONNECTION_MODE_PSKC = 1;
const CONNECTION_MODE_PSKD = 2;
const CONNECTION_MODE_VENDOR = 3;
const CONNECTION_MODE_X509 = 4;
const THREAD_IF_STATUS_INITIALIZED = (1<<3);
const THREAD_IF_STATUS_ACTIVE = (2<<3);
const HIGH_AVAILABILITY = (1<<5);
const BBR_IS_ACTIVE = (1<<7);
const BBR_IS_PRIMARY = (1<<8);
}
}
fn calc_meshcop_service_txt<OT>(
ot_instance: &OT,
vendor: &str,
product: &str,
) -> Vec<(String, Vec<u8>)>
where
OT: ot::InstanceInterface,
{
let mut txt: Vec<(String, Vec<u8>)> = Vec::new();
let mut border_agent_state = BorderAgentState::HIGH_AVAILABILITY;
if ot_instance.border_agent_get_state() != ot::BorderAgentState::Stopped {
border_agent_state |= BorderAgentState::CONNECTION_MODE_PSKC;
}
if ot_instance.is_commissioned() {
match ot_instance.get_device_role() {
ot::DeviceRole::Disabled => {
border_agent_state |= BorderAgentState::THREAD_IF_STATUS_INITIALIZED
}
_ => border_agent_state |= BorderAgentState::THREAD_IF_STATUS_ACTIVE,
}
}
// `rv` - Version of TXT record format.
txt.push(("rv".to_string(), b"1".to_vec()));
// `tv` - Version of Thread specification in use.
txt.push(("tv".to_string(), ot::get_thread_version_str().as_bytes().to_vec()));
// `sb` - State bitmap
txt.push(("sb".to_string(), border_agent_state.bits().to_be_bytes().to_vec()));
// `nn` - Network Name
if ot_instance.is_commissioned() {
match ot_instance.get_network_name().try_as_str() {
Ok(nn) => txt.push(("nn".to_string(), nn.as_bytes().to_vec())),
Err(err) => {
warn!("Can't render network name: {:?}", err);
}
}
// `xp` - Extended PAN-ID
txt.push(("xp".to_string(), ot_instance.get_extended_pan_id().as_slice().to_vec()));
}
// `vn` - Vendor Name
txt.push(("vn".to_string(), vendor.as_bytes().to_vec()));
// `mn` - Model Name
txt.push(("mn".to_string(), product.as_bytes().to_vec()));
// `xa` - Extended Address
txt.push(("xa".to_string(), ot_instance.get_extended_address().as_slice().to_vec()));
if ot_instance.get_device_role().is_active() {
let mut dataset = Default::default();
match ot_instance.dataset_get_active(&mut dataset) {
Ok(()) => {
if let Some(at) = dataset.get_active_timestamp() {
// `at` - Active Operational Dataset Timestamp
txt.push(("at".to_string(), at.to_be_bytes().to_vec()));
}
}
Err(err) => {
warn!(tag = "meshcop", "Failed to get active dataset: {:?}", err);
}
}
// `pt` - Partition ID
txt.push(("pt".to_string(), ot_instance.get_partition_id().to_be_bytes().to_vec()));
}
txt
}
fn publish_border_agent_service(
service_instance: String,
txt: Vec<(String, Vec<u8>)>,
port: u16,
) -> impl Future<Output = Result<(), anyhow::Error>> + 'static {
let (client, server) = create_endpoints::<ServiceInstancePublicationResponder_Marker>();
let publisher = connect_to_protocol::<ServiceInstancePublisherMarker>().unwrap();
let publish_init_future = publisher
.publish_service_instance(
BORDER_AGENT_SERVICE_TYPE,
service_instance.as_str(),
&ServiceInstancePublicationOptions::default(),
client,
)
.map(|x| -> Result<(), anyhow::Error> {
match x {
Ok(Ok(x)) => Ok(x),
Ok(Err(err)) => Err(anyhow::format_err!("{:?}", err)),
Err(zx_err) => Err(zx_err.into()),
}
});
// Render out the keys and values as ascii bytes.
let txt = txt
.iter()
.map(|(key, value)| {
let mut x = key.as_bytes().to_vec();
x.push(b'=');
x.extend(value.as_slice());
x
})
.collect::<Vec<_>>();
// Prepare our static response for all queries.
let publication =
ServiceInstancePublication { port: Some(port), text: Some(txt), ..Default::default() };
let publish_responder_future = server.into_stream().unwrap().map_err(Into::into).try_for_each(
move |ServiceInstancePublicationResponder_Request::OnPublication {
publication_cause,
subtype,
source_addresses,
responder,
}| {
debug!(
tag = "meshcop",
"publish_border_agent_service: publication_cause: {publication_cause:?}"
);
debug!(tag = "meshcop", "publish_border_agent_service: publication_cause: {subtype:?}");
debug!(
tag = "meshcop",
"publish_border_agent_service: source_addresses: {source_addresses:?}"
);
debug!(
tag = "meshcop",
"publish_border_agent_service: publication: {:?}", &publication
);
// Due to https://fxbug.dev/42182233, the publication responder channel will close
// if the publisher that created it is closed.
// TODO(https://fxbug.dev/42182233): Remove this line once https://fxbug.dev/42182233 is fixed.
let _ = publisher.clone();
let result = if subtype.is_some() {
debug!(
tag = "meshcop",
"publish_border_agent_service: Subtype specified, skipping advertisement."
);
Err(OnPublicationError::DoNotRespond)
} else {
Ok(&publication)
};
futures::future::ready(
responder.send(result).context("Unable to call publication responder"),
)
},
);
futures::future::try_join(
publish_init_future.inspect_err(|err| {
error!(
tag = "meshcop",
"publish_border_agent_service: publish_init_future failed: {:?}", err
);
}),
publish_responder_future.inspect_err(|err| {
error!(
tag = "meshcop",
"publish_border_agent_service: publish_responder_future failed: {:?}", err
);
}),
)
.map_ok(|_| ())
}
async fn get_product_info() -> Result<fidl_fuchsia_hwinfo::ProductInfo, anyhow::Error> {
Ok(connect_to_protocol::<fidl_fuchsia_hwinfo::ProductMarker>()?.get_info().await?)
}
impl<OT: ot::InstanceInterface, NI, BI> OtDriver<OT, NI, BI> {
pub async fn update_border_agent_service(&self) {
let (vendor, product) = match get_product_info().await {
Ok(info) => {
let vendor = info.manufacturer.unwrap_or_else(|| "Unknown".to_string());
let model = info.model.unwrap_or_else(|| "Fuchsia".to_string());
(vendor, model)
}
Err(err) => {
warn!(tag = "meshcop", "Unable to get product info: {:?}", err);
("Unknown".to_string(), "Fuchsia".to_string())
}
};
// Add the last two bytes (in hex) of the extended address to the device name
// to make the name more stable.
let service_instance_name = {
let driver_state = self.driver_state.lock();
let ot_instance = &driver_state.ot_instance;
format!(
"{} ({})",
product,
hex::encode(&ot_instance.get_extended_address().as_slice()[6..])
)
};
let (txt, port) = {
let mut txt = self.border_agent_vendor_txt_entries.lock().await.clone();
let driver_state = self.driver_state.lock();
let ot_instance = &driver_state.ot_instance;
txt.extend(calc_meshcop_service_txt(ot_instance, &vendor, &product));
let port = if ot_instance.border_agent_get_state() != ot::BorderAgentState::Stopped {
ot_instance.border_agent_get_udp_port()
} else {
// The following comment is from the original ot-br-posix implementation:
// ---
// When thread interface is not active, the border agent is not started,
// thus it's not listening to any port and not handling requests. In such
// situation, we use a placeholder port number for publishing the MeshCoP
// service to advertise the status of the border router. One can learn
// the thread interface status from `sb` entry so it doesn't have to send
// requests to the placeholder port when border agent is not running.
BORDER_AGENT_SERVICE_PLACEHOLDER_PORT
};
(txt, port)
};
let border_agent_current_txt_entries = self.border_agent_current_txt_entries.clone();
let mut last_txt_entries = border_agent_current_txt_entries.lock().await;
if txt == *last_txt_entries {
debug!(tag = "meshcop", "update_border_agent_service: No changes.");
} else {
debug!(
tag = "meshcop",
"update_border_agent_service: Updating meshcop dns-sd: port={} txt=[PII]({:?})",
port,
txt
);
*last_txt_entries = txt.clone();
let original_task = self.border_agent_service.lock().take();
if let Some(task) = original_task {
// We must wait for the original task to fully stop.
if let Err(err) = task.cancel().transpose() {
warn!(tag="meshcop","update_border_agent_service: Previous publication task ended with an error: {:?}", err);
}
}
let task = publish_border_agent_service(service_instance_name, txt, port);
if let Err(err) = self
.border_agent_service
.lock()
.replace(fasync::Task::spawn(task))
.and_then(|x| x.cancel())
.transpose()
{
warn!(tag="meshcop","update_border_agent_service: Previous publication task ended with an error: {:?}", err);
}
}
}
}