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>,
|
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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(),
|
||||||
)?;
|
)?;
|
||||||
|
|
|
@ -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,7 +68,14 @@ 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 {
|
||||||
|
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 curr_opened_ports = fw::get_opened_ports(ipt)?;
|
||||||
|
|
||||||
let diff_tcp = self
|
let diff_tcp = self
|
||||||
|
@ -84,7 +97,6 @@ impl FirewallActor {
|
||||||
};
|
};
|
||||||
|
|
||||||
fw::open_ports(ipt, ports_to_open)?;
|
fw::open_ports(ipt, ports_to_open)?;
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue