summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2019-12-17 12:31:11 +0100
committerDavid Lönnhager <david.l@mullvad.net>2019-12-17 12:31:11 +0100
commita85e9db8d1689ac292daa60f674067f21cec1f33 (patch)
tree70f42303da155de47edbf1689cf2b8958eeea671
parenta5f588de6cc711ea3b5a790056f1a50242e98dcc (diff)
parent964d5211746a3e2841e6a86c22d1c5bd757707bb (diff)
downloadmullvadvpn-a85e9db8d1689ac292daa60f674067f21cec1f33.tar.xz
mullvadvpn-a85e9db8d1689ac292daa60f674067f21cec1f33.zip
Merge branch 'rotate-wireguard-pubkey'
-rw-r--r--CHANGELOG.md4
-rw-r--r--mullvad-cli/src/cmds/tunnel.rs50
-rw-r--r--mullvad-daemon/src/lib.rs64
-rw-r--r--mullvad-daemon/src/management_interface.rs22
-rw-r--r--mullvad-daemon/src/wireguard.rs245
-rw-r--r--mullvad-ipc-client/src/lib.rs4
-rw-r--r--mullvad-types/src/settings/mod.rs17
-rw-r--r--talpid-types/src/net/wireguard.rs2
8 files changed, 398 insertions, 10 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2c70a86bac..4aad5e6eaa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -49,6 +49,10 @@ Line wrap the file at 100 chars. Th
improve battery life in some cases.
### Security
+- Add automatic key rotation for WireGuard (every 7 days by default). This limits the potential
+ for an attacker to correlate traffic with a public key and identity, and reduces the harm of
+ software that might leak the private tunnel IP (since it is no longer fixed).
+
#### Linux
- Stop [CVE-2019-14899](https://seclists.org/oss-sec/2019/q4/122) by dropping all packets destined
for the tunnel IP coming in on some other interface than the tunnel.
diff --git a/mullvad-cli/src/cmds/tunnel.rs b/mullvad-cli/src/cmds/tunnel.rs
index 75119ac87d..2d0da4bf32 100644
--- a/mullvad-cli/src/cmds/tunnel.rs
+++ b/mullvad-cli/src/cmds/tunnel.rs
@@ -56,6 +56,18 @@ fn create_wireguard_keys_subcommand() -> clap::App<'static, 'static> {
.setting(clap::AppSettings::SubcommandRequiredElseHelp)
.subcommand(clap::SubCommand::with_name("check"))
.subcommand(clap::SubCommand::with_name("generate"))
+ .subcommand(create_wireguard_keys_rotation_interval_subcommand())
+}
+
+fn create_wireguard_keys_rotation_interval_subcommand() -> clap::App<'static, 'static> {
+ clap::SubCommand::with_name("rotation-interval")
+ .about("Manage automatic key rotation (specified in hours; 0 = disabled)")
+ .setting(clap::AppSettings::SubcommandRequiredElseHelp)
+ .subcommand(clap::SubCommand::with_name("get"))
+ .subcommand(clap::SubCommand::with_name("reset").about("Use the default rotation interval"))
+ .subcommand(
+ clap::SubCommand::with_name("set").arg(clap::Arg::with_name("interval").required(true)),
+ )
}
@@ -120,8 +132,17 @@ impl Tunnel {
("key", Some(matches)) => match matches.subcommand() {
("check", _) => Self::process_wireguard_key_check(),
("generate", _) => Self::process_wireguard_key_generate(),
+ ("rotation-interval", Some(matches)) => match matches.subcommand() {
+ ("get", _) => Self::process_wireguard_rotation_interval_get(),
+ ("set", Some(matches)) => {
+ Self::process_wireguard_rotation_interval_set(matches)
+ }
+ ("reset", _) => Self::process_wireguard_rotation_interval_reset(),
+ _ => unreachable!("unhandled command"),
+ },
_ => unreachable!("unhandled command"),
},
+
_ => unreachable!("unhandled command"),
}
}
@@ -184,6 +205,35 @@ impl Tunnel {
Ok(())
}
+ fn process_wireguard_rotation_interval_get() -> Result<()> {
+ let tunnel_options = Self::get_tunnel_options()?;
+ println!(
+ "Rotation interval: {} hour(s)",
+ tunnel_options
+ .wireguard
+ .automatic_rotation
+ .map(|interval| interval.to_string())
+ .unwrap_or_else(|| "default".to_owned())
+ );
+ Ok(())
+ }
+
+ fn process_wireguard_rotation_interval_set(matches: &clap::ArgMatches<'_>) -> Result<()> {
+ let rotate_interval =
+ value_t!(matches.value_of("interval"), u32).unwrap_or_else(|e| e.exit());
+ let mut rpc = new_rpc_client()?;
+ rpc.set_wireguard_rotation_interval(Some(rotate_interval))?;
+ println!("Set key rotation interval: {} hour(s)", rotate_interval);
+ Ok(())
+ }
+
+ fn process_wireguard_rotation_interval_reset() -> Result<()> {
+ let mut rpc = new_rpc_client()?;
+ rpc.set_wireguard_rotation_interval(None)?;
+ println!("Set key rotation interval: default");
+ Ok(())
+ }
+
fn handle_ipv6_cmd(matches: &clap::ArgMatches<'_>) -> Result<()> {
if matches.subcommand_matches("get").is_some() {
Self::process_ipv6_get()
diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs
index aabcbb6952..4d01e802f5 100644
--- a/mullvad-daemon/src/lib.rs
+++ b/mullvad-daemon/src/lib.rs
@@ -448,7 +448,6 @@ where
)
.map_err(Error::TunnelError)?;
-
let wireguard_key_manager = wireguard::KeyManager::new(
internal_event_tx.clone(),
rpc_handle.clone(),
@@ -483,6 +482,19 @@ where
daemon.ensure_wireguard_keys_for_current_account();
+ if let Some(token) = daemon.settings.get_account_token() {
+ daemon.wireguard_key_manager.set_rotation_interval(
+ &mut daemon.account_history,
+ token,
+ daemon
+ .settings
+ .get_tunnel_options()
+ .wireguard
+ .automatic_rotation
+ .map(|hours| 60 * hours),
+ );
+ }
+
Ok(daemon)
}
@@ -824,6 +836,9 @@ where
SetBridgeState(tx, bridge_state) => self.on_set_bridge_state(tx, bridge_state),
SetEnableIpv6(tx, enable_ipv6) => self.on_set_enable_ipv6(tx, enable_ipv6),
SetWireguardMtu(tx, mtu) => self.on_set_wireguard_mtu(tx, mtu),
+ SetWireguardRotationInterval(tx, interval) => {
+ self.on_set_wireguard_rotation_interval(tx, interval)
+ }
GetSettings(tx) => self.on_get_settings(tx),
GenerateWireguardKey(tx) => self.on_generate_wireguard_key(tx),
GetWireguardKey(tx) => self.on_get_wireguard_key(tx),
@@ -1104,13 +1119,19 @@ where
self.event_listener.notify_settings(self.settings.clone());
// Bump account history if a token was set
- if let Some(token) = account_token {
+ if let Some(token) = account_token.clone() {
if let Err(e) = self.account_history.bump_history(&token) {
log::error!("Failed to bump account history: {}", e);
}
}
self.ensure_wireguard_keys_for_current_account();
+
+ if let Some(token) = account_token {
+ // update automatic rotation
+ self.wireguard_key_manager
+ .reset_rotation(&mut self.account_history, token);
+ }
}
Ok(account_changed)
}
@@ -1348,6 +1369,33 @@ where
}
}
+ fn on_set_wireguard_rotation_interval(
+ &mut self,
+ tx: oneshot::Sender<()>,
+ interval: Option<u32>,
+ ) {
+ let save_result = self.settings.set_wireguard_rotation_interval(interval);
+ match save_result {
+ Ok(settings_changed) => {
+ Self::oneshot_send(tx, (), "set_wireguard_rotation_interval response");
+ if settings_changed {
+ let account_token = self.settings.get_account_token();
+
+ if let Some(token) = account_token {
+ self.wireguard_key_manager.set_rotation_interval(
+ &mut self.account_history,
+ token,
+ interval.map(|hours| 60 * hours),
+ );
+ }
+
+ self.event_listener.notify_settings(self.settings.clone());
+ }
+ }
+ Err(e) => error!("{}", e.display_chain_with_msg("Unable to save settings")),
+ }
+ }
+
fn ensure_wireguard_keys_for_current_account(&mut self) {
if let Some(account) = self.settings.get_account_token() {
if self
@@ -1411,6 +1459,18 @@ where
})?;
let keygen_event = KeygenEvent::NewKey(public_key);
self.event_listener.notify_key_event(keygen_event.clone());
+
+ // update automatic rotation
+ self.wireguard_key_manager.set_rotation_interval(
+ &mut self.account_history,
+ account_token.clone(),
+ self.settings
+ .get_tunnel_options()
+ .wireguard
+ .automatic_rotation
+ .map(|hours| 60 * hours),
+ );
+
Ok(keygen_event)
}
Err(wireguard::Error::TooManyKeys) => Ok(KeygenEvent::TooManyKeys),
diff --git a/mullvad-daemon/src/management_interface.rs b/mullvad-daemon/src/management_interface.rs
index 4ddeda473c..0e4547b42d 100644
--- a/mullvad-daemon/src/management_interface.rs
+++ b/mullvad-daemon/src/management_interface.rs
@@ -142,6 +142,10 @@ build_rpc_trait! {
#[rpc(meta, name = "set_wireguard_mtu")]
fn set_wireguard_mtu(&self, Self::Metadata, Option<u16>) -> BoxFuture<(), Error>;
+ /// Set automatic key rotation interval for wireguard tunnels
+ #[rpc(meta, name = "set_wireguard_rotation_interval")]
+ fn set_wireguard_rotation_interval(&self, Self::Metadata, Option<u32>) -> BoxFuture<(), Error>;
+
/// Returns the current daemon settings
#[rpc(meta, name = "get_settings")]
fn get_settings(&self, Self::Metadata) -> BoxFuture<Settings, Error>;
@@ -239,6 +243,8 @@ pub enum ManagementCommand {
SetEnableIpv6(OneshotSender<()>, bool),
/// Set MTU for wireguard tunnels
SetWireguardMtu(OneshotSender<()>, Option<u16>),
+ /// Set automatic key rotation interval for wireguard tunnels
+ SetWireguardRotationInterval(OneshotSender<()>, Option<u32>),
/// Get the daemon settings
GetSettings(OneshotSender<Settings>),
/// Generate new wireguard key
@@ -695,6 +701,22 @@ impl<T: From<ManagementCommand> + 'static + Send> ManagementInterfaceApi
Box::new(future)
}
+ /// Set automatic key rotation interval for wireguard tunnels
+ fn set_wireguard_rotation_interval(
+ &self,
+ _: Self::Metadata,
+ interval: Option<u32>,
+ ) -> BoxFuture<(), Error> {
+ log::debug!("set_wireguard_rotation_interval({:?})", interval);
+ let (tx, rx) = sync::oneshot::channel();
+ let future = self
+ .send_command_to_daemon(ManagementCommand::SetWireguardRotationInterval(
+ tx, interval,
+ ))
+ .and_then(|_| rx.map_err(|_| Error::internal_error()));
+ Box::new(future)
+ }
+
fn get_settings(&self, _: Self::Metadata) -> BoxFuture<Settings, Error> {
log::debug!("get_settings");
let (tx, rx) = sync::oneshot::channel();
diff --git a/mullvad-daemon/src/wireguard.rs b/mullvad-daemon/src/wireguard.rs
index 0290d7a750..1e1b8f72a5 100644
--- a/mullvad-daemon/src/wireguard.rs
+++ b/mullvad-daemon/src/wireguard.rs
@@ -1,10 +1,14 @@
-use crate::InternalDaemonEvent;
+use crate::{account_history::AccountHistory, InternalDaemonEvent};
use chrono::offset::Utc;
-use futures::{future::Executor, sync::oneshot, Async, Future, Poll};
+use futures::{
+ future::{Executor, IntoFuture},
+ sync::oneshot,
+ Async, Future, Poll,
+};
use jsonrpc_client_core::Error as JsonRpcError;
use mullvad_types::account::AccountToken;
pub use mullvad_types::wireguard::*;
-use std::{sync::mpsc, time::Duration};
+use std::{cmp, sync::mpsc, time::Duration};
pub use talpid_types::net::wireguard::{
ConnectionConfig, PrivateKey, TunnelConfig, TunnelParameters,
};
@@ -14,9 +18,17 @@ use tokio_retry::{
strategy::{jitter, ExponentialBackoff},
RetryIf,
};
+use tokio_timer;
const TOO_MANY_KEYS_ERROR_CODE: i64 = -703;
+/// Default automatic key rotation (in minutes)
+const DEFAULT_AUTOMATIC_KEY_ROTATION: u32 = 7 * 24 * 60;
+/// How long to wait before reattempting to rotate keys on failure (secs)
+const AUTOMATIC_ROTATION_RETRY_DELAY: u64 = 5;
+/// Minimum interval used by automatic rotation (secs)
+const MINIMUM_ROTATION_INTERVAL: u64 = 5;
+
#[derive(err_derive::Error, Debug)]
pub enum Error {
@@ -28,6 +40,8 @@ pub enum Error {
RpcError(#[error(source)] jsonrpc_client_core::Error),
#[error(display = "Account already has maximum number of keys")]
TooManyKeys,
+ #[error(display = "Failed to create rotation timer")]
+ RotationScheduleError(#[error(source)] tokio_timer::TimerError),
}
pub type Result<T> = std::result::Result<T, Error>;
@@ -37,6 +51,10 @@ pub struct KeyManager {
http_handle: mullvad_rpc::HttpHandle,
tokio_remote: Remote,
current_job: Option<CancelHandle>,
+
+ abort_scheduler_tx: Option<CancelHandle>,
+ // unit: minutes
+ auto_rotation_interval: u32,
}
impl KeyManager {
@@ -50,9 +68,51 @@ impl KeyManager {
http_handle,
tokio_remote,
current_job: None,
+ abort_scheduler_tx: None,
+ auto_rotation_interval: 0,
}
}
+ /// Update automatic key rotation interval (given in minutes)
+ /// Passing `None` for the interval will use the default value.
+ /// A value of `0` disables automatic key rotation.
+ pub fn reset_rotation(
+ &mut self,
+ account_history: &mut AccountHistory,
+ account_token: AccountToken,
+ ) {
+ log::debug!("reset_rotation");
+
+ match account_history
+ .get(&account_token)
+ .map(|entry| entry.map(|entry| entry.wireguard.map(|wg| wg.get_public_key())))
+ {
+ Ok(Some(Some(public_key))) => self.run_automatic_rotation(account_token, public_key),
+ Ok(Some(None)) => {
+ log::error!("reset_rotation: failed to obtain public key for account entry.")
+ }
+ Ok(None) => log::error!("reset_rotation: account entry not found."),
+ Err(e) => log::error!("reset_rotation: failed to obtain account entry. {}", e),
+ };
+ }
+
+ /// Update automatic key rotation interval (given in minutes)
+ /// Passing `None` for the interval will use the default value.
+ /// A value of `0` disables automatic key rotation.
+ pub fn set_rotation_interval(
+ &mut self,
+ account_history: &mut AccountHistory,
+ account_token: AccountToken,
+ auto_rotation_interval_mins: Option<u32>,
+ ) {
+ log::debug!("set_rotation_interval");
+
+ self.auto_rotation_interval =
+ auto_rotation_interval_mins.unwrap_or(DEFAULT_AUTOMATIC_KEY_ROTATION);
+
+ self.reset_rotation(account_history, account_token);
+ }
+
/// Stop current key generation
pub fn reset(&mut self) {
if let Some(job) = self.current_job.take() {
@@ -90,8 +150,13 @@ impl KeyManager {
) -> Result<WireguardData> {
self.reset();
let new_key = PrivateKey::new_from_random().map_err(Error::GenerationError)?;
- self.run_future_sync(self.replace_key_rpc(account, old_key, new_key))
- .map_err(Self::map_rpc_error)
+ self.run_future_sync(Self::replace_key_rpc(
+ self.http_handle.clone(),
+ account,
+ old_key,
+ new_key,
+ ))
+ .map_err(Self::map_rpc_error)
}
@@ -193,12 +258,12 @@ impl KeyManager {
}
fn replace_key_rpc(
- &self,
+ http_handle: mullvad_rpc::HttpHandle,
account: AccountToken,
old_key: PublicKey,
new_key: PrivateKey,
) -> impl Future<Item = WireguardData, Error = JsonRpcError> + Send {
- let mut rpc = mullvad_rpc::WireguardKeyProxy::new(self.http_handle.clone());
+ let mut rpc = mullvad_rpc::WireguardKeyProxy::new(http_handle.clone());
let new_public_key = new_key.public_key();
rpc.replace_wg_key(account.clone(), old_key.key, new_public_key)
.map(move |addresses| WireguardData {
@@ -217,6 +282,172 @@ impl KeyManager {
_ => Error::RpcError(err),
}
}
+ fn create_key_expiration_timer(
+ public_key: PublicKey,
+ rotation_interval_secs: u64,
+ ) -> impl Future<Item = (), Error = Error> + Send {
+ let last_update = public_key.created.clone();
+ let key_age = Duration::from_secs(
+ (Utc::now().signed_duration_since(last_update)).num_seconds() as u64,
+ );
+
+ let interval_duration = Duration::from_secs(rotation_interval_secs);
+ let remaining_time = interval_duration
+ .checked_sub(key_age)
+ .unwrap_or(Duration::from_secs(0));
+ let key_expiry = cmp::max(
+ Duration::from_secs(MINIMUM_ROTATION_INTERVAL),
+ remaining_time,
+ );
+
+ log::info!("Next key rotation (time left): {:?}", key_expiry);
+
+ tokio_timer::wheel()
+ .max_timeout(Duration::new(std::u64::MAX, 0))
+ .build()
+ .sleep(key_expiry)
+ .map_err(|e| Error::RotationScheduleError(e))
+ }
+
+ fn next_automatic_rotation(
+ daemon_tx: mpsc::Sender<InternalDaemonEvent>,
+ http_handle: mullvad_rpc::HttpHandle,
+ public_key: PublicKey,
+ rotation_interval_secs: u64,
+ account_token: AccountToken,
+ ) -> impl Future<Item = WireguardData, Error = Error> + Send {
+ let expiration_timer =
+ Self::create_key_expiration_timer(public_key.clone(), rotation_interval_secs);
+
+ let account_token_copy = account_token.clone();
+
+ expiration_timer
+ .and_then(move |_| {
+ log::info!("Replacing WireGuard key");
+
+ let private_key = PrivateKey::new_from_random()
+ .map_err(Error::GenerationError)
+ .into_future();
+ private_key.and_then(move |private_key| {
+ Self::replace_key_rpc(
+ http_handle.clone(),
+ account_token.clone(),
+ public_key.clone(),
+ private_key,
+ )
+ .map_err(|err| Error::RpcError(err))
+ })
+ })
+ .map(move |wireguard_data| {
+ // Update account data
+ let _ = daemon_tx.send(InternalDaemonEvent::WgKeyEvent((
+ account_token_copy,
+ Ok(wireguard_data.clone()),
+ )));
+
+ wireguard_data
+ })
+ }
+
+ fn create_automatic_rotation(
+ daemon_tx: mpsc::Sender<InternalDaemonEvent>,
+ http_handle: mullvad_rpc::HttpHandle,
+ public_key: PublicKey,
+ rotation_interval_secs: u64,
+ account_token: AccountToken,
+ ) -> Box<dyn Future<Item = (), Error = Error> + Send> {
+ log::debug!("create_automatic_rotation");
+
+ let fut = Self::next_automatic_rotation(
+ daemon_tx.clone(),
+ http_handle.clone(),
+ public_key.clone(),
+ rotation_interval_secs,
+ account_token.clone(),
+ );
+
+ let create_repeat_future = move |result: Result<WireguardData>| {
+ let next_public_key;
+ let next_interval: u64;
+
+ match result {
+ Ok(wg_data) => {
+ next_interval = rotation_interval_secs;
+ next_public_key = wg_data.get_public_key();
+
+ Self::create_automatic_rotation(
+ daemon_tx.clone(),
+ http_handle.clone(),
+ next_public_key,
+ next_interval,
+ account_token.clone(),
+ )
+ }
+ Err(e) => {
+ log::error!(
+ "Key rotation failed: {}. Retrying in {} seconds",
+ e,
+ AUTOMATIC_ROTATION_RETRY_DELAY,
+ );
+
+ next_interval = rotation_interval_secs;
+ next_public_key = public_key.clone();
+
+ let daemon_tx = daemon_tx.clone();
+ let http_handle = http_handle.clone();
+ let account_token = account_token.clone();
+
+ Box::new(
+ tokio_timer::wheel()
+ .build()
+ .sleep(Duration::from_secs(AUTOMATIC_ROTATION_RETRY_DELAY))
+ .then(move |_| {
+ Self::create_automatic_rotation(
+ daemon_tx,
+ http_handle,
+ next_public_key,
+ next_interval,
+ account_token,
+ )
+ }),
+ )
+ }
+ }
+ };
+
+ Box::new(fut.then(create_repeat_future).map(|_| ()))
+ }
+
+ fn run_automatic_rotation(&mut self, account_token: AccountToken, public_key: PublicKey) {
+ self.stop_automatic_rotation();
+
+ if let 0 = self.auto_rotation_interval {
+ // disabled
+ return;
+ }
+
+ // Schedule cancellable series of repeating rotation tasks
+ let fut = Self::create_automatic_rotation(
+ self.daemon_tx.clone(),
+ self.http_handle.clone(),
+ public_key,
+ 60u64 * (self.auto_rotation_interval as u64),
+ account_token,
+ );
+ let (fut, cancel_handle) = Cancellable::new(fut);
+
+ if let Err(e) = self.tokio_remote.execute(fut.map_err(|_| ())) {
+ log::error!("Failed to execute auto key rotation: {:?}", e.kind());
+ }
+ self.abort_scheduler_tx = Some(cancel_handle);
+ }
+
+ fn stop_automatic_rotation(&mut self) {
+ if let Some(cancel_handle) = self.abort_scheduler_tx.take() {
+ log::info!("Stopping automatic key rotation");
+ cancel_handle.cancel();
+ }
+ }
}
pub enum CancelErr<E> {
diff --git a/mullvad-ipc-client/src/lib.rs b/mullvad-ipc-client/src/lib.rs
index dad3be309d..ed55bc15d7 100644
--- a/mullvad-ipc-client/src/lib.rs
+++ b/mullvad-ipc-client/src/lib.rs
@@ -199,6 +199,10 @@ impl DaemonRpcClient {
self.call("set_wireguard_mtu", &[mtu])
}
+ pub fn set_wireguard_rotation_interval(&mut self, interval: Option<u32>) -> Result<()> {
+ self.call("set_wireguard_rotation_interval", &[interval])
+ }
+
pub fn set_openvpn_mssfix(&mut self, mssfix: Option<u16>) -> Result<()> {
self.call("set_openvpn_mssfix", &[mssfix])
}
diff --git a/mullvad-types/src/settings/mod.rs b/mullvad-types/src/settings/mod.rs
index 0c3aa2f6fe..89b0fa2ad7 100644
--- a/mullvad-types/src/settings/mod.rs
+++ b/mullvad-types/src/settings/mod.rs
@@ -284,6 +284,18 @@ impl Settings {
}
}
+ pub fn set_wireguard_rotation_interval(
+ &mut self,
+ automatic_rotation: Option<u32>,
+ ) -> Result<bool> {
+ if self.tunnel_options.wireguard.automatic_rotation != automatic_rotation {
+ self.tunnel_options.wireguard.automatic_rotation = automatic_rotation;
+ self.save().map(|_| true)
+ } else {
+ Ok(false)
+ }
+ }
+
pub fn get_tunnel_options(&self) -> &TunnelOptions {
&self.tunnel_options
}
@@ -334,7 +346,10 @@ impl Default for TunnelOptions {
fn default() -> Self {
TunnelOptions {
openvpn: openvpn::TunnelOptions::default(),
- wireguard: wireguard::TunnelOptions { mtu: None },
+ wireguard: wireguard::TunnelOptions {
+ mtu: None,
+ automatic_rotation: None,
+ },
generic: GenericTunnelOptions { enable_ipv6: false },
}
}
diff --git a/talpid-types/src/net/wireguard.rs b/talpid-types/src/net/wireguard.rs
index 68beef9aa0..805d1884ba 100644
--- a/talpid-types/src/net/wireguard.rs
+++ b/talpid-types/src/net/wireguard.rs
@@ -51,6 +51,8 @@ pub struct TunnelConfig {
pub struct TunnelOptions {
/// MTU for the wireguard tunnel
pub mtu: Option<u16>,
+ /// Interval used for automatic key rotation, in hours
+ pub automatic_rotation: Option<u32>,
}
/// Wireguard x25519 private key