missing file
This commit is contained in:
parent
dac8e75229
commit
d5a4be8a36
582
veilid-core/src/network_manager/native/discovery_context.rs
Normal file
582
veilid-core/src/network_manager/native/discovery_context.rs
Normal file
@ -0,0 +1,582 @@
|
||||
/// Context detection of public dial info for a single protocol and address type
|
||||
/// Also performs UPNP/IGD mapping if enabled and possible
|
||||
use super::*;
|
||||
use futures_util::stream::FuturesUnordered;
|
||||
|
||||
const PORT_MAP_VALIDATE_TRY_COUNT: usize = 3;
|
||||
const PORT_MAP_VALIDATE_DELAY_MS: u32 = 500;
|
||||
const PORT_MAP_TRY_COUNT: usize = 3;
|
||||
|
||||
// Detection result of dial info detection futures
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DetectedDialInfo {
|
||||
SymmetricNAT,
|
||||
Detected(DialInfoDetail),
|
||||
}
|
||||
|
||||
// Result of checking external address
|
||||
#[derive(Clone, Debug)]
|
||||
struct ExternalInfo {
|
||||
dial_info: DialInfo,
|
||||
address: SocketAddress,
|
||||
node: NodeRef,
|
||||
}
|
||||
|
||||
struct DiscoveryContextInner {
|
||||
// first node contacted
|
||||
external_1: Option<ExternalInfo>,
|
||||
// second node contacted
|
||||
external_2: Option<ExternalInfo>,
|
||||
}
|
||||
|
||||
struct DiscoveryContextUnlockedInner {
|
||||
routing_table: RoutingTable,
|
||||
net: Network,
|
||||
// per-protocol
|
||||
intf_addrs: Vec<SocketAddress>,
|
||||
protocol_type: ProtocolType,
|
||||
address_type: AddressType,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DiscoveryContext {
|
||||
unlocked_inner: Arc<DiscoveryContextUnlockedInner>,
|
||||
inner: Arc<Mutex<DiscoveryContextInner>>,
|
||||
}
|
||||
|
||||
impl DiscoveryContext {
|
||||
pub fn new(
|
||||
routing_table: RoutingTable,
|
||||
net: Network,
|
||||
protocol_type: ProtocolType,
|
||||
address_type: AddressType,
|
||||
) -> Self {
|
||||
let intf_addrs =
|
||||
Self::get_local_addresses(routing_table.clone(), protocol_type, address_type);
|
||||
|
||||
Self {
|
||||
unlocked_inner: Arc::new(DiscoveryContextUnlockedInner {
|
||||
routing_table,
|
||||
net,
|
||||
intf_addrs,
|
||||
protocol_type,
|
||||
address_type,
|
||||
}),
|
||||
inner: Arc::new(Mutex::new(DiscoveryContextInner {
|
||||
external_1: None,
|
||||
external_2: None,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
///////
|
||||
// Utilities
|
||||
|
||||
// This pulls the already-detected local interface dial info from the routing table
|
||||
#[instrument(level = "trace", skip(routing_table), ret)]
|
||||
fn get_local_addresses(
|
||||
routing_table: RoutingTable,
|
||||
protocol_type: ProtocolType,
|
||||
address_type: AddressType,
|
||||
) -> Vec<SocketAddress> {
|
||||
let filter = DialInfoFilter::all()
|
||||
.with_protocol_type(protocol_type)
|
||||
.with_address_type(address_type);
|
||||
routing_table
|
||||
.dial_info_details(RoutingDomain::LocalNetwork)
|
||||
.iter()
|
||||
.filter_map(|did| {
|
||||
if did.dial_info.matches_filter(&filter) {
|
||||
Some(did.dial_info.socket_address())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Ask for a public address check from a particular noderef
|
||||
// This is done over the normal port using RPC
|
||||
#[instrument(level = "trace", skip(self), ret)]
|
||||
async fn request_public_address(&self, node_ref: NodeRef) -> Option<SocketAddress> {
|
||||
let rpc = self.unlocked_inner.routing_table.rpc_processor();
|
||||
|
||||
let res = network_result_value_or_log!(match rpc.rpc_call_status(Destination::direct(node_ref.clone())).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
log_net!(error
|
||||
"failed to get status answer from {:?}: {}",
|
||||
node_ref, e
|
||||
);
|
||||
return None;
|
||||
}
|
||||
} => [ format!(": node_ref={}", node_ref) ] {
|
||||
return None;
|
||||
}
|
||||
);
|
||||
|
||||
log_net!(
|
||||
"request_public_address {:?}: Value({:?})",
|
||||
node_ref,
|
||||
res.answer
|
||||
);
|
||||
res.answer.map(|si| si.socket_address)
|
||||
}
|
||||
|
||||
// find fast peers with a particular address type, and ask them to tell us what our external address is
|
||||
// This is done over the normal port using RPC
|
||||
#[instrument(level = "trace", skip(self), ret)]
|
||||
async fn discover_external_addresses(&self) -> bool {
|
||||
let node_count = {
|
||||
let config = self.unlocked_inner.routing_table.network_manager().config();
|
||||
let c = config.get();
|
||||
c.network.dht.max_find_node_count as usize
|
||||
};
|
||||
let routing_domain = RoutingDomain::PublicInternet;
|
||||
let protocol_type = self.unlocked_inner.protocol_type;
|
||||
let address_type = self.unlocked_inner.address_type;
|
||||
|
||||
// Build an filter that matches our protocol and address type
|
||||
// and excludes relayed nodes so we can get an accurate external address
|
||||
let dial_info_filter = DialInfoFilter::all()
|
||||
.with_protocol_type(protocol_type)
|
||||
.with_address_type(address_type);
|
||||
let inbound_dial_info_entry_filter = RoutingTable::make_inbound_dial_info_entry_filter(
|
||||
routing_domain,
|
||||
dial_info_filter.clone(),
|
||||
);
|
||||
let disallow_relays_filter = Box::new(
|
||||
move |rti: &RoutingTableInner, v: Option<Arc<BucketEntry>>| {
|
||||
let v = v.unwrap();
|
||||
v.with(rti, |_rti, e| {
|
||||
if let Some(n) = e.signed_node_info(routing_domain) {
|
||||
n.relay_ids().is_empty()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
},
|
||||
) as RoutingTableEntryFilter;
|
||||
let will_validate_dial_info_filter = Box::new(
|
||||
move |rti: &RoutingTableInner, v: Option<Arc<BucketEntry>>| {
|
||||
let entry = v.unwrap();
|
||||
entry.with(rti, move |_rti, e| {
|
||||
e.node_info(routing_domain)
|
||||
.map(|ni| {
|
||||
ni.has_capability(CAP_VALIDATE_DIAL_INFO)
|
||||
&& ni.is_fully_direct_inbound()
|
||||
})
|
||||
.unwrap_or(false)
|
||||
})
|
||||
},
|
||||
) as RoutingTableEntryFilter;
|
||||
|
||||
let filters = VecDeque::from([
|
||||
inbound_dial_info_entry_filter,
|
||||
disallow_relays_filter,
|
||||
will_validate_dial_info_filter,
|
||||
]);
|
||||
|
||||
// Find public nodes matching this filter
|
||||
let nodes = self
|
||||
.unlocked_inner
|
||||
.routing_table
|
||||
.find_fast_public_nodes_filtered(node_count, filters);
|
||||
if nodes.is_empty() {
|
||||
log_net!(debug
|
||||
"no external address detection peers of type {:?}:{:?}",
|
||||
protocol_type,
|
||||
address_type
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// For each peer, ask them for our public address, filtering on desired dial info
|
||||
let mut unord = FuturesUnordered::new();
|
||||
|
||||
let get_public_address_func = |node: NodeRef| {
|
||||
let this = self.clone();
|
||||
let node = node.filtered_clone(
|
||||
NodeRefFilter::new()
|
||||
.with_routing_domain(routing_domain)
|
||||
.with_dial_info_filter(dial_info_filter.clone()),
|
||||
);
|
||||
async move {
|
||||
if let Some(address) = this.request_public_address(node.clone()).await {
|
||||
let dial_info = this
|
||||
.unlocked_inner
|
||||
.net
|
||||
.make_dial_info(address, this.unlocked_inner.protocol_type);
|
||||
return Some(ExternalInfo {
|
||||
dial_info,
|
||||
address,
|
||||
node,
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let mut external_address_infos = Vec::new();
|
||||
|
||||
for ni in 0..nodes.len() - 1 {
|
||||
let node = nodes[ni].clone();
|
||||
|
||||
let gpa_future = get_public_address_func(node);
|
||||
unord.push(gpa_future);
|
||||
|
||||
// Always process two at a time so we get both addresses in parallel if possible
|
||||
if unord.len() == 2 {
|
||||
// Process one
|
||||
if let Some(Some(ei)) = unord.next().await {
|
||||
external_address_infos.push(ei);
|
||||
if external_address_infos.len() == 2 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Finish whatever is left if we need to
|
||||
if external_address_infos.len() < 2 {
|
||||
while let Some(res) = unord.next().await {
|
||||
if let Some(ei) = res {
|
||||
external_address_infos.push(ei);
|
||||
if external_address_infos.len() == 2 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if external_address_infos.len() < 2 {
|
||||
log_net!(debug "not enough peers responded with an external address");
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
let mut inner = self.inner.lock();
|
||||
inner.external_1 = Some(external_address_infos[0].clone());
|
||||
log_net!(debug "external_1: {:?}", inner.external_1);
|
||||
inner.external_2 = Some(external_address_infos[1].clone());
|
||||
log_net!(debug "external_2: {:?}", inner.external_2);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip(self), ret)]
|
||||
async fn validate_dial_info(
|
||||
&self,
|
||||
node_ref: NodeRef,
|
||||
dial_info: DialInfo,
|
||||
redirect: bool,
|
||||
) -> bool {
|
||||
let rpc_processor = self.unlocked_inner.routing_table.rpc_processor();
|
||||
|
||||
// asking for node validation doesn't have to use the dial info filter of the dial info we are validating
|
||||
let mut node_ref = node_ref.clone();
|
||||
node_ref.set_filter(None);
|
||||
|
||||
// ask the node to send us a dial info validation receipt
|
||||
let out = rpc_processor
|
||||
.rpc_call_validate_dial_info(node_ref.clone(), dial_info, redirect)
|
||||
.await
|
||||
.map_err(logthru_net!(
|
||||
"failed to send validate_dial_info to {:?}",
|
||||
node_ref
|
||||
))
|
||||
.unwrap_or(false);
|
||||
out
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip(self), ret)]
|
||||
async fn try_upnp_port_mapping(&self) -> Option<DialInfo> {
|
||||
let protocol_type = self.unlocked_inner.protocol_type;
|
||||
let low_level_protocol_type = protocol_type.low_level_protocol_type();
|
||||
let address_type = self.unlocked_inner.address_type;
|
||||
let local_port = self
|
||||
.unlocked_inner
|
||||
.net
|
||||
.get_local_port(protocol_type)
|
||||
.unwrap();
|
||||
let external_1 = self.inner.lock().external_1.as_ref().unwrap().clone();
|
||||
|
||||
let igd_manager = self.unlocked_inner.net.unlocked_inner.igd_manager.clone();
|
||||
let mut tries = 0;
|
||||
loop {
|
||||
tries += 1;
|
||||
|
||||
// Attempt a port mapping. If this doesn't succeed, it's not going to
|
||||
let Some(mapped_external_address) = igd_manager
|
||||
.map_any_port(low_level_protocol_type, address_type, local_port, Some(external_1.address.to_ip_addr()))
|
||||
.await else
|
||||
{
|
||||
return None;
|
||||
};
|
||||
|
||||
// Make dial info from the port mapping
|
||||
let external_mapped_dial_info = self.unlocked_inner.net.make_dial_info(
|
||||
SocketAddress::from_socket_addr(mapped_external_address),
|
||||
protocol_type,
|
||||
);
|
||||
|
||||
// Attempt to validate the port mapping
|
||||
let mut validate_tries = 0;
|
||||
loop {
|
||||
validate_tries += 1;
|
||||
|
||||
// Ensure people can reach us. If we're firewalled off, this is useless
|
||||
if self
|
||||
.validate_dial_info(
|
||||
external_1.node.clone(),
|
||||
external_mapped_dial_info.clone(),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Some(external_mapped_dial_info);
|
||||
}
|
||||
|
||||
if validate_tries == PORT_MAP_VALIDATE_TRY_COUNT {
|
||||
log_net!(debug "UPNP port mapping succeeded but port {}/{} is still unreachable.\nretrying\n",
|
||||
local_port, match low_level_protocol_type {
|
||||
LowLevelProtocolType::UDP => "udp",
|
||||
LowLevelProtocolType::TCP => "tcp",
|
||||
});
|
||||
sleep(PORT_MAP_VALIDATE_DELAY_MS).await
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Release the mapping if we're still unreachable
|
||||
let _ = igd_manager
|
||||
.unmap_port(
|
||||
low_level_protocol_type,
|
||||
address_type,
|
||||
external_1.address.port(),
|
||||
)
|
||||
.await;
|
||||
|
||||
if tries == PORT_MAP_TRY_COUNT {
|
||||
warn!("UPNP port mapping succeeded but port {}/{} is still unreachable.\nYou may need to add a local firewall allowed port on this machine.\n",
|
||||
local_port, match low_level_protocol_type {
|
||||
LowLevelProtocolType::UDP => "udp",
|
||||
LowLevelProtocolType::TCP => "tcp",
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
///////
|
||||
// Per-protocol discovery routines
|
||||
|
||||
// If we know we are not behind NAT, check our firewall status
|
||||
#[instrument(level = "trace", skip(self), ret)]
|
||||
async fn protocol_process_no_nat(&self) -> DetectedDialInfo {
|
||||
let external_1 = self.inner.lock().external_1.as_ref().unwrap().clone();
|
||||
|
||||
// Do a validate_dial_info on the external address from a redirected node
|
||||
if self
|
||||
.validate_dial_info(external_1.node.clone(), external_1.dial_info.clone(), true)
|
||||
.await
|
||||
{
|
||||
// Add public dial info with Direct dialinfo class
|
||||
DetectedDialInfo::Detected(DialInfoDetail {
|
||||
dial_info: external_1.dial_info.clone(),
|
||||
class: DialInfoClass::Direct,
|
||||
})
|
||||
} else {
|
||||
// Add public dial info with Blocked dialinfo class
|
||||
DetectedDialInfo::Detected(DialInfoDetail {
|
||||
dial_info: external_1.dial_info.clone(),
|
||||
class: DialInfoClass::Blocked,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// If we know we are behind NAT check what kind
|
||||
#[instrument(level = "trace", skip(self), ret)]
|
||||
async fn protocol_process_nat(&self) -> Option<DetectedDialInfo> {
|
||||
// Get the external dial info for our use here
|
||||
let external_1 = self.inner.lock().external_1.as_ref().unwrap().clone();
|
||||
let external_2 = self.inner.lock().external_2.as_ref().unwrap().clone();
|
||||
|
||||
// If we have two different external addresses, then this is a symmetric NAT
|
||||
if external_2.address != external_1.address {
|
||||
// No more retries
|
||||
return Some(DetectedDialInfo::SymmetricNAT);
|
||||
}
|
||||
|
||||
// Do these detections in parallel, but with ordering preference
|
||||
let mut ord = FuturesOrdered::new();
|
||||
|
||||
// Manual Mapping Detection
|
||||
///////////
|
||||
let this = self.clone();
|
||||
if let Some(local_port) = self
|
||||
.unlocked_inner
|
||||
.net
|
||||
.get_local_port(self.unlocked_inner.protocol_type)
|
||||
{
|
||||
if external_1.dial_info.port() != local_port {
|
||||
let c_external_1 = external_1.clone();
|
||||
let do_manual_map_fut: SendPinBoxFuture<Option<DetectedDialInfo>> =
|
||||
Box::pin(async move {
|
||||
// Do a validate_dial_info on the external address, but with the same port as the local port of local interface, from a redirected node
|
||||
// This test is to see if a node had manual port forwarding done with the same port number as the local listener
|
||||
let mut external_1_dial_info_with_local_port =
|
||||
c_external_1.dial_info.clone();
|
||||
external_1_dial_info_with_local_port.set_port(local_port);
|
||||
|
||||
if this
|
||||
.validate_dial_info(
|
||||
c_external_1.node.clone(),
|
||||
external_1_dial_info_with_local_port.clone(),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
{
|
||||
// Add public dial info with Direct dialinfo class
|
||||
return Some(DetectedDialInfo::Detected(DialInfoDetail {
|
||||
dial_info: external_1_dial_info_with_local_port,
|
||||
class: DialInfoClass::Direct,
|
||||
}));
|
||||
}
|
||||
|
||||
None
|
||||
});
|
||||
ord.push_back(do_manual_map_fut);
|
||||
}
|
||||
}
|
||||
|
||||
// Full Cone NAT Detection
|
||||
///////////
|
||||
let this = self.clone();
|
||||
let c_external_1 = external_1.clone();
|
||||
let do_full_cone_fut: SendPinBoxFuture<Option<DetectedDialInfo>> = Box::pin(async move {
|
||||
// Let's see what kind of NAT we have
|
||||
// Does a redirected dial info validation from a different address and a random port find us?
|
||||
if this
|
||||
.validate_dial_info(
|
||||
c_external_1.node.clone(),
|
||||
c_external_1.dial_info.clone(),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
{
|
||||
// Yes, another machine can use the dial info directly, so Full Cone
|
||||
// Add public dial info with full cone NAT network class
|
||||
|
||||
return Some(DetectedDialInfo::Detected(DialInfoDetail {
|
||||
dial_info: c_external_1.dial_info,
|
||||
class: DialInfoClass::FullConeNAT,
|
||||
}));
|
||||
}
|
||||
None
|
||||
});
|
||||
ord.push_back(do_full_cone_fut);
|
||||
|
||||
// Run detections in parallel and take the first one, ordered by preference, that returns a result
|
||||
while let Some(res) = ord.next().await {
|
||||
if let Some(ddi) = res {
|
||||
return Some(ddi);
|
||||
}
|
||||
}
|
||||
|
||||
// We are restricted, determine what kind of restriction
|
||||
|
||||
// If we're going to end up as a restricted NAT of some sort
|
||||
// Address is the same, so it's address or port restricted
|
||||
|
||||
// Do a validate_dial_info on the external address from a random port
|
||||
if self
|
||||
.validate_dial_info(external_2.node.clone(), external_1.dial_info.clone(), false)
|
||||
.await
|
||||
{
|
||||
// Got a reply from a non-default port, which means we're only address restricted
|
||||
return Some(DetectedDialInfo::Detected(DialInfoDetail {
|
||||
dial_info: external_1.dial_info.clone(),
|
||||
class: DialInfoClass::AddressRestrictedNAT,
|
||||
}));
|
||||
}
|
||||
// Didn't get a reply from a non-default port, which means we are also port restricted
|
||||
Some(DetectedDialInfo::Detected(DialInfoDetail {
|
||||
dial_info: external_1.dial_info.clone(),
|
||||
class: DialInfoClass::PortRestrictedNAT,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Add discovery futures to an unordered set that may detect dialinfo when they complete
|
||||
pub async fn discover(
|
||||
&self,
|
||||
unord: &mut FuturesUnordered<SendPinBoxFuture<Option<DetectedDialInfo>>>,
|
||||
) {
|
||||
let (mut retry_count, enable_upnp) = {
|
||||
let c = self.unlocked_inner.net.config.get();
|
||||
(c.network.restricted_nat_retries, c.network.upnp)
|
||||
};
|
||||
|
||||
// Do this right away because it's fast and every detection is going to need it
|
||||
// Get our external addresses from two fast nodes
|
||||
if !self.discover_external_addresses().await {
|
||||
// If we couldn't get an external address, then we should just try the whole network class detection again later
|
||||
return;
|
||||
}
|
||||
|
||||
// UPNP Automatic Mapping
|
||||
///////////
|
||||
if enable_upnp {
|
||||
let this = self.clone();
|
||||
let do_mapped_fut: SendPinBoxFuture<Option<DetectedDialInfo>> = Box::pin(async move {
|
||||
// Attempt a port mapping via all available and enabled mechanisms
|
||||
// Try this before the direct mapping in the event that we are restarting
|
||||
// and may not have recorded a mapping created the last time
|
||||
if let Some(external_mapped_dial_info) = this.try_upnp_port_mapping().await {
|
||||
// Got a port mapping, let's use it
|
||||
return Some(DetectedDialInfo::Detected(DialInfoDetail {
|
||||
dial_info: external_mapped_dial_info.clone(),
|
||||
class: DialInfoClass::Mapped,
|
||||
}));
|
||||
}
|
||||
None
|
||||
});
|
||||
unord.push(do_mapped_fut);
|
||||
}
|
||||
|
||||
// NAT Detection
|
||||
///////////
|
||||
|
||||
// If our local interface list contains external_1 then there is no NAT in place
|
||||
let external_1 = self.inner.lock().external_1.as_ref().unwrap().clone();
|
||||
|
||||
if self.unlocked_inner.intf_addrs.contains(&external_1.address) {
|
||||
this.protocol_process_no_nat(unord).await
|
||||
xxx continue here
|
||||
} else {
|
||||
// Loop for restricted NAT retries
|
||||
let this = self.clone();
|
||||
let do_nat_fut: SendPinBoxFuture<Option<DetectedDialInfo>> = Box::pin(async move {
|
||||
loop {
|
||||
// There is -some NAT-
|
||||
if let Some(ddi) = this.protocol_process_nat().await {
|
||||
if let DetectedDialInfo::Detected(did) = &ddi {
|
||||
// If we got something better than restricted NAT or we're done retrying
|
||||
if did.class < DialInfoClass::AddressRestrictedNAT || retry_count == 0 {
|
||||
return Some(ddi);
|
||||
}
|
||||
}
|
||||
}
|
||||
if retry_count == 0 {
|
||||
break;
|
||||
}
|
||||
retry_count -= 1;
|
||||
}
|
||||
None
|
||||
});
|
||||
unord.push(do_nat_fut);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user