public IP address autodiscovery #20
6 changed files with 129 additions and 56 deletions
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
)?;
|
||||
|
|
|
@ -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,7 +68,14 @@ impl FirewallActor {
|
|||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
|
@ -84,7 +97,6 @@ impl FirewallActor {
|
|||
};
|
||||
|
||||
fw::open_ports(ipt, ports_to_open)?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue