Compare commits

..

3 commits

Author SHA1 Message Date
Baptiste Jonglez
c5d6840e27 Update README with new feature set 2025-02-20 00:44:52 +01:00
Baptiste Jonglez
6614dfaeb6 Rename STUN actor to autodiscovery actor 2025-02-20 00:44:52 +01:00
Baptiste Jonglez
6147bb0b8c Implement IPv6 address discovery using local address instead of STUN
We ask the kernel what source IPv6 address it would use to communicate
with a target global IPv6 address (without actually sending any packet).

It should be more robust than depending on a STUN server on the Internet.

This code does the equivalent of:

    ip -j -6 r get 2001::1 | jq '.[0].prefsrc'
2025-02-20 00:44:52 +01:00
6 changed files with 119 additions and 29 deletions

View file

@ -5,9 +5,14 @@ Diplonat
## Feature set ## Feature set
* [X] (Re)Configure NAT via UPNP/IGD (prio: high) Diplonat performs two main tasks:
* [X] (Re)Configure iptables (prio: low)
* [ ] (Re)Configure DNS via ??? (prio: low) 1) ensure that all services are accessible from the Internet
- it detects services by watching Consul and looking for a special "diplonat" tag (see below)
- for each service, it configures the host firewall with iptables and the router NAT with IGD
2) autodiscovery of the public IP addresses of the local node
- it uses STUN to a remote server for IPv4, and looks locally for a usable IPv6 address
- it then writes the discovered IP addresses to Consul, so that other services can use them (D53 for the DNS, Garage, Jitsi...)
## Understand scope ## Understand scope

View file

@ -1,11 +1,11 @@
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::net::{IpAddr, IpAddr::V6, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket};
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use log::*; use log::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::config::{RuntimeConfigConsul, RuntimeConfigStun}; use crate::config::{RuntimeConfigAutoDiscovery, RuntimeConfigConsul};
use crate::consul; use crate::consul;
/// If autodiscovery returns None but an address was obtained less than /// If autodiscovery returns None but an address was obtained less than
@ -13,12 +13,12 @@ use crate::consul;
/// in the Consul db instead of insterting a None. /// in the Consul db instead of insterting a None.
const PERSIST_SOME_RESULT_DURATION_SECS: u64 = 900; const PERSIST_SOME_RESULT_DURATION_SECS: u64 = 900;
pub struct StunActor { pub struct AutoDiscoveryActor {
consul: consul::Consul, consul: consul::Consul,
refresh_time: Duration, refresh_time: Duration,
autodiscovery_v4: StunAutodiscovery, autodiscovery_v4: StunAutodiscovery,
autodiscovery_v6: StunAutodiscovery, autodiscovery_v6: Localv6AddressAutodiscovery,
} }
pub struct StunAutodiscovery { pub struct StunAutodiscovery {
@ -28,35 +28,38 @@ pub struct StunAutodiscovery {
last_result: Option<AutodiscoverResult>, last_result: Option<AutodiscoverResult>,
} }
pub struct Localv6AddressAutodiscovery {
consul_key: String,
last_result: Option<AutodiscoverResult>,
}
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct AutodiscoverResult { pub struct AutodiscoverResult {
pub timestamp: u64, pub timestamp: u64,
pub address: Option<IpAddr>, pub address: Option<IpAddr>,
} }
impl StunActor { impl AutoDiscoveryActor {
pub fn new( pub fn new(
consul_config: &RuntimeConfigConsul, consul_config: &RuntimeConfigConsul,
stun_config: &RuntimeConfigStun, autodiscovery_config: &RuntimeConfigAutoDiscovery,
node: &str, node: &str,
) -> Self { ) -> Self {
assert!(stun_config assert!(autodiscovery_config
.stun_server_v4 .stun_server_v4
.map(|x| x.is_ipv4()) .map(|x| x.is_ipv4())
.unwrap_or(true)); .unwrap_or(true));
assert!(stun_config.stun_server_v6.is_ipv6()); assert!(autodiscovery_config.stun_server_v6.is_ipv6());
let autodiscovery_v4 = StunAutodiscovery { let autodiscovery_v4 = StunAutodiscovery {
consul_key: format!("diplonat/autodiscovery/ipv4/{}", node), consul_key: format!("diplonat/autodiscovery/ipv4/{}", node),
is_ipv4: true, is_ipv4: true,
stun_server: stun_config.stun_server_v4, stun_server: autodiscovery_config.stun_server_v4,
last_result: None, last_result: None,
}; };
let autodiscovery_v6 = StunAutodiscovery { let autodiscovery_v6 = Localv6AddressAutodiscovery {
consul_key: format!("diplonat/autodiscovery/ipv6/{}", node), consul_key: format!("diplonat/autodiscovery/ipv6/{}", node),
is_ipv4: false,
stun_server: Some(stun_config.stun_server_v6),
last_result: None, last_result: None,
}; };
@ -64,7 +67,7 @@ impl StunActor {
consul: consul::Consul::new(consul_config), consul: consul::Consul::new(consul_config),
autodiscovery_v4, autodiscovery_v4,
autodiscovery_v6, autodiscovery_v6,
refresh_time: stun_config.refresh_time, refresh_time: autodiscovery_config.refresh_time,
} }
} }
@ -168,6 +171,81 @@ async fn get_mapped_addr(
Ok(Some(xor_mapped_addr)) Ok(Some(xor_mapped_addr))
} }
impl Localv6AddressAutodiscovery {
async fn do_iteration(&mut self, consul: &consul::Consul) -> Result<()> {
let now = timestamp();
let discovered_addr = match get_kernel_src_ipv6_addr() {
Err(e) => {
if let Some(last_result) = &self.last_result {
if last_result.address.is_some()
&& now - last_result.timestamp <= PERSIST_SOME_RESULT_DURATION_SECS
{
// Keep non-None result that was obtained before by not
// writing/taking into account None result.
info!("Temporarily failed to determine IPv6 address (error: {}), keeping existing address in Consul ", e);
return Ok(());
}
}
error!(
"Failed to determine IPv6 address (error: {}), removing from Consul",
e
);
None
}
Ok(addr) => Some(addr),
};
let current_result = AutodiscoverResult {
timestamp: now,
address: discovered_addr,
};
let msg = format!(
"Local IPv6 address autodiscovery result: {} -> {:?}",
self.consul_key, discovered_addr
);
if self.last_result.as_ref().and_then(|x| x.address) != discovered_addr {
info!("{}", msg);
} else {
debug!("{}", msg);
}
consul
.kv_put(&self.consul_key, serde_json::to_vec(&current_result)?)
.await?;
self.last_result = Some(current_result);
Ok(())
}
}
fn get_kernel_src_ipv6_addr() -> Result<IpAddr> {
let binding_ip = IpAddr::V6(Ipv6Addr::UNSPECIFIED);
let binding_addr = SocketAddr::new(binding_ip, 0);
let socket = UdpSocket::bind(binding_addr)?;
// Old trick: connecting a UDP socket does not actually send any
// packet, but it forces the kernel to determine the correct
// source IP address.
socket.connect("[2001::1]:4242")?;
let discovered_addr = socket.local_addr().and_then(|x| Ok(x.ip()))?;
// Filter out unwanted addresses (localhost, unspecified, link-local)
// TODO: use is_global() in the future: https://doc.rust-lang.org/std/net/struct.Ipv6Addr.html#method.is_global
match discovered_addr {
V6(addr) if addr.is_loopback() => Err(anyhow!("ignoring loopback address {}", addr)),
V6(addr) if addr.is_unspecified() => Err(anyhow!("ignoring unspecified address {}", addr)),
// Reimplement is_unicast_link_local (only available with Rust 1.84)
V6(addr) if (addr.segments()[0] & 0xffc0) == 0xfe80 => {
Err(anyhow!("ignoring link-local address {}", addr))
}
addr => Ok(addr),
}
}
fn timestamp() -> u64 { fn timestamp() -> u64 {
SystemTime::now() SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH) .duration_since(SystemTime::UNIX_EPOCH)

View file

@ -5,7 +5,8 @@ mod runtime;
pub use options::{ConfigOpts, ConfigOptsBase, ConfigOptsConsul}; pub use options::{ConfigOpts, ConfigOptsBase, ConfigOptsConsul};
pub use runtime::{ pub use runtime::{
RuntimeConfig, RuntimeConfigConsul, RuntimeConfigFirewall, RuntimeConfigIgd, RuntimeConfigStun, RuntimeConfig, RuntimeConfigAutoDiscovery, RuntimeConfigConsul, RuntimeConfigFirewall,
RuntimeConfigIgd,
}; };
pub const EXPIRATION_TIME: u16 = 300; pub const EXPIRATION_TIME: u16 = 300;

View file

@ -33,7 +33,7 @@ pub struct RuntimeConfigIgd {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct RuntimeConfigStun { pub struct RuntimeConfigAutoDiscovery {
pub stun_server_v4: Option<SocketAddr>, pub stun_server_v4: Option<SocketAddr>,
pub stun_server_v6: SocketAddr, pub stun_server_v6: SocketAddr,
pub refresh_time: Duration, pub refresh_time: Duration,
@ -44,7 +44,7 @@ pub struct RuntimeConfig {
pub consul: RuntimeConfigConsul, pub consul: RuntimeConfigConsul,
pub firewall: RuntimeConfigFirewall, pub firewall: RuntimeConfigFirewall,
pub igd: Option<RuntimeConfigIgd>, pub igd: Option<RuntimeConfigIgd>,
pub stun: RuntimeConfigStun, pub autodiscovery: RuntimeConfigAutoDiscovery,
} }
impl RuntimeConfig { impl RuntimeConfig {
@ -55,13 +55,13 @@ impl RuntimeConfig {
false => Some(RuntimeConfigIgd::new(&opts.base)?), false => Some(RuntimeConfigIgd::new(&opts.base)?),
true => None, true => None,
}; };
let stun = RuntimeConfigStun::new(&opts.base)?; let autodiscovery = RuntimeConfigAutoDiscovery::new(&opts.base)?;
Ok(Self { Ok(Self {
consul, consul,
firewall, firewall,
igd, igd,
stun, autodiscovery,
}) })
} }
} }
@ -153,7 +153,7 @@ impl RuntimeConfigIgd {
} }
} }
impl RuntimeConfigStun { impl RuntimeConfigAutoDiscovery {
pub(super) fn new(opts: &ConfigOptsBase) -> Result<Self> { pub(super) fn new(opts: &ConfigOptsBase) -> Result<Self> {
let mut stun_server_v4 = None; let mut stun_server_v4 = None;
let mut stun_server_v6 = None; let mut stun_server_v6 = None;

View file

@ -3,15 +3,15 @@ use futures::future::FutureExt;
use tokio::try_join; use tokio::try_join;
use crate::{ use crate::{
config::ConfigOpts, consul_actor::ConsulActor, fw_actor::FirewallActor, igd_actor::IgdActor, autodiscovery_actor::AutoDiscoveryActor, config::ConfigOpts, consul_actor::ConsulActor,
stun_actor::StunActor, fw_actor::FirewallActor, igd_actor::IgdActor,
}; };
pub struct Diplonat { pub struct Diplonat {
consul: ConsulActor, consul: ConsulActor,
firewall: FirewallActor, firewall: FirewallActor,
igd: Option<IgdActor>, igd: Option<IgdActor>,
stun: StunActor, autodiscovery: AutoDiscoveryActor,
} }
impl Diplonat { impl Diplonat {
@ -43,13 +43,17 @@ impl Diplonat {
None => None, None => None,
}; };
let sa = StunActor::new(&rt_cfg.consul, &rt_cfg.stun, &rt_cfg.consul.node_name); let ad = AutoDiscoveryActor::new(
&rt_cfg.consul,
&rt_cfg.autodiscovery,
&rt_cfg.consul.node_name,
);
let ctx = Self { let ctx = Self {
consul: ca, consul: ca,
igd: ia, igd: ia,
firewall: fw, firewall: fw,
stun: sa, autodiscovery: ad,
}; };
Ok(ctx) Ok(ctx)
@ -70,7 +74,9 @@ impl Diplonat {
self.firewall self.firewall
.listen() .listen()
.map(|x| x.context("Run firewall actor")), .map(|x| x.context("Run firewall actor")),
self.stun.listen().map(|x| x.context("Run STUN actor")), self.autodiscovery
.listen()
.map(|x| x.context("Run autodiscovery actor")),
)?; )?;
Ok(()) Ok(())

View file

@ -1,3 +1,4 @@
mod autodiscovery_actor;
mod config; mod config;
mod consul; mod consul;
mod consul_actor; mod consul_actor;
@ -6,7 +7,6 @@ mod fw;
mod fw_actor; mod fw_actor;
mod igd_actor; mod igd_actor;
mod messages; mod messages;
mod stun_actor;
use diplonat::Diplonat; use diplonat::Diplonat;
use log::*; use log::*;