diff options
| -rw-r--r-- | gui/src/main/daemon-rpc.ts | 23 | ||||
| -rw-r--r-- | gui/src/main/index.ts | 13 | ||||
| -rw-r--r-- | gui/src/renderer/app.tsx | 44 | ||||
| -rw-r--r-- | gui/src/renderer/lib/relay-settings-builder.ts | 22 | ||||
| -rw-r--r-- | gui/src/renderer/redux/settings/reducers.ts | 3 | ||||
| -rw-r--r-- | gui/src/shared/daemon-rpc-types.ts | 23 | ||||
| -rw-r--r-- | gui/test/relay-settings-builder.spec.ts | 20 | ||||
| -rw-r--r-- | mullvad-cli/src/cmds/relay.rs | 16 | ||||
| -rw-r--r-- | mullvad-daemon/src/lib.rs | 17 | ||||
| -rw-r--r-- | mullvad-daemon/src/relays.rs | 132 | ||||
| -rw-r--r-- | mullvad-jni/src/from_java.rs | 4 | ||||
| -rw-r--r-- | mullvad-types/src/relay_constraints.rs | 62 | ||||
| -rw-r--r-- | mullvad-types/src/settings/migrations/mod.rs | 145 | ||||
| -rw-r--r-- | mullvad-types/src/settings/migrations/v1.rs | 221 | ||||
| -rw-r--r-- | mullvad-types/src/settings/mod.rs (renamed from mullvad-types/src/settings.rs) | 33 |
15 files changed, 612 insertions, 166 deletions
diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index e317ffb794..24671fac23 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -101,21 +101,14 @@ const relaySettingsSchema = oneOf( object({ normal: partialObject({ location: locationConstraintSchema, - tunnel: constraint( - oneOf( - object({ - openvpn: partialObject({ - port: constraint(number), - protocol: constraint(enumeration('udp', 'tcp')), - }), - }), - object({ - wireguard: partialObject({ - port: constraint(number), - }), - }), - ), - ), + tunnel_protocol: constraint(enumeration('wireguard', 'openvpn')), + wireguard_constraints: partialObject({ + port: constraint(number), + }), + openvpn_constraints: partialObject({ + port: constraint(number), + protocol: constraint(enumeration('udp', 'tcp')), + }), }), }), object({ diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index a0dcbeb50b..1d766629cf 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -83,7 +83,14 @@ class ApplicationMain { relaySettings: { normal: { location: 'any', - tunnel: 'any', + tunnelProtocol: 'any', + openvpnConstraints: { + port: 'any', + protocol: 'any', + }, + wireguardConstraints: { + port: 'any', + }, }, }, bridgeSettings: { @@ -611,8 +618,8 @@ class ApplicationMain { let fnHasWantedTunnels = hasOpenVpnTunnels; if ('normal' in relaySettings) { - const tunnelConstraints = relaySettings.normal.tunnel; - if (tunnelConstraints !== 'any' && 'wireguard' in tunnelConstraints.only) { + const tunnelConstraints = relaySettings.normal.tunnelProtocol; + if (tunnelConstraints !== 'any' && 'wireguard' === tunnelConstraints.only) { fnHasWantedTunnels = hasWireguardTunnels; } } diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index 6d5d9ae5ab..d77cf65e89 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -37,6 +37,7 @@ import { IRelayList, ISettings, KeygenEvent, + liftConstraint, RelaySettings, RelaySettingsUpdate, TunnelState, @@ -375,43 +376,30 @@ export default class AppRenderer { if ('normal' in relaySettings) { const normal = relaySettings.normal; - const tunnel = normal.tunnel; + const tunnelProtocol = normal.tunnelProtocol; const location = normal.location; - const relayLocation = location === 'any' ? 'any' : location.only; - if (tunnel === 'any') { + if (tunnelProtocol === 'any' || tunnelProtocol.only === 'openvpn') { + const { port, protocol } = normal.openvpnConstraints; actions.settings.updateRelay({ normal: { location: relayLocation, - port: 'any', - protocol: 'any', + port: port === 'any' ? port : port.only, + protocol: protocol === 'any' ? protocol : protocol.only, + tunnelProtocol: liftConstraint(tunnelProtocol), }, }); } else { - const constraints = tunnel.only; - - if ('openvpn' in constraints) { - const { port, protocol } = constraints.openvpn; - - actions.settings.updateRelay({ - normal: { - location: relayLocation, - port: port === 'any' ? port : port.only, - protocol: protocol === 'any' ? protocol : protocol.only, - }, - }); - } else if ('wireguard' in constraints) { - const { port } = constraints.wireguard; - - actions.settings.updateRelay({ - normal: { - location: relayLocation, - port: port === 'any' ? port : port.only, - protocol: 'udp', - }, - }); - } + const { port } = normal.wireguardConstraints; + actions.settings.updateRelay({ + normal: { + tunnelProtocol: liftConstraint(tunnelProtocol), + location: relayLocation, + port: port === 'any' ? port : port.only, + protocol: 'udp', + }, + }); } } else if ('customTunnelEndpoint' in relaySettings) { const customTunnelEndpoint = relaySettings.customTunnelEndpoint; diff --git a/gui/src/renderer/lib/relay-settings-builder.ts b/gui/src/renderer/lib/relay-settings-builder.ts index 72dc95e759..bbf85b1fed 100644 --- a/gui/src/renderer/lib/relay-settings-builder.ts +++ b/gui/src/renderer/lib/relay-settings-builder.ts @@ -80,19 +80,13 @@ class NormalRelaySettingsBuilder { get tunnel(): ITunnelBuilder { const updateOpenvpn = (next: Partial<IOpenVpnConstraints>) => { - const tunnel = this.payload.tunnel; - if (typeof tunnel === 'string' || typeof tunnel === 'undefined') { - this.payload.tunnel = { - only: { - openvpn: next, - }, - }; - } else if (typeof tunnel === 'object') { - const prev = tunnel.only && 'openvpn' in tunnel.only ? tunnel.only.openvpn : {}; - this.payload.tunnel = { - only: { - openvpn: { ...prev, ...next }, - }, + if (this.payload.openvpnConstraints === undefined) { + this.payload.openvpnConstraints = next; + } else { + const prev = this.payload.openvpnConstraints; + this.payload.openvpnConstraints = { + ...prev, + ...next, }; } }; @@ -127,7 +121,7 @@ class NormalRelaySettingsBuilder { return this; }, any: () => { - this.payload.tunnel = 'any'; + this.payload.tunnelProtocol = 'any'; return this; }, }; diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts index 97a6a21ee4..e41750b496 100644 --- a/gui/src/renderer/redux/settings/reducers.ts +++ b/gui/src/renderer/redux/settings/reducers.ts @@ -4,6 +4,7 @@ import { KeygenEvent, RelayLocation, RelayProtocol, + TunnelProtocol, } from '../../../shared/daemon-rpc-types'; import { IGuiSettingsState } from '../../../shared/gui-settings-state'; import { ReduxAction } from '../store'; @@ -11,6 +12,7 @@ import { ReduxAction } from '../store'; export type RelaySettingsRedux = | { normal: { + tunnelProtocol: 'any' | TunnelProtocol; location: 'any' | RelayLocation; port: 'any' | number; protocol: 'any' | RelayProtocol; @@ -108,6 +110,7 @@ const initialState: ISettingsReduxState = { relaySettings: { normal: { location: 'any', + tunnelProtocol: 'any', port: 'any', protocol: 'any', }, diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts index 5d2c77d223..8dc87cc2b1 100644 --- a/gui/src/shared/daemon-rpc-types.ts +++ b/gui/src/shared/daemon-rpc-types.ts @@ -44,6 +44,13 @@ export function tunnelTypeToString(tunnel: TunnelType): string { export type RelayProtocol = 'tcp' | 'udp'; +export function liftConstraint<T>(constraint: 'any' | { only: T }): 'any' | T { + if (constraint === 'any') { + return 'any'; + } + return constraint.only; +} + export type ProxyType = 'shadowsocks' | 'custom'; export function proxyTypeToString(proxy: ProxyType): string { switch (proxy) { @@ -101,19 +108,21 @@ export interface IWireguardConstraints { port: 'any' | { only: number }; } -type TunnelConstraints<OpenVpn, Wireguard> = { wireguard: Wireguard } | { openvpn: OpenVpn }; +export type TunnelProtocol = 'wireguard' | 'openvpn'; -interface IRelaySettingsNormal<TTunnelConstraints> { +interface IRelaySettingsNormal<OpenVpn, Wireguard> { location: | 'any' | { only: RelayLocation; }; - tunnel: + tunnelProtocol: | 'any' | { - only: TTunnelConstraints; + only: TunnelProtocol; }; + openvpnConstraints: OpenVpn; + wireguardConstraints: Wireguard; } export type ConnectionConfig = @@ -150,7 +159,7 @@ export interface IRelaySettingsCustom { } export type RelaySettings = | { - normal: IRelaySettingsNormal<TunnelConstraints<IOpenVpnConstraints, IWireguardConstraints>>; + normal: IRelaySettingsNormal<IOpenVpnConstraints, IWireguardConstraints>; } | { customTunnelEndpoint: IRelaySettingsCustom; @@ -158,9 +167,7 @@ export type RelaySettings = // types describing the partial update of RelaySettings export type RelaySettingsNormalUpdate = Partial< - IRelaySettingsNormal< - TunnelConstraints<Partial<IOpenVpnConstraints>, Partial<IWireguardConstraints>> - > + IRelaySettingsNormal<Partial<IOpenVpnConstraints>, Partial<IWireguardConstraints>> >; export type RelaySettingsUpdate = diff --git a/gui/test/relay-settings-builder.spec.ts b/gui/test/relay-settings-builder.spec.ts index fd7f4c1cf4..f9ceccd744 100644 --- a/gui/test/relay-settings-builder.spec.ts +++ b/gui/test/relay-settings-builder.spec.ts @@ -54,13 +54,9 @@ describe('Relay settings builder', () => { .build(), ).to.deep.equal({ normal: { - tunnel: { - only: { - openvpn: { - port: 'any', - protocol: 'any', - }, - }, + openvpnConstraints: { + port: 'any', + protocol: 'any', }, }, }); @@ -75,13 +71,9 @@ describe('Relay settings builder', () => { .build(), ).to.deep.equal({ normal: { - tunnel: { - only: { - openvpn: { - port: { only: 80 }, - protocol: { only: 'tcp' }, - }, - }, + openvpnConstraints: { + port: { only: 80 }, + protocol: { only: 'tcp' }, }, }, }); diff --git a/mullvad-cli/src/cmds/relay.rs b/mullvad-cli/src/cmds/relay.rs index c9bf61be43..fe411b06a2 100644 --- a/mullvad-cli/src/cmds/relay.rs +++ b/mullvad-cli/src/cmds/relay.rs @@ -9,7 +9,7 @@ use std::{ use mullvad_types::{ relay_constraints::{ Constraint, OpenVpnConstraints, RelayConstraintsUpdate, RelaySettingsUpdate, - TunnelConstraints, WireguardConstraints, + TunnelProtocol, WireguardConstraints, }, ConnectionConfig, CustomTunnelEndpoint, }; @@ -271,7 +271,7 @@ impl Relay { self.update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { location: Some(location_constraint), - tunnel: None, + ..Default::default() })) } @@ -287,17 +287,17 @@ impl Relay { } self.update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { location: None, - tunnel: Some(Constraint::Only(TunnelConstraints::Wireguard( - WireguardConstraints { port }, - ))), + tunnel_protocol: Some(Constraint::Only(TunnelProtocol::Wireguard)), + wireguard_constraints: Some(WireguardConstraints { port }), + ..Default::default() })) } "openvpn" => { self.update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { location: None, - tunnel: Some(Constraint::Only(TunnelConstraints::OpenVpn( - OpenVpnConstraints { port, protocol }, - ))), + tunnel_protocol: Some(Constraint::Any), + openvpn_constraints: Some(OpenVpnConstraints { port, protocol }), + ..Default::default() })) } _ => unreachable!(), diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index 27594c8adf..70acaead19 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -38,7 +38,7 @@ use mullvad_types::{ location::GeoIpLocation, relay_constraints::{ BridgeSettings, BridgeState, Constraint, InternalBridgeConstraints, OpenVpnConstraints, - RelayConstraintsUpdate, RelaySettings, RelaySettingsUpdate, TunnelConstraints, + RelayConstraintsUpdate, RelaySettings, RelaySettingsUpdate, TunnelProtocol, }, relay_list::{Relay, RelayList}, states::{TargetState, TunnelState}, @@ -1164,16 +1164,13 @@ where // Set the OpenVPN tunnel to use TCP. fn apply_proxy_constraints(&mut self) -> settings::Result<bool> { - let openvpn_constraints = OpenVpnConstraints { - port: Constraint::Any, - protocol: Constraint::Only(TransportProtocol::Tcp), - }; - - let tunnel_constraints = TunnelConstraints::OpenVpn(openvpn_constraints); - let constraints_update = RelayConstraintsUpdate { - location: None, - tunnel: Some(Constraint::Only(tunnel_constraints)), + tunnel_protocol: Some(Constraint::Only(TunnelProtocol::OpenVpn)), + openvpn_constraints: Some(OpenVpnConstraints { + protocol: Constraint::Only(TransportProtocol::Tcp), + port: Constraint::Any, + }), + ..Default::default() }; let settings_update = RelaySettingsUpdate::Normal(constraints_update); diff --git a/mullvad-daemon/src/relays.rs b/mullvad-daemon/src/relays.rs index 5f238244fd..025c535e6c 100644 --- a/mullvad-daemon/src/relays.rs +++ b/mullvad-daemon/src/relays.rs @@ -6,7 +6,7 @@ use mullvad_types::{ location::Location, relay_constraints::{ Constraint, InternalBridgeConstraints, LocationConstraint, Match, OpenVpnConstraints, - RelayConstraints, TunnelConstraints, WireguardConstraints, + RelayConstraints, TunnelProtocol, WireguardConstraints, }, relay_list::{Relay, RelayList, RelayTunnels, WireguardEndpointData}, }; @@ -239,40 +239,48 @@ impl RelaySelector { _ => (Constraint::Any, TransportProtocol::Tcp), }; + let mut relay_constraints = RelayConstraints { + location: original_constraints.location.clone(), + tunnel_protocol: original_constraints.tunnel_protocol.clone(), + ..Default::default() + }; // Highest priority preference. Where we prefer OpenVPN using UDP. But without changing // any constraints that are explicitly specified. - let tunnel_constraints = match original_constraints.tunnel { - // No constraints, we use our preferred ones. + match original_constraints.tunnel_protocol { + // If no tunnel protocol is selected, use preferred constraints #[cfg(not(target_os = "android"))] - Constraint::Any => TunnelConstraints::OpenVpn(OpenVpnConstraints { - port: preferred_port, - protocol: Constraint::Only(preferred_protocol), - }), - #[cfg(target_os = "android")] - Constraint::Any => TunnelConstraints::Wireguard(WireguardConstraints { - port: Constraint::Any, - }), - Constraint::Only(TunnelConstraints::OpenVpn(ref openvpn_constraints)) => { - match openvpn_constraints { - // Constrained to OpenVpn, but port/protocol not constrained. Use our preferred. - OpenVpnConstraints { - port: Constraint::Any, - protocol: Constraint::Any, - } => TunnelConstraints::OpenVpn(OpenVpnConstraints { + Constraint::Any => { + if original_constraints.openvpn_constraints.port.is_any() + && original_constraints.openvpn_constraints.protocol.is_any() + { + relay_constraints.openvpn_constraints = OpenVpnConstraints { port: preferred_port, protocol: Constraint::Only(preferred_protocol), - }), - // Other constraints, use the original constraints. - openvpn_constraints => TunnelConstraints::OpenVpn(openvpn_constraints.clone()), + }; + } else { + relay_constraints.openvpn_constraints = OpenVpnConstraints { + port: original_constraints.openvpn_constraints.port, + protocol: original_constraints.openvpn_constraints.protocol, + }; } } - // Non-OpenVPN constraints. Respect and keep those constraints. - Constraint::Only(ref tunnel_constraints) => tunnel_constraints.clone(), - }; - RelayConstraints { - location: original_constraints.location.clone(), - tunnel: Constraint::Only(tunnel_constraints), + #[cfg(not(target_os = "android"))] + Constraint::Only(TunnelProtocol::OpenVpn) => { + relay_constraints.openvpn_constraints = original_constraints.openvpn_constraints; + } + #[cfg(not(target_os = "android"))] + Constraint::Only(TunnelProtocol::Wireguard) => { + relay_constraints.wireguard_constraints = + original_constraints.wireguard_constraints; + } + #[cfg(target_os = "android")] + _ => { + relay_constraints.wireguard_constraints = + original_constraints.wireguard_constraints; + } } + + relay_constraints } pub fn get_auto_proxy_settings( @@ -350,7 +358,7 @@ impl RelaySelector { "Selected relay {} at {}", selected_relay.hostname, selected_relay.ipv4_addr_in ); - self.get_random_tunnel(&selected_relay, &constraints.tunnel) + self.get_random_tunnel(&selected_relay, &constraints) .map(|endpoint| (selected_relay.clone(), endpoint)) }) } @@ -362,22 +370,34 @@ impl RelaySelector { return None; } - let relay = match constraints.tunnel { + let relay = match constraints.tunnel_protocol { Constraint::Any => relay.clone(), - Constraint::Only(ref tunnel_constraints) => { + Constraint::Only(TunnelProtocol::Wireguard) => { let mut relay = relay.clone(); - relay.tunnels = Self::matching_tunnels(&relay.tunnels, tunnel_constraints); + relay.tunnels = Self::matching_wireguard_tunnels( + &relay.tunnels, + &constraints.wireguard_constraints, + ); + relay + } + + Constraint::Only(TunnelProtocol::OpenVpn) => { + let mut relay = relay.clone(); + relay.tunnels = Self::matching_openvpn_tunnels( + &relay.tunnels, + &constraints.openvpn_constraints, + ); relay } }; - let relay_matches = match constraints.tunnel { + + + let relay_matches = match constraints.tunnel_protocol { Constraint::Any => { !relay.tunnels.openvpn.is_empty() || !relay.tunnels.wireguard.is_empty() } - Constraint::Only(TunnelConstraints::OpenVpn(_)) => !relay.tunnels.openvpn.is_empty(), - Constraint::Only(TunnelConstraints::Wireguard(_)) => { - !relay.tunnels.wireguard.is_empty() - } + Constraint::Only(TunnelProtocol::OpenVpn) => !relay.tunnels.openvpn.is_empty(), + Constraint::Only(TunnelProtocol::Wireguard) => !relay.tunnels.wireguard.is_empty(), }; if relay_matches { @@ -432,29 +452,35 @@ impl RelaySelector { Some(filtered_relay) } - /// Takes a `RelayTunnels` object which in turn is a collection of tunnel configurations for - /// a given relay. Then returns a new `RelayTunnels` instance with only the entries that - /// matches the given `TunnelConstraints`. - fn matching_tunnels( + fn matching_openvpn_tunnels( tunnels: &RelayTunnels, - tunnel_constraints: &TunnelConstraints, + constraints: &OpenVpnConstraints, ) -> RelayTunnels { RelayTunnels { openvpn: tunnels .openvpn .iter() - .filter(|endpoint| tunnel_constraints.matches(*endpoint)) + .filter(|endpoint| constraints.matches(*endpoint)) .cloned() .collect(), + wireguard: vec![], + } + } + + fn matching_wireguard_tunnels( + tunnels: &RelayTunnels, + constraints: &WireguardConstraints, + ) -> RelayTunnels { + RelayTunnels { + openvpn: vec![], wireguard: tunnels .wireguard .iter() - .filter(|endpoint| tunnel_constraints.matches(*endpoint)) + .filter(|endpoint| constraints.matches(*endpoint)) .cloned() .collect(), } } - /// Pick a random relay from the given slice. Will return `None` if the given slice is empty /// or all relays in it has zero weight. fn pick_random_relay<'a>(&mut self, relays: &'a [Relay]) -> Option<&'a Relay> { @@ -493,25 +519,29 @@ impl RelaySelector { fn get_random_tunnel( &mut self, relay: &Relay, - constraints: &Constraint<TunnelConstraints>, + constraints: &RelayConstraints, ) -> Option<MullvadEndpoint> { - match constraints { + match constraints.tunnel_protocol { // TODO: Handle Constraint::Any case by selecting from both openvpn and wireguard // tunnels once wireguard is mature enough #[cfg(not(target_os = "android"))] - Constraint::Only(TunnelConstraints::OpenVpn(_)) | Constraint::Any => relay + Constraint::Only(TunnelProtocol::OpenVpn) | Constraint::Any => relay .tunnels .openvpn .choose(&mut self.rng) .cloned() .map(|endpoint| endpoint.into_mullvad_endpoint(relay.ipv4_addr_in.into())), - Constraint::Only(TunnelConstraints::Wireguard(wg_constraints)) => relay + Constraint::Only(TunnelProtocol::Wireguard) => relay .tunnels .wireguard .choose(&mut self.rng) .cloned() .and_then(|wg_tunnel| { - self.wg_data_to_endpoint(relay.ipv4_addr_in.into(), wg_tunnel, wg_constraints) + self.wg_data_to_endpoint( + relay.ipv4_addr_in.into(), + wg_tunnel, + &constraints.wireguard_constraints, + ) }), #[cfg(target_os = "android")] Constraint::Any => relay @@ -527,7 +557,7 @@ impl RelaySelector { ) }), #[cfg(target_os = "android")] - Constraint::Only(TunnelConstraints::OpenVpn(_)) => None, + Constraint::Only(TunnelProtocol::OpenVpn) => None, } } @@ -537,7 +567,7 @@ impl RelaySelector { data: WireguardEndpointData, constraints: &WireguardConstraints, ) -> Option<MullvadEndpoint> { - let port = self.get_port_for_wireguard_relay(&data, constraints)?; + let port = self.get_port_for_wireguard_relay(&data, &constraints)?; let peer_config = wireguard::PeerConfig { public_key: data.public_key, endpoint: SocketAddr::new(host, port), diff --git a/mullvad-jni/src/from_java.rs b/mullvad-jni/src/from_java.rs index 3acd9717ef..c016134f23 100644 --- a/mullvad-jni/src/from_java.rs +++ b/mullvad-jni/src/from_java.rs @@ -110,7 +110,9 @@ impl<'env> FromJava<'env> for RelayConstraintsUpdate { RelayConstraintsUpdate { location: FromJava::from_java(env, location), - tunnel: None, + tunnel_protocol: None, + openvpn_constraints: None, + wireguard_constraints: None, } } } diff --git a/mullvad-types/src/relay_constraints.rs b/mullvad-types/src/relay_constraints.rs index cd901c6bbc..562bf0bee8 100644 --- a/mullvad-types/src/relay_constraints.rs +++ b/mullvad-types/src/relay_constraints.rs @@ -33,6 +33,13 @@ impl<T: fmt::Debug + Clone + Eq + PartialEq> Constraint<T> { Constraint::Only(value) => Constraint::Only(value), } } + + pub fn is_any(&self) -> bool { + match self { + Constraint::Any => true, + Constraint::Only(_value) => false, + } + } } impl<T: fmt::Debug + Clone + Eq + PartialEq> Default for Constraint<T> { @@ -89,23 +96,43 @@ impl RelaySettings { #[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] pub struct RelayConstraints { pub location: Constraint<LocationConstraint>, - pub tunnel: Constraint<TunnelConstraints>, + pub tunnel_protocol: Constraint<TunnelProtocol>, + pub wireguard_constraints: WireguardConstraints, + pub openvpn_constraints: OpenVpnConstraints, } impl RelayConstraints { pub fn merge(&self, update: RelayConstraintsUpdate) -> Self { RelayConstraints { location: update.location.unwrap_or_else(|| self.location.clone()), - tunnel: update.tunnel.unwrap_or_else(|| self.tunnel.clone()), + tunnel_protocol: update + .tunnel_protocol + .unwrap_or_else(|| self.tunnel_protocol.clone()), + wireguard_constraints: update + .wireguard_constraints + .unwrap_or_else(|| self.wireguard_constraints.clone()), + openvpn_constraints: update + .openvpn_constraints + .unwrap_or_else(|| self.openvpn_constraints.clone()), } } } impl fmt::Display for RelayConstraints { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - match self.tunnel { - Constraint::Any => write!(f, "any relay")?, - Constraint::Only(ref tunnel_constraint) => tunnel_constraint.fmt(f)?, + match self.tunnel_protocol { + Constraint::Any => write!(f, "any tunnel protocol")?, + Constraint::Only(ref tunnel_protocol) => { + tunnel_protocol.fmt(f)?; + match tunnel_protocol { + TunnelProtocol::Wireguard => { + write!(f, " over {}", &self.wireguard_constraints)?; + } + TunnelProtocol::OpenVpn => { + write!(f, " over {}", &self.openvpn_constraints)?; + } + }; + } } write!(f, " in ")?; match self.location { @@ -140,6 +167,23 @@ impl fmt::Display for LocationConstraint { } #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +pub enum TunnelProtocol { + #[serde(rename = "wireguard")] + Wireguard, + #[serde(rename = "openvpn")] + OpenVpn, +} + +impl fmt::Display for TunnelProtocol { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + TunnelProtocol::Wireguard => write!(f, "WireGuard"), + TunnelProtocol::OpenVpn => write!(f, "OpenVPN"), + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] pub enum TunnelConstraints { #[serde(rename = "openvpn")] OpenVpn(OpenVpnConstraints), @@ -180,7 +224,7 @@ impl Match<WireguardEndpointData> for TunnelConstraints { } } -#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Deserialize, Serialize)] pub struct OpenVpnConstraints { pub port: Constraint<u16>, pub protocol: Constraint<TransportProtocol>, @@ -206,7 +250,7 @@ impl Match<OpenVpnEndpointData> for OpenVpnConstraints { } } -#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Deserialize, Serialize)] pub struct WireguardConstraints { pub port: Constraint<u16>, } @@ -296,5 +340,7 @@ pub enum RelaySettingsUpdate { #[serde(default)] pub struct RelayConstraintsUpdate { pub location: Option<Constraint<LocationConstraint>>, - pub tunnel: Option<Constraint<TunnelConstraints>>, + pub tunnel_protocol: Option<Constraint<TunnelProtocol>>, + pub wireguard_constraints: Option<WireguardConstraints>, + pub openvpn_constraints: Option<OpenVpnConstraints>, } diff --git a/mullvad-types/src/settings/migrations/mod.rs b/mullvad-types/src/settings/migrations/mod.rs new file mode 100644 index 0000000000..521d462f81 --- /dev/null +++ b/mullvad-types/src/settings/migrations/mod.rs @@ -0,0 +1,145 @@ +use super::{Error, Result, Settings}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::io::Read; +mod v1; + + +#[derive(Debug, PartialEq, Clone, Copy)] +#[repr(u32)] +pub enum SettingsVersion { + V2 = 2, +} + +impl SettingsVersion { + pub fn as_u32(&self) -> u32 { + unsafe { ::std::mem::transmute(*self) } + } + + pub fn max_version() -> Self { + SettingsVersion::V2 + } + + pub fn min_version() -> Self { + SettingsVersion::V2 + } +} + +impl<'de> Deserialize<'de> for SettingsVersion { + fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let version = <u32>::deserialize(deserializer)?; + if version < SettingsVersion::min_version().as_u32() { + return Err(serde::de::Error::custom(format!( + "Version number {} too small", + version + ))); + } + + if version > SettingsVersion::max_version().as_u32() { + return Err(serde::de::Error::custom(format!( + "Version number {} too large", + version + ))); + } + + unsafe { Ok(::std::mem::transmute(version)) } + } +} + +impl Serialize for SettingsVersion { + fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_u32(self.as_u32()) + } +} + + +#[derive(Debug)] +enum VersionedSettings { + V1(v1::Settings), + V2(crate::settings::Settings), +} + +impl VersionedSettings { + /// Unrwaps the latest version of settings or panics. + fn unwrap(self) -> Settings { + match self { + VersionedSettings::V2(settings) => settings, + lower => { + panic!("Unexpected settings version - {:?}", lower); + } + } + } +} + + +trait SettingsMigration { + fn read(&self, reader: &mut dyn Read) -> Result<VersionedSettings>; + fn migrate(&self, settings: VersionedSettings) -> VersionedSettings; +} + +fn migrations() -> Vec<Box<dyn SettingsMigration>> { + vec![Box::new(v1::Migration)] +} + +pub fn try_migrate_settings(mut settings_file: &[u8]) -> Result<crate::settings::Settings> { + let mut migrations_to_apply = vec![]; + let mut valid_settings = None; + + let migrations = migrations(); + for migration in migrations.iter() { + match migration.read(&mut settings_file) { + Ok(settings) => { + valid_settings = Some(migration.migrate(settings)); + break; + } + Err(_e) => { + migrations_to_apply.push(migration); + } + }; + } + + if let Some(settings) = valid_settings { + let upgraded_settings = migrations_to_apply + .iter() + .rev() + .fold(settings, |old_settings, migration| { + migration.migrate(old_settings) + }); + return Ok(upgraded_settings.unwrap()); + } + return Err(Error::NoMatchingVersion); +} + +#[cfg(test)] +mod test { + use super::SettingsVersion; + + #[test] + #[should_panic] + fn test_deserialization_failure_version_too_small() { + let _version: SettingsVersion = serde_json::from_str("1").expect("Version too small"); + } + + #[test] + #[should_panic] + fn test_deserialization_failure_version_too_big() { + let _version: SettingsVersion = serde_json::from_str("3").expect("Version too big"); + } + + #[test] + fn test_deserialization_success() { + let _version: SettingsVersion = serde_json::from_str("2").expect("Failed to deserialize valid version"); + } + + #[test] + fn test_serialization_success() { + let version = SettingsVersion::V2; + let s = serde_json::to_string(&version).expect("Failed to serialize"); + assert_eq!(s, "2"); + } +} diff --git a/mullvad-types/src/settings/migrations/v1.rs b/mullvad-types/src/settings/migrations/v1.rs new file mode 100644 index 0000000000..a5b4e7c618 --- /dev/null +++ b/mullvad-types/src/settings/migrations/v1.rs @@ -0,0 +1,221 @@ +use super::{Error, Result, VersionedSettings}; +use crate::{ + custom_tunnel::CustomTunnelEndpoint, + relay_constraints::{ + BridgeSettings, BridgeState, Constraint, LocationConstraint, OpenVpnConstraints, + RelaySettings as NewRelaySettings, TunnelProtocol, WireguardConstraints, + }, + settings::TunnelOptions, +}; +use serde::{Deserialize, Serialize}; +use std::io::Read; + + +/// Mullvad daemon settings. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Settings { + account_token: Option<String>, + relay_settings: RelaySettings, + bridge_settings: BridgeSettings, + bridge_state: BridgeState, + /// If the daemon should allow communication with private (LAN) networks. + allow_lan: bool, + /// Extra level of kill switch. When this setting is on, the disconnected state will block + /// the firewall to not allow any traffic in or out. + block_when_disconnected: bool, + /// If the daemon should connect the VPN tunnel directly on start or not. + auto_connect: bool, + /// Options that should be applied to tunnels of a specific type regardless of where the relays + /// might be located. + tunnel_options: TunnelOptions, +} + +pub(super) struct Migration; +impl super::SettingsMigration for Migration { + fn read(&self, mut reader: &mut dyn Read) -> Result<VersionedSettings> { + serde_json::from_reader(&mut reader) + .map(VersionedSettings::V1) + .map_err(Error::ParseError) + } + fn migrate(&self, old: VersionedSettings) -> VersionedSettings { + match old { + VersionedSettings::V1(old) => VersionedSettings::V2(crate::settings::Settings { + account_token: old.account_token, + relay_settings: migrate_relay_settings(old.relay_settings), + bridge_settings: old.bridge_settings, + bridge_state: old.bridge_state, + allow_lan: old.allow_lan, + block_when_disconnected: old.block_when_disconnected, + auto_connect: old.auto_connect, + tunnel_options: old.tunnel_options, + settings_version: super::SettingsVersion::V2, + }), + VersionedSettings::V2(new) => VersionedSettings::V2(new), + } + } +} + +fn migrate_relay_settings(relay_settings: RelaySettings) -> NewRelaySettings { + match relay_settings { + RelaySettings::CustomTunnelEndpoint(endpoint) => { + crate::relay_constraints::RelaySettings::CustomTunnelEndpoint(endpoint) + } + RelaySettings::Normal(old_constraints) => { + let mut new_constraints = crate::relay_constraints::RelayConstraints { + location: old_constraints.location, + ..Default::default() + }; + match old_constraints.tunnel { + Constraint::Any => (), + Constraint::Only(TunnelConstraints::OpenVpn(constraints)) => { + new_constraints.openvpn_constraints = constraints; + } + Constraint::Only(TunnelConstraints::Wireguard(constraints)) => { + new_constraints.wireguard_constraints = constraints; + new_constraints.tunnel_protocol = Constraint::Only(TunnelProtocol::Wireguard); + } + }; + crate::relay_constraints::RelaySettings::Normal(new_constraints) + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum RelaySettings { + CustomTunnelEndpoint(CustomTunnelEndpoint), + Normal(RelayConstraints), +} + +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +pub struct RelayConstraints { + pub location: Constraint<LocationConstraint>, + pub tunnel: Constraint<TunnelConstraints>, +} + +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +pub enum TunnelConstraints { + #[serde(rename = "openvpn")] + OpenVpn(OpenVpnConstraints), + #[serde(rename = "wireguard")] + Wireguard(WireguardConstraints), +} + +#[cfg(test)] +mod test { + use super::super::SettingsMigration; + use serde_json; + const OLD_SETTINGS: &str = r#" +{ + "account_token": "1234", + "relay_settings": { + "normal": { + "location": { + "only": { + "country": "se" + } + }, + "tunnel": { + "only": { + "openvpn": { + "port": { + "only": 53 + }, + "protocol": { + "only": "udp" + } + } + } + } + } + }, + "bridge_settings": { + "normal": { + "location": "any" + } + }, + "bridge_state": "auto", + "allow_lan": true, + "block_when_disconnected": false, + "auto_connect": false, + "tunnel_options": { + "openvpn": { + "mssfix": null + }, + "wireguard": { + "mtu": null + }, + "generic": { + "enable_ipv6": false + } + } +} +"#; + + const NEW_SETTINGS: &str = r#" +{ + "account_token": "1234", + "relay_settings": { + "normal": { + "location": { + "only": { + "country": "se" + } + }, + "tunnel_protocol": "any", + "wireguard_constraints": { + "port": "any" + }, + "openvpn_constraints": { + "port": { + "only": 53 + }, + "protocol": { + "only": "udp" + } + } + } + }, + "bridge_settings": { + "normal": { + "location": "any" + } + }, + "bridge_state": "auto", + "allow_lan": true, + "block_when_disconnected": false, + "auto_connect": false, + "tunnel_options": { + "openvpn": { + "mssfix": null + }, + "wireguard": { + "mtu": null + }, + "generic": { + "enable_ipv6": false + } + }, + "settings_version": 2 +} +"#; + + #[test] + fn test_migration() { + let m = super::Migration; + let old_settings = m + .read(&mut OLD_SETTINGS.as_bytes()) + .expect("Failed to deserialize old format"); + let new_settings = serde_json::from_str(&NEW_SETTINGS).unwrap(); + + assert_eq!(&m.migrate(old_settings).unwrap(), &new_settings); + } + + #[test] + #[should_panic] + fn test_deserialization_failure() { + let m = super::Migration; + m.read(&mut NEW_SETTINGS.as_bytes()) + .expect("Failed to deserialize old format"); + } +} diff --git a/mullvad-types/src/settings.rs b/mullvad-types/src/settings/mod.rs index dab81bf43a..c2f84cff59 100644 --- a/mullvad-types/src/settings.rs +++ b/mullvad-types/src/settings/mod.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use serde_json; use std::{ fs::{self, File}, - io, + io::{self, Read}, path::PathBuf, }; use talpid_types::{ @@ -15,6 +15,7 @@ use talpid_types::{ ErrorExt, }; +mod migrations; pub type Result<T> = std::result::Result<T, Error>; @@ -40,13 +41,16 @@ pub enum Error { #[error(display = "Invalid OpenVPN proxy configuration: {}", _0)] InvalidProxyData(String), + + #[error(display = "Unable to read any version of the settings")] + NoMatchingVersion, } static SETTINGS_FILE: &str = "settings.json"; /// Mullvad daemon settings. -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] #[serde(default)] pub struct Settings { account_token: Option<String>, @@ -63,6 +67,8 @@ pub struct Settings { /// Options that should be applied to tunnels of a specific type regardless of where the relays /// might be located. tunnel_options: TunnelOptions, + /// Specifies settings schema version + settings_version: migrations::SettingsVersion, } impl Default for Settings { @@ -71,7 +77,7 @@ impl Default for Settings { account_token: None, relay_settings: RelaySettings::Normal(RelayConstraints { location: Constraint::Only(LocationConstraint::Country("se".to_owned())), - tunnel: Constraint::Any, + ..Default::default() }), bridge_settings: BridgeSettings::Normal(BridgeConstraints { location: Constraint::Any, @@ -81,6 +87,7 @@ impl Default for Settings { block_when_disconnected: false, auto_connect: false, tunnel_options: TunnelOptions::default(), + settings_version: migrations::SettingsVersion::V2, } } } @@ -92,7 +99,21 @@ impl Settings { match File::open(&path) { Ok(file) => { info!("Loading settings from {}", path.display()); - Self::read_settings(&mut io::BufReader::new(file)) + let mut settings_bytes = vec![]; + io::BufReader::new(file) + .read_to_end(&mut settings_bytes) + .map_err(|e| Error::ReadError("Failed to read settings file".to_owned(), e))?; + Self::parse_settings(&mut settings_bytes).or_else(|e| { + log::error!( + "{}", + e.display_chain_with_msg("Failed to parse settings file") + ); + let settings = migrations::try_migrate_settings(&settings_bytes)?; + if let Err(e) = settings.save() { + log::error!("Failed to save settings after migration: {}", e); + } + Ok(settings) + }) } Err(e) => Err(Error::ReadError(path.display().to_string(), e)), } @@ -132,8 +153,8 @@ impl Settings { Ok(dir.join(SETTINGS_FILE)) } - fn read_settings<T: io::Read>(file: &mut T) -> Result<Settings> { - serde_json::from_reader(file).map_err(Error::ParseError) + fn parse_settings(bytes: &[u8]) -> Result<Settings> { + serde_json::from_slice(bytes).map_err(Error::ParseError) } pub fn get_account_token(&self) -> Option<String> { |
