public IP address autodiscovery #20

Merged
lx merged 10 commits from stun into main 2023-04-21 09:56:22 +00:00
6 changed files with 129 additions and 56 deletions
Showing only changes of commit c356c4d1c4 - Show all commits

View file

@ -19,6 +19,9 @@ pub struct ConfigOptsBase {
pub refresh_time: Option<u16>, pub refresh_time: Option<u16>,
/// STUN server [default: stun.nextcloud.com:443] /// STUN server [default: stun.nextcloud.com:443]
pub stun_server: Option<String>, pub stun_server: Option<String>,
/// IPv6-only mode (disables IGD, IPv4 firewall and IPv4 address autodiscovery) [default: false]
#[serde(default)]
pub ipv6_only: bool,
} }
/// ACME configuration options /// ACME configuration options

View file

@ -63,15 +63,13 @@ fn ok_from_iter_minimal_valid_options() {
rt_config.firewall.refresh_time, rt_config.firewall.refresh_time,
Duration::from_secs(REFRESH_TIME.into()) Duration::from_secs(REFRESH_TIME.into())
); );
assert!(rt_config.igd.private_ip.is_none()); let igd = rt_config.igd.unwrap();
assert!(igd.private_ip.is_none());
assert_eq!( assert_eq!(
rt_config.igd.expiration_time, igd.expiration_time,
Duration::from_secs(EXPIRATION_TIME.into()) Duration::from_secs(EXPIRATION_TIME.into())
); );
assert_eq!( assert_eq!(igd.refresh_time, Duration::from_secs(REFRESH_TIME.into()));
rt_config.igd.refresh_time,
Duration::from_secs(REFRESH_TIME.into())
);
} }
#[test] #[test]
@ -117,10 +115,11 @@ fn ok_from_iter_all_valid_options() {
opts.get(&"DIPLONAT_CONSUL_URL".to_string()).unwrap() opts.get(&"DIPLONAT_CONSUL_URL".to_string()).unwrap()
); );
assert_eq!(rt_config.firewall.refresh_time, refresh_time); assert_eq!(rt_config.firewall.refresh_time, refresh_time);
let igd = rt_config.igd.unwrap();
assert_eq!( assert_eq!(
&rt_config.igd.private_ip.unwrap().to_string(), &igd.private_ip.unwrap().to_string(),
opts.get(&"DIPLONAT_PRIVATE_IP".to_string()).unwrap() opts.get(&"DIPLONAT_PRIVATE_IP".to_string()).unwrap()
); );
assert_eq!(rt_config.igd.expiration_time, expiration_time); assert_eq!(igd.expiration_time, expiration_time);
assert_eq!(rt_config.igd.refresh_time, refresh_time); assert_eq!(igd.refresh_time, refresh_time);
} }

View file

@ -26,6 +26,7 @@ pub struct RuntimeConfigConsul {
#[derive(Debug)] #[derive(Debug)]
pub struct RuntimeConfigFirewall { pub struct RuntimeConfigFirewall {
pub ipv6_only: bool,
pub refresh_time: Duration, pub refresh_time: Duration,
} }
@ -38,7 +39,7 @@ pub struct RuntimeConfigIgd {
#[derive(Debug)] #[derive(Debug)]
pub struct RuntimeConfigStun { pub struct RuntimeConfigStun {
pub stun_server_v4: 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,
} }
@ -48,7 +49,7 @@ pub struct RuntimeConfig {
pub acme: Option<RuntimeConfigAcme>, pub acme: Option<RuntimeConfigAcme>,
pub consul: RuntimeConfigConsul, pub consul: RuntimeConfigConsul,
pub firewall: RuntimeConfigFirewall, pub firewall: RuntimeConfigFirewall,
pub igd: RuntimeConfigIgd, pub igd: Option<RuntimeConfigIgd>,
pub stun: RuntimeConfigStun, pub stun: RuntimeConfigStun,
} }
@ -57,7 +58,10 @@ impl RuntimeConfig {
let acme = RuntimeConfigAcme::new(opts.acme)?; let acme = RuntimeConfigAcme::new(opts.acme)?;
let consul = RuntimeConfigConsul::new(opts.consul)?; let consul = RuntimeConfigConsul::new(opts.consul)?;
let firewall = RuntimeConfigFirewall::new(&opts.base)?; let firewall = RuntimeConfigFirewall::new(&opts.base)?;
let igd = RuntimeConfigIgd::new(&opts.base)?; let igd = match opts.base.ipv6_only {
false => Some(RuntimeConfigIgd::new(&opts.base)?),
true => None,
};
let stun = RuntimeConfigStun::new(&opts.base)?; let stun = RuntimeConfigStun::new(&opts.base)?;
Ok(Self { Ok(Self {
@ -131,7 +135,10 @@ impl RuntimeConfigFirewall {
let refresh_time = let refresh_time =
Duration::from_secs(opts.refresh_time.unwrap_or(super::REFRESH_TIME).into()); Duration::from_secs(opts.refresh_time.unwrap_or(super::REFRESH_TIME).into());
Ok(Self { refresh_time }) Ok(Self {
refresh_time,
ipv6_only: opts.ipv6_only,
})
} }
} }
@ -189,9 +196,15 @@ impl RuntimeConfigStun {
let refresh_time = let refresh_time =
Duration::from_secs(opts.refresh_time.unwrap_or(super::REFRESH_TIME).into()); Duration::from_secs(opts.refresh_time.unwrap_or(super::REFRESH_TIME).into());
let stun_server_v4 = match opts.ipv6_only {
false => Some(
stun_server_v4.ok_or(anyhow!("Unable to resolve STUN server's IPv4 address"))?,
),
true => None,
};
Ok(Self { Ok(Self {
stun_server_v4: stun_server_v4 stun_server_v4,
.ok_or(anyhow!("Unable to resolve STUN server's IPv4 address"))?,
stun_server_v6: stun_server_v6 stun_server_v6: stun_server_v6
.ok_or(anyhow!("Unable to resolve STUN server's IPv6 address"))?, .ok_or(anyhow!("Unable to resolve STUN server's IPv6 address"))?,
refresh_time, refresh_time,

View file

@ -9,7 +9,7 @@ use crate::{
pub struct Diplonat { pub struct Diplonat {
consul: ConsulActor, consul: ConsulActor,
firewall: FirewallActor, firewall: FirewallActor,
igd: IgdActor, igd: Option<IgdActor>,
stun: StunActor, stun: StunActor,
} }
@ -20,16 +20,26 @@ impl Diplonat {
let ca = ConsulActor::new(&rt_cfg.consul, &rt_cfg.consul.node_name); let ca = ConsulActor::new(&rt_cfg.consul, &rt_cfg.consul.node_name);
let fw = FirewallActor::new(rt_cfg.firewall.refresh_time, &ca.rx_open_ports).await?; let fw = FirewallActor::new(
rt_cfg.firewall.ipv6_only,
let ia = IgdActor::new( rt_cfg.firewall.refresh_time,
rt_cfg.igd.private_ip,
rt_cfg.igd.refresh_time,
rt_cfg.igd.expiration_time,
&ca.rx_open_ports, &ca.rx_open_ports,
) )
.await?; .await?;
let ia = match rt_cfg.igd {
Some(igdc) => Some(
IgdActor::new(
igdc.private_ip,
igdc.refresh_time,
igdc.expiration_time,
&ca.rx_open_ports,
)
.await?,
),
None => None,
};
let sa = StunActor::new(&rt_cfg.consul, &rt_cfg.stun, &rt_cfg.consul.node_name); let sa = StunActor::new(&rt_cfg.consul, &rt_cfg.stun, &rt_cfg.consul.node_name);
let ctx = Self { let ctx = Self {
@ -43,9 +53,17 @@ impl Diplonat {
} }
pub async fn listen(&mut self) -> Result<()> { pub async fn listen(&mut self) -> Result<()> {
let igd_opt = &mut self.igd;
try_join!( try_join!(
self.consul.listen(), self.consul.listen(),
self.igd.listen(), async {
if let Some(igd) = igd_opt {
igd.listen().await
} else {
Ok(())
}
},
self.firewall.listen(), self.firewall.listen(),
self.stun.listen(), self.stun.listen(),
)?; )?;

View file

@ -12,7 +12,7 @@ use tokio::{
use crate::{fw, messages}; use crate::{fw, messages};
pub struct FirewallActor { pub struct FirewallActor {
pub ipt_v4: iptables::IPTables, pub ipt_v4: Option<iptables::IPTables>,
pub ipt_v6: iptables::IPTables, pub ipt_v6: iptables::IPTables,
rx_ports: watch::Receiver<messages::PublicExposedPorts>, rx_ports: watch::Receiver<messages::PublicExposedPorts>,
last_ports: messages::PublicExposedPorts, last_ports: messages::PublicExposedPorts,
@ -21,18 +21,24 @@ pub struct FirewallActor {
impl FirewallActor { impl FirewallActor {
pub async fn new( pub async fn new(
ipv6_only: bool,
refresh: Duration, refresh: Duration,
rxp: &watch::Receiver<messages::PublicExposedPorts>, rxp: &watch::Receiver<messages::PublicExposedPorts>,
) -> Result<Self> { ) -> Result<Self> {
let ctx = Self { let ctx = Self {
ipt_v4: iptables::new(false)?, ipt_v4: match ipv6_only {
false => Some(iptables::new(false)?),
true => None,
},
ipt_v6: iptables::new(true)?, ipt_v6: iptables::new(true)?,
rx_ports: rxp.clone(), rx_ports: rxp.clone(),
last_ports: messages::PublicExposedPorts::new(), last_ports: messages::PublicExposedPorts::new(),
refresh, refresh,
}; };
fw::setup(&ctx.ipt_v4)?; if let Some(ipt_v4) = &ctx.ipt_v4 {
fw::setup(ipt_v4)?;
}
fw::setup(&ctx.ipt_v6)?; fw::setup(&ctx.ipt_v6)?;
return Ok(ctx); return Ok(ctx);
@ -62,29 +68,35 @@ impl FirewallActor {
} }
pub async fn do_fw_update(&self) -> Result<()> { pub async fn do_fw_update(&self) -> Result<()> {
for ipt in [&self.ipt_v4, &self.ipt_v6] { if let Some(ipt_v4) = &self.ipt_v4 {
let curr_opened_ports = fw::get_opened_ports(ipt)?; self.do_fw_update_on(ipt_v4).await?;
let diff_tcp = self
.last_ports
.tcp_ports
.difference(&curr_opened_ports.tcp_ports)
.copied()
.collect::<HashSet<u16>>();
let diff_udp = self
.last_ports
.udp_ports
.difference(&curr_opened_ports.udp_ports)
.copied()
.collect::<HashSet<u16>>();
let ports_to_open = messages::PublicExposedPorts {
tcp_ports: diff_tcp,
udp_ports: diff_udp,
};
fw::open_ports(ipt, ports_to_open)?;
} }
self.do_fw_update_on(&self.ipt_v6).await?;
Ok(())
}
pub async fn do_fw_update_on(&self, ipt: &iptables::IPTables) -> Result<()> {
let curr_opened_ports = fw::get_opened_ports(ipt)?;
let diff_tcp = self
.last_ports
.tcp_ports
.difference(&curr_opened_ports.tcp_ports)
.copied()
.collect::<HashSet<u16>>();
let diff_udp = self
.last_ports
.udp_ports
.difference(&curr_opened_ports.udp_ports)
.copied()
.collect::<HashSet<u16>>();
let ports_to_open = messages::PublicExposedPorts {
tcp_ports: diff_tcp,
udp_ports: diff_udp,
};
fw::open_ports(ipt, ports_to_open)?;
return Ok(()); return Ok(());
} }

View file

@ -11,7 +11,7 @@ use crate::consul;
pub struct StunActor { pub struct StunActor {
node: String, node: String,
consul: consul::Consul, consul: consul::Consul,
stun_server_v4: SocketAddr, stun_server_v4: Option<SocketAddr>,
stun_server_v6: SocketAddr, stun_server_v6: SocketAddr,
refresh_time: Duration, refresh_time: Duration,
} }
@ -19,7 +19,7 @@ pub struct StunActor {
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct AutodiscoverResult { pub struct AutodiscoverResult {
pub timestamp: u64, pub timestamp: u64,
pub address: IpAddr, pub address: Option<IpAddr>,
} }
impl StunActor { impl StunActor {
@ -28,7 +28,10 @@ impl StunActor {
stun_config: &RuntimeConfigStun, stun_config: &RuntimeConfigStun,
node: &str, node: &str,
) -> Self { ) -> Self {
assert!(stun_config.stun_server_v4.is_ipv4()); assert!(stun_config
.stun_server_v4
.map(|x| x.is_ipv4())
.unwrap_or(true));
assert!(stun_config.stun_server_v6.is_ipv6()); assert!(stun_config.stun_server_v6.is_ipv6());
Self { Self {
@ -42,7 +45,11 @@ impl StunActor {
pub async fn listen(&mut self) -> Result<()> { pub async fn listen(&mut self) -> Result<()> {
loop { loop {
if let Err(e) = self.autodiscover_ip(self.stun_server_v4).await { let ipv4_result = match self.stun_server_v4 {
Some(stun_server_v4) => self.autodiscover_ip(stun_server_v4).await,
None => self.autodiscover_none_ipv4().await,
};
if let Err(e) = ipv4_result {
error!("Unable to autodiscover IPv4 address: {}", e); error!("Unable to autodiscover IPv4 address: {}", e);
} }
if let Err(e) = self.autodiscover_ip(self.stun_server_v6).await { if let Err(e) = self.autodiscover_ip(self.stun_server_v6).await {
@ -75,10 +82,24 @@ impl StunActor {
.kv_put( .kv_put(
&consul_key, &consul_key,
serde_json::to_vec(&AutodiscoverResult { serde_json::to_vec(&AutodiscoverResult {
timestamp: SystemTime::now() timestamp: timestamp(),
.duration_since(SystemTime::UNIX_EPOCH)? address: Some(discovered_addr),
.as_secs(), })?,
address: discovered_addr, )
.await?;
Ok(())
}
async fn autodiscover_none_ipv4(&self) -> Result<()> {
let consul_key = format!("diplonat/autodiscovery/ipv4/{}", self.node);
self.consul
.kv_put(
&consul_key,
serde_json::to_vec(&AutodiscoverResult {
timestamp: timestamp(),
address: None,
})?, })?,
) )
.await?; .await?;
@ -101,3 +122,10 @@ async fn get_mapped_addr(stun_server: SocketAddr, binding_addr: SocketAddr) -> R
.ok_or(anyhow!("no XorMappedAddress found in STUN response"))?; .ok_or(anyhow!("no XorMappedAddress found in STUN response"))?;
Ok(xor_mapped_addr) Ok(xor_mapped_addr)
} }
fn timestamp() -> u64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("clock error")
.as_secs()
}