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>,
/// STUN server [default: stun.nextcloud.com:443]
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

View file

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

View file

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

View file

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

View file

@ -12,7 +12,7 @@ use tokio::{
use crate::{fw, messages};
pub struct FirewallActor {
pub ipt_v4: iptables::IPTables,
pub ipt_v4: Option<iptables::IPTables>,
pub ipt_v6: iptables::IPTables,
rx_ports: watch::Receiver<messages::PublicExposedPorts>,
last_ports: messages::PublicExposedPorts,
@ -21,18 +21,24 @@ pub struct FirewallActor {
impl FirewallActor {
pub async fn new(
ipv6_only: bool,
refresh: Duration,
rxp: &watch::Receiver<messages::PublicExposedPorts>,
) -> Result<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)?,
rx_ports: rxp.clone(),
last_ports: messages::PublicExposedPorts::new(),
refresh,
};
fw::setup(&ctx.ipt_v4)?;
if let Some(ipt_v4) = &ctx.ipt_v4 {
fw::setup(ipt_v4)?;
}
fw::setup(&ctx.ipt_v6)?;
return Ok(ctx);
@ -62,29 +68,35 @@ impl FirewallActor {
}
pub async fn do_fw_update(&self) -> Result<()> {
for ipt in [&self.ipt_v4, &self.ipt_v6] {
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)?;
if let Some(ipt_v4) = &self.ipt_v4 {
self.do_fw_update_on(ipt_v4).await?;
}
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(());
}

View file

@ -11,7 +11,7 @@ use crate::consul;
pub struct StunActor {
node: String,
consul: consul::Consul,
stun_server_v4: SocketAddr,
stun_server_v4: Option<SocketAddr>,
stun_server_v6: SocketAddr,
refresh_time: Duration,
}
@ -19,7 +19,7 @@ pub struct StunActor {
#[derive(Serialize, Deserialize, Debug)]
pub struct AutodiscoverResult {
pub timestamp: u64,
pub address: IpAddr,
pub address: Option<IpAddr>,
}
impl StunActor {
@ -28,7 +28,10 @@ impl StunActor {
stun_config: &RuntimeConfigStun,
node: &str,
) -> 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());
Self {
@ -42,7 +45,11 @@ impl StunActor {
pub async fn listen(&mut self) -> Result<()> {
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);
}
if let Err(e) = self.autodiscover_ip(self.stun_server_v6).await {
@ -75,10 +82,24 @@ impl StunActor {
.kv_put(
&consul_key,
serde_json::to_vec(&AutodiscoverResult {
timestamp: SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)?
.as_secs(),
address: discovered_addr,
timestamp: timestamp(),
address: Some(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?;
@ -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(xor_mapped_addr)
}
fn timestamp() -> u64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("clock error")
.as_secs()
}