veilid/veilid-core/src/routing_table/tasks/bootstrap.rs
2023-02-13 16:12:46 -05:00

369 lines
14 KiB
Rust

use super::*;
use futures_util::stream::{FuturesUnordered, StreamExt};
use stop_token::future::FutureExt as StopFutureExt;
pub const BOOTSTRAP_TXT_VERSION_0: u8 = 0;
#[derive(Clone, Debug)]
pub struct BootstrapRecord {
node_ids: TypedKeySet,
envelope_support: Vec<u8>,
dial_info_details: Vec<DialInfoDetail>,
}
impl BootstrapRecord {
pub fn merge(&mut self, other: &BootstrapRecord) {
self.node_ids.add_all(&other.node_ids);
for x in other.envelope_support {
if !self.envelope_support.contains(&x) {
self.envelope_support.push(x);
self.envelope_support.sort();
}
}
for did in &other.dial_info_details {
if !self.dial_info_details.contains(did) {
self.dial_info_details.push(did.clone());
}
}
}
}
impl RoutingTable {
/// Process bootstrap version 0
async fn process_bootstrap_records_v0(
&self,
records: Vec<String>,
) -> EyreResult<Option<BootstrapRecord>> {
// Bootstrap TXT Record Format Version 0:
// txt_version|envelope_support|node_ids|hostname|dialinfoshort*
//
// Split bootstrap node record by '|' and then lists by ','. Example:
// 0|0|VLD0:7lxDEabK_qgjbe38RtBa3IZLrud84P6NhGP-pRTZzdQ|bootstrap-1.dev.veilid.net|T5150,U5150,W5150/ws
if records.len() != 5 {
bail!("invalid number of fields in bootstrap v0 txt record");
}
// Envelope support
let mut envelope_support = Vec::new();
for ess in records[1].split(",") {
let ess = ess.trim();
let es = match records[1].parse::<u8>() {
Ok(v) => v,
Err(e) => {
bail!(
"invalid envelope version specified in bootstrap node txt record: {}",
e
);
}
};
envelope_support.push(es);
}
envelope_support.dedup();
envelope_support.sort();
// Node Id
let node_ids = TypedKeySet::new();
for node_id_str in records[2].split(",") {
let node_id_str = node_id_str.trim();
let node_id = match TypedKey::from_str(&node_id_str) {
Ok(v) => v,
Err(e) => {
bail!(
"Invalid node id in bootstrap node record {}: {}",
node_id_str,
e
);
}
};
}
// If this is our own node id, then we skip it for bootstrap, in case we are a bootstrap node
if self.unlocked_inner.matches_own_node_id(&node_ids) {
return Ok(None);
}
// Hostname
let hostname_str = records[3].trim();
// Resolve each record and store in node dial infos list
let mut dial_info_details = Vec::new();
for rec in records[4].split(",") {
let rec = rec.trim();
let dial_infos = match DialInfo::try_vec_from_short(rec, hostname_str) {
Ok(dis) => dis,
Err(e) => {
warn!("Couldn't resolve bootstrap node dial info {}: {}", rec, e);
continue;
}
};
for di in dial_infos {
dial_info_details.push(DialInfoDetail {
dial_info: di,
class: DialInfoClass::Direct,
});
}
}
Ok(Some(BootstrapRecord {
node_ids,
envelope_support,
dial_info_details,
}))
}
// Bootstrap lookup process
#[instrument(level = "trace", skip(self), ret, err)]
pub(crate) async fn resolve_bootstrap(
&self,
bootstrap: Vec<String>,
) -> EyreResult<Vec<BootstrapRecord>> {
// Resolve from bootstrap root to bootstrap hostnames
let mut bsnames = Vec::<String>::new();
for bh in bootstrap {
// Get TXT record for bootstrap (bootstrap.veilid.net, or similar)
let records = intf::txt_lookup(&bh).await?;
for record in records {
// Split the bootstrap name record by commas
for rec in record.split(',') {
let rec = rec.trim();
// If the name specified is fully qualified, go with it
let bsname = if rec.ends_with('.') {
rec.to_string()
}
// If the name is not fully qualified, prepend it to the bootstrap name
else {
format!("{}.{}", rec, bh)
};
// Add to the list of bootstrap name to look up
bsnames.push(bsname);
}
}
}
// Get bootstrap nodes from hostnames concurrently
let mut unord = FuturesUnordered::new();
for bsname in bsnames {
unord.push(
async move {
// look up boostrap node txt records
let bsnirecords = match intf::txt_lookup(&bsname).await {
Err(e) => {
warn!("bootstrap node txt lookup failed for {}: {}", bsname, e);
return None;
}
Ok(v) => v,
};
// for each record resolve into key/bootstraprecord pairs
let mut bootstrap_records: Vec<BootstrapRecord> = Vec::new();
for bsnirecord in bsnirecords {
// All formats split on '|' character
let records: Vec<String> = bsnirecord
.trim()
.split('|')
.map(|x| x.trim().to_owned())
.collect();
// Bootstrap TXT record version
let txt_version: u8 = match records[0].parse::<u8>() {
Ok(v) => v,
Err(e) => {
warn!(
"invalid txt_version specified in bootstrap node txt record: {}",
e
);
continue;
}
};
let bootstrap_record = match txt_version {
BOOTSTRAP_TXT_VERSION_0 => {
match self.process_bootstrap_records_v0(records).await {
Err(e) => {
warn!(
"couldn't process v0 bootstrap records from {}: {}",
bsname, e
);
continue;
}
Ok(Some(v)) => v,
Ok(None) => {
// skipping
continue;
}
}
}
_ => {
warn!("unsupported bootstrap txt record version");
continue;
}
};
bootstrap_records.push(bootstrap_record);
}
Some(bootstrap_records)
}
.instrument(Span::current()),
);
}
let mut merged_bootstrap_records: Vec<BootstrapRecord> = Vec::new();
while let Some(bootstrap_records) = unord.next().await {
let Some(bootstrap_records) = bootstrap_records else {
continue;
};
for mut bsrec in bootstrap_records {
let mut mbi = 0;
while mbi < merged_bootstrap_records.len() {
let mbr = &mut merged_bootstrap_records[mbi];
if mbr.node_ids.contains_any(&bsrec.node_ids) {
// Merge record, pop this one out
let mbr = merged_bootstrap_records.remove(mbi);
bsrec.merge(&mbr);
} else {
// No overlap, go to next record
mbi += 1;
}
}
// Append merged record
merged_bootstrap_records.push(bsrec);
}
}
// ensure dial infos are sorted
for mbr in &mut merged_bootstrap_records {
mbr.dial_info_details.sort();
}
Ok(merged_bootstrap_records)
}
// 'direct' bootstrap task routine for systems incapable of resolving TXT records, such as browser WASM
#[instrument(level = "trace", skip(self), err)]
pub(crate) async fn direct_bootstrap_task_routine(
self,
stop_token: StopToken,
bootstrap_dialinfos: Vec<DialInfo>,
) -> EyreResult<()> {
let mut unord = FuturesUnordered::new();
let network_manager = self.network_manager();
for bootstrap_di in bootstrap_dialinfos {
log_rtab!(debug "direct bootstrap with: {}", bootstrap_di);
let peer_info = network_manager.boot_request(bootstrap_di).await?;
log_rtab!(debug " direct bootstrap peerinfo: {:?}", peer_info);
// Got peer info, let's add it to the routing table
for pi in peer_info {
// Register the node
if let Some(nr) =
self.register_node_with_peer_info(RoutingDomain::PublicInternet, pi, false)
{
// Add this our futures to process in parallel
let routing_table = self.clone();
unord.push(
// lets ask bootstrap to find ourselves now
async move { routing_table.reverse_find_node(nr, true).await }
.instrument(Span::current()),
);
}
}
}
// Wait for all bootstrap operations to complete before we complete the singlefuture
while let Ok(Some(_)) = unord.next().timeout_at(stop_token.clone()).await {}
Ok(())
}
#[instrument(level = "trace", skip(self), err)]
pub(crate) async fn bootstrap_task_routine(self, stop_token: StopToken) -> EyreResult<()> {
let bootstrap = self
.unlocked_inner
.with_config(|c| c.network.routing_table.bootstrap.clone());
// Don't bother if bootstraps aren't configured
if bootstrap.is_empty() {
return Ok(());
}
log_rtab!(debug "--- bootstrap_task");
// See if we are specifying a direct dialinfo for bootstrap, if so use the direct mechanism
let mut bootstrap_dialinfos = Vec::<DialInfo>::new();
for b in &bootstrap {
if let Ok(bootstrap_di_vec) = DialInfo::try_vec_from_url(&b) {
for bootstrap_di in bootstrap_di_vec {
bootstrap_dialinfos.push(bootstrap_di);
}
}
}
if bootstrap_dialinfos.len() > 0 {
return self
.direct_bootstrap_task_routine(stop_token, bootstrap_dialinfos)
.await;
}
// If not direct, resolve bootstrap servers and recurse their TXT entries
let bsrecs = self.resolve_bootstrap(bootstrap).await?;
// Run all bootstrap operations concurrently
let mut unord = FuturesUnordered::new();
for bsrec in bsrecs {
log_rtab!(
"--- bootstrapping {} with {:?}",
&bsrec.node_ids,
&bsrec.dial_info_details
);
// Get crypto support from list of node ids
let crypto_support = bsrec.node_ids.kinds();
// Make unsigned SignedNodeInfo
let sni = SignedNodeInfo::Direct(SignedDirectNodeInfo::with_no_signature(NodeInfo {
network_class: NetworkClass::InboundCapable, // Bootstraps are always inbound capable
outbound_protocols: ProtocolTypeSet::only(ProtocolType::UDP), // Bootstraps do not participate in relaying and will not make outbound requests, but will have UDP enabled
address_types: AddressTypeSet::all(), // Bootstraps are always IPV4 and IPV6 capable
envelope_support: bsrec.envelope_support, // Envelope support is as specified in the bootstrap list
crypto_support, // Crypto support is derived from list of node ids
dial_info_detail_list: bsrec.dial_info_details, // Dial info is as specified in the bootstrap list
}));
let pi = PeerInfo::new(bsrec.node_ids, sni);
if let Some(nr) =
self.register_node_with_peer_info(RoutingDomain::PublicInternet, pi, true)
{
// Add this our futures to process in parallel
let routing_table = self.clone();
unord.push(
async move {
// Need VALID signed peer info, so ask bootstrap to find_node of itself
// which will ensure it has the bootstrap's signed peer info as part of the response
let _ = routing_table.find_target(nr.clone()).await;
// Ensure we got the signed peer info
if !nr.signed_node_info_has_valid_signature(RoutingDomain::PublicInternet) {
log_rtab!(warn
"bootstrap at {:?} did not return valid signed node info",
nr
);
// If this node info is invalid, it will time out after being unpingable
} else {
// otherwise this bootstrap is valid, lets ask it to find ourselves now
routing_table.reverse_find_node(nr, true).await
}
}
.instrument(Span::current()),
);
}
}
// Wait for all bootstrap operations to complete before we complete the singlefuture
while let Ok(Some(_)) = unord.next().timeout_at(stop_token.clone()).await {}
Ok(())
}
}