From c356c4d1c471acd9d2f7e1dcfd3a432442177b48 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 5 Apr 2023 09:47:58 +0200 Subject: [PATCH] IPv6-only mode --- src/config/options.rs | 3 ++ src/config/options_test.rs | 17 +++++------ src/config/runtime.rs | 25 +++++++++++---- src/diplonat.rs | 34 ++++++++++++++++----- src/fw_actor.rs | 62 +++++++++++++++++++++++--------------- src/stun_actor.rs | 44 ++++++++++++++++++++++----- 6 files changed, 129 insertions(+), 56 deletions(-) diff --git a/src/config/options.rs b/src/config/options.rs index 100c23c..fc9df16 100644 --- a/src/config/options.rs +++ b/src/config/options.rs @@ -19,6 +19,9 @@ pub struct ConfigOptsBase { pub refresh_time: Option, /// STUN server [default: stun.nextcloud.com:443] pub stun_server: Option, + /// IPv6-only mode (disables IGD, IPv4 firewall and IPv4 address autodiscovery) [default: false] + #[serde(default)] + pub ipv6_only: bool, } /// ACME configuration options diff --git a/src/config/options_test.rs b/src/config/options_test.rs index 8e05c90..073b9ac 100644 --- a/src/config/options_test.rs +++ b/src/config/options_test.rs @@ -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); } diff --git a/src/config/runtime.rs b/src/config/runtime.rs index 45a29c3..d1a3f89 100644 --- a/src/config/runtime.rs +++ b/src/config/runtime.rs @@ -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, pub stun_server_v6: SocketAddr, pub refresh_time: Duration, } @@ -48,7 +49,7 @@ pub struct RuntimeConfig { pub acme: Option, pub consul: RuntimeConfigConsul, pub firewall: RuntimeConfigFirewall, - pub igd: RuntimeConfigIgd, + pub igd: Option, 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, diff --git a/src/diplonat.rs b/src/diplonat.rs index a66836a..a94a6f8 100644 --- a/src/diplonat.rs +++ b/src/diplonat.rs @@ -9,7 +9,7 @@ use crate::{ pub struct Diplonat { consul: ConsulActor, firewall: FirewallActor, - igd: IgdActor, + igd: Option, 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(), )?; diff --git a/src/fw_actor.rs b/src/fw_actor.rs index fe68381..02d8bcb 100644 --- a/src/fw_actor.rs +++ b/src/fw_actor.rs @@ -12,7 +12,7 @@ use tokio::{ use crate::{fw, messages}; pub struct FirewallActor { - pub ipt_v4: iptables::IPTables, + pub ipt_v4: Option, pub ipt_v6: iptables::IPTables, rx_ports: watch::Receiver, 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, ) -> Result { 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::>(); - let diff_udp = self - .last_ports - .udp_ports - .difference(&curr_opened_ports.udp_ports) - .copied() - .collect::>(); - - 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::>(); + let diff_udp = self + .last_ports + .udp_ports + .difference(&curr_opened_ports.udp_ports) + .copied() + .collect::>(); + + let ports_to_open = messages::PublicExposedPorts { + tcp_ports: diff_tcp, + udp_ports: diff_udp, + }; + + fw::open_ports(ipt, ports_to_open)?; return Ok(()); } diff --git a/src/stun_actor.rs b/src/stun_actor.rs index 684d3d8..b112bb0 100644 --- a/src/stun_actor.rs +++ b/src/stun_actor.rs @@ -11,7 +11,7 @@ use crate::consul; pub struct StunActor { node: String, consul: consul::Consul, - stun_server_v4: SocketAddr, + stun_server_v4: Option, 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, } 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() +}