Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add QUIC Address Discovery to iroh #3049

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
134 changes: 134 additions & 0 deletions iroh-net-report/src/ip_mapped_addrs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
use std::{
collections::BTreeMap,
net::{IpAddr, Ipv6Addr, SocketAddr},
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
};

/// The dummy port used for all mapped addresses
pub const MAPPED_ADDR_PORT: u16 = 12345;

/// Can occur when converting a [`SocketAddr`] to an [`IpMappedAddr`]
#[derive(Debug, thiserror::Error)]
#[error("Failed to convert: {0}")]
pub struct IpMappedAddrError(String);

/// A mirror for the `NodeIdMappedAddr`, mapping a fake Ipv6 address with an actual IP address.
///
/// You can consider this as nothing more than a lookup key for an IP that iroh's magicsocket knows
/// about.
///
/// And in our QUIC-facing socket APIs like iroh's `AsyncUdpSocket` it
/// comes in as the inner [`Ipv6Addr`], in those interfaces we have to be careful to do
/// the conversion to this type.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct IpMappedAddr(Ipv6Addr);

/// Counter to always generate unique addresses for `NodeIdMappedAddr`.
static IP_ADDR_COUNTER: AtomicU64 = AtomicU64::new(1);

impl IpMappedAddr {
/// The Prefix/L of our Unique Local Addresses.
const ADDR_PREFIXL: u8 = 0xfd;
/// The Global ID used in our Unique Local Addresses.
const ADDR_GLOBAL_ID: [u8; 5] = [21, 7, 10, 81, 11];
/// The Subnet ID used in our Unique Local Addresses.
const ADDR_SUBNET: [u8; 2] = [0, 1];

/// Generates a globally unique fake UDP address.
///
/// This generates and IPv6 Unique Local Address according to RFC 4193.
pub fn generate() -> Self {
let mut addr = [0u8; 16];
addr[0] = Self::ADDR_PREFIXL;
addr[1..6].copy_from_slice(&Self::ADDR_GLOBAL_ID);
addr[6..8].copy_from_slice(&Self::ADDR_SUBNET);

let counter = IP_ADDR_COUNTER.fetch_add(1, Ordering::Relaxed);
addr[8..16].copy_from_slice(&counter.to_be_bytes());

Self(Ipv6Addr::from(addr))
}

/// Return a [`SocketAddr`] from the [`IpMappedAddr`].
pub fn socket_addr(&self) -> SocketAddr {
SocketAddr::new(IpAddr::from(self.0), MAPPED_ADDR_PORT)
}
}

impl TryFrom<Ipv6Addr> for IpMappedAddr {
type Error = IpMappedAddrError;

fn try_from(value: Ipv6Addr) -> std::result::Result<Self, Self::Error> {
let octets = value.octets();
if octets[0] == Self::ADDR_PREFIXL
&& octets[1..6] == Self::ADDR_GLOBAL_ID
&& octets[6..8] == Self::ADDR_SUBNET
{
return Ok(Self(value));
}
Err(IpMappedAddrError(String::from(
"{value:?} is not an IpMappedAddr",
)))
}
}

impl std::fmt::Display for IpMappedAddr {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "IpMappedAddr({})", self.0)
}
}

#[derive(Debug, Clone)]
/// A Map of [`IpMappedAddrs`] to [`SocketAddr`]
// TODO(ramfox): before this is ready to be used beyond QAD, we should add
// mechanisms for keeping track of "aliveness" and pruning address, as we do
// with the `NodeMap`
pub struct IpMappedAddrs(Arc<std::sync::Mutex<Inner>>);

#[derive(Debug, Default)]
pub struct Inner {
by_mapped_addr: BTreeMap<IpMappedAddr, SocketAddr>,
by_socket_addr: BTreeMap<SocketAddr, IpMappedAddr>,
}

impl IpMappedAddrs {
/// Create an empty [`IpMappedAddrs`]
pub fn new() -> Self {
Self(Arc::new(std::sync::Mutex::new(Inner::default())))
}

/// Add a [`SocketAddr`] to the map and the generated [`IpMappedAddr`] it is now associated with back.
///
/// If this [`SocketAddr`] already exists in the map, it returns its associated [`IpMappedAddr`].
pub fn add(&self, ip_addr: SocketAddr) -> IpMappedAddr {
let mut inner = self.0.lock().expect("poisoned");
if let Some(mapped_addr) = inner.by_socket_addr.get(&ip_addr) {
return *mapped_addr;
}
let ip_mapped_addr = IpMappedAddr::generate();
inner.by_mapped_addr.insert(ip_mapped_addr, ip_addr);
inner.by_socket_addr.insert(ip_addr, ip_mapped_addr);
ip_mapped_addr
}

/// Get the [`IpMappedAddr`] for the given [`SocketAddr`].
pub fn get_mapped_addr(&self, ip_addr: &SocketAddr) -> Option<IpMappedAddr> {
let inner = self.0.lock().expect("poisoned");
inner.by_socket_addr.get(ip_addr).copied()
}

/// Get the [`SocketAddr`] for the given [`IpMappedAddr`].
pub fn get_ip_addr(&self, mapped_addr: &IpMappedAddr) -> Option<SocketAddr> {
let inner = self.0.lock().expect("poisoned");
inner.by_mapped_addr.get(mapped_addr).copied()
}
}

impl Default for IpMappedAddrs {
fn default() -> Self {
IpMappedAddrs::new()
}
}
30 changes: 23 additions & 7 deletions iroh-net-report/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ use tracing::{debug, error, info_span, trace, warn, Instrument};

mod defaults;
mod dns;
mod ip_mapped_addrs;
mod metrics;
mod ping;
mod reportgen;

pub use ip_mapped_addrs::{IpMappedAddr, IpMappedAddrError, IpMappedAddrs, MAPPED_ADDR_PORT};
pub use metrics::Metrics;
use reportgen::ProbeProto;
pub use reportgen::QuicConfig;
Expand Down Expand Up @@ -348,8 +350,12 @@ impl Client {
///
/// This starts a connected actor in the background. Once the client is dropped it will
/// stop running.
pub fn new(port_mapper: Option<portmapper::Client>, dns_resolver: DnsResolver) -> Result<Self> {
let mut actor = Actor::new(port_mapper, dns_resolver)?;
pub fn new(
port_mapper: Option<portmapper::Client>,
dns_resolver: DnsResolver,
ip_mapped_addrs: Option<IpMappedAddrs>,
) -> Result<Self> {
let mut actor = Actor::new(port_mapper, dns_resolver, ip_mapped_addrs)?;
let addr = actor.addr();
let task = tokio::spawn(
async move { actor.run().await }.instrument(info_span!("net_report.actor")),
Expand Down Expand Up @@ -566,14 +572,21 @@ struct Actor {

/// The DNS resolver to use for probes that need to perform DNS lookups
dns_resolver: DnsResolver,

/// The [`IpMappedAddrs`] that allows you to do QAD in iroh
ip_mapped_addrs: Option<IpMappedAddrs>,
}

impl Actor {
/// Creates a new actor.
///
/// This does not start the actor, see [`Actor::run`] for this. You should not
/// normally create this directly but rather create a [`Client`].
fn new(port_mapper: Option<portmapper::Client>, dns_resolver: DnsResolver) -> Result<Self> {
fn new(
port_mapper: Option<portmapper::Client>,
dns_resolver: DnsResolver,
ip_mapped_addrs: Option<IpMappedAddrs>,
) -> Result<Self> {
// TODO: consider an instrumented flume channel so we have metrics.
let (sender, receiver) = mpsc::channel(32);
Ok(Self {
Expand All @@ -584,6 +597,7 @@ impl Actor {
in_flight_stun_requests: Default::default(),
current_report_run: None,
dns_resolver,
ip_mapped_addrs,
})
}

Expand Down Expand Up @@ -644,6 +658,7 @@ impl Actor {
quic_config,
..
} = opts;
trace!("Attempting probes for protocols {protocols:#?}");
if self.current_report_run.is_some() {
response_tx
.send(Err(anyhow!(
Expand Down Expand Up @@ -686,6 +701,7 @@ impl Actor {
quic_config,
self.dns_resolver.clone(),
protocols,
self.ip_mapped_addrs.clone(),
);

self.current_report_run = Some(ReportRun {
Expand Down Expand Up @@ -1134,7 +1150,7 @@ mod tests {
stun_utils::serve("127.0.0.1".parse().unwrap()).await?;

let resolver = crate::dns::tests::resolver();
let mut client = Client::new(None, resolver.clone())?;
let mut client = Client::new(None, resolver.clone(), None)?;
let dm = stun_utils::relay_map_of([stun_addr].into_iter());

// Note that the ProbePlan will change with each iteration.
Expand Down Expand Up @@ -1182,7 +1198,7 @@ mod tests {

// Now create a client and generate a report.
let resolver = crate::dns::tests::resolver();
let mut client = Client::new(None, resolver.clone())?;
let mut client = Client::new(None, resolver.clone(), None)?;

let r = client.get_report(dm, None, None, None).await?;
let mut r: Report = (*r).clone();
Expand Down Expand Up @@ -1385,7 +1401,7 @@ mod tests {
let resolver = crate::dns::tests::resolver();
for mut tt in tests {
println!("test: {}", tt.name);
let mut actor = Actor::new(None, resolver.clone()).unwrap();
let mut actor = Actor::new(None, resolver.clone(), None).unwrap();
for s in &mut tt.steps {
// trigger the timer
time::advance(Duration::from_secs(s.after)).await;
Expand Down Expand Up @@ -1420,7 +1436,7 @@ mod tests {
dbg!(&dm);

let resolver = crate::dns::tests::resolver().clone();
let mut client = Client::new(None, resolver)?;
let mut client = Client::new(None, resolver, None)?;

// Set up an external socket to send STUN requests from, this will be discovered as
// our public socket address by STUN. We send back any packets received on this
Expand Down
Loading
Loading