summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorLinus Färnstrand <linus@mullvad.net>2018-09-05 18:39:51 +0200
committerLinus Färnstrand <linus@mullvad.net>2018-09-05 18:39:51 +0200
commit619e081cb90c3cf6b49c51e81ea6f6c3fbe1d9a9 (patch)
tree5a5601eddbc639aa80808f80bf184ab97fe00220
parentfb40dedf46348e8764aed95610b135e40c3b1ba1 (diff)
parentc3eeeaaa9db90bbf4c6328286854e850d2f8156b (diff)
downloadmullvadvpn-619e081cb90c3cf6b49c51e81ea6f6c3fbe1d9a9.tar.xz
mullvadvpn-619e081cb90c3cf6b49c51e81ea6f6c3fbe1d9a9.zip
Merge branch 'select-individual-servers'
-rw-r--r--CHANGELOG.md1
-rw-r--r--gui/packages/desktop/src/renderer/app.js1
-rw-r--r--gui/packages/desktop/src/renderer/components/SelectLocation.js94
-rw-r--r--gui/packages/desktop/src/renderer/components/SelectLocationStyles.js18
-rw-r--r--gui/packages/desktop/src/renderer/containers/ConnectPage.js9
-rw-r--r--gui/packages/desktop/src/renderer/lib/daemon-rpc.js26
-rw-r--r--gui/packages/desktop/src/renderer/lib/relay-settings-builder.js9
-rw-r--r--gui/packages/desktop/src/renderer/redux/settings/reducers.js9
-rw-r--r--gui/packages/desktop/test/components/SelectLocation.spec.js18
-rw-r--r--mullvad-cli/src/cmds/relay.rs32
-rw-r--r--mullvad-daemon/src/relays.rs18
-rw-r--r--mullvad-types/src/location.rs1
-rw-r--r--mullvad-types/src/relay_constraints.rs4
-rw-r--r--mullvad-types/src/relay_list.rs12
14 files changed, 230 insertions, 22 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 720dac928a..7c9ee2c6cd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -31,6 +31,7 @@ Line wrap the file at 100 chars. Th
configuration, which prevents leaking traffic until the user specifically requests to disconnect.
- Add support for Ubuntu 14.04 and other distributions that use the Upstart init system.
- Make scrollbar thumb draggable.
+- Ability to expand cities with multiple servers and configure the app to use a specific server.
#### Windows
- Extend uninstaller to also remove logs, cache and optionally settings.
diff --git a/gui/packages/desktop/src/renderer/app.js b/gui/packages/desktop/src/renderer/app.js
index 8c170c1446..6cf203f4cb 100644
--- a/gui/packages/desktop/src/renderer/app.js
+++ b/gui/packages/desktop/src/renderer/app.js
@@ -346,6 +346,7 @@ export default class AppRenderer {
latitude: city.latitude,
longitude: city.longitude,
hasActiveRelays: city.has_active_relays,
+ relays: city.relays,
})),
}));
diff --git a/gui/packages/desktop/src/renderer/components/SelectLocation.js b/gui/packages/desktop/src/renderer/components/SelectLocation.js
index 70e2bed978..ee1bb13c2c 100644
--- a/gui/packages/desktop/src/renderer/components/SelectLocation.js
+++ b/gui/packages/desktop/src/renderer/components/SelectLocation.js
@@ -15,6 +15,7 @@ import type {
SettingsReduxState,
RelayLocationRedux,
RelayLocationCityRedux,
+ RelayLocationRelayRedux,
} from '../redux/settings/reducers';
import type { RelayLocation } from '../lib/daemon-rpc';
@@ -48,7 +49,15 @@ export default class SelectLocation extends React.Component<SelectLocationProps,
} else if (location.country) {
this.state.expanded.push(location.country);
} else if (location.city) {
- this.state.expanded.push(location.city[0]);
+ const countryCode = location.city[0];
+
+ this.state.expanded.push(countryCode);
+ } else if (location.hostname) {
+ const countryCode = location.hostname[0];
+ const cityCode = location.hostname[1];
+
+ this.state.expanded.push(countryCode);
+ this.state.expanded.push(`${countryCode}_${cityCode}`);
}
}
}
@@ -124,6 +133,16 @@ export default class SelectLocation extends React.Component<SelectLocationProps,
selectedCity.every((v, i) => v === otherCity[i])
);
}
+
+ if (Array.isArray(selectedLocation.hostname) && Array.isArray(otherLocation.hostname)) {
+ const selectedRelay = selectedLocation.hostname;
+ const otherRelay = otherLocation.hostname;
+
+ return (
+ selectedRelay.length === otherRelay.length &&
+ selectedRelay.every((v, i) => v === otherRelay[i])
+ );
+ }
}
return false;
}
@@ -210,6 +229,7 @@ export default class SelectLocation extends React.Component<SelectLocationProps,
}
_renderCity(countryCode: string, relayCity: RelayLocationCityRedux) {
+ const expandedCode = `${countryCode}_${relayCity.code}`;
const relayLocation: RelayLocation = { city: [countryCode, relayCity.code] };
const isSelected = this._isSelected(relayLocation);
@@ -220,6 +240,9 @@ export default class SelectLocation extends React.Component<SelectLocationProps,
}
: undefined;
+ // either expanded by user or when the city or a relay from the city is selected
+ const isExpanded = this.state.expanded.includes(expandedCode);
+
const handleSelect =
relayCity.hasActiveRelays && !isSelected
? () => {
@@ -227,18 +250,73 @@ export default class SelectLocation extends React.Component<SelectLocationProps,
}
: undefined;
+ const handleCollapse = (e) => {
+ this._toggleCollapse(expandedCode);
+ e.stopPropagation();
+ };
+
+ return (
+ <View key={expandedCode}>
+ <Cell.CellButton
+ onPress={handleSelect}
+ disabled={!relayCity.hasActiveRelays}
+ cellHoverStyle={isSelected ? styles.sub_cell__selected : null}
+ style={isSelected ? styles.sub_cell__selected : styles.sub_cell}
+ testName="city"
+ ref={onRef}>
+ {this._relayStatusIndicator(relayCity.hasActiveRelays, isSelected)}
+
+ <Cell.Label>{relayCity.name}</Cell.Label>
+
+ {relayCity.relays.length > 1 ? (
+ <Cell.Img
+ style={styles.collapse_button}
+ hoverStyle={styles.expand_chevron_hover}
+ onPress={handleCollapse}
+ source={isExpanded ? 'icon-chevron-up' : 'icon-chevron-down'}
+ height={24}
+ width={24}
+ />
+ ) : null}
+ </Cell.CellButton>
+
+ {relayCity.relays.length > 1 && (
+ <Accordion height={isExpanded ? 'auto' : 0}>
+ {relayCity.relays.map((relay) => this._renderRelay(countryCode, relayCity.code, relay))}
+ </Accordion>
+ )}
+ </View>
+ );
+ }
+
+ _renderRelay(countryCode: string, cityCode: string, relay: RelayLocationRelayRedux) {
+ const relayLocation: RelayLocation = { hostname: [countryCode, cityCode, relay.hostname] };
+
+ const isSelected = this._isSelected(relayLocation);
+
+ const onRef = isSelected
+ ? (element) => {
+ this._selectedCell = element;
+ }
+ : undefined;
+
+ const handleSelect = !isSelected
+ ? () => {
+ this.props.onSelect(relayLocation);
+ }
+ : undefined;
+
return (
<Cell.CellButton
- key={`${countryCode}_${relayCity.code}`}
+ key={`${countryCode}_${cityCode}_${relay.hostname}`}
onPress={handleSelect}
- disabled={!relayCity.hasActiveRelays}
- cellHoverStyle={isSelected ? styles.sub_cell__selected : null}
- style={isSelected ? styles.sub_cell__selected : styles.sub_cell}
- testName="city"
+ cellHoverStyle={isSelected ? styles.sub_sub_cell__selected : null}
+ style={isSelected ? styles.sub_sub_cell__selected : styles.sub_sub_cell}
+ testName="relay"
ref={onRef}>
- {this._relayStatusIndicator(relayCity.hasActiveRelays, isSelected)}
+ {this._relayStatusIndicator(true, isSelected)}
- <Cell.Label>{relayCity.name}</Cell.Label>
+ <Cell.Label>{relay.hostname}</Cell.Label>
</Cell.CellButton>
);
}
diff --git a/gui/packages/desktop/src/renderer/components/SelectLocationStyles.js b/gui/packages/desktop/src/renderer/components/SelectLocationStyles.js
index 49cbd441d5..d6d1abec65 100644
--- a/gui/packages/desktop/src/renderer/components/SelectLocationStyles.js
+++ b/gui/packages/desktop/src/renderer/components/SelectLocationStyles.js
@@ -60,6 +60,13 @@ export default {
paddingLeft: 20,
paddingRight: 0,
}),
+ cell_selected: Styles.createViewStyle({
+ paddingTop: 0,
+ paddingBottom: 0,
+ paddingLeft: 20,
+ paddingRight: 0,
+ backgroundColor: colors.green,
+ }),
sub_cell: Styles.createViewStyle({
paddingTop: 0,
paddingBottom: 0,
@@ -74,11 +81,18 @@ export default {
paddingLeft: 40,
backgroundColor: colors.green,
}),
- cell_selected: Styles.createViewStyle({
+ sub_sub_cell: Styles.createViewStyle({
+ paddingTop: 0,
+ paddingBottom: 0,
+ paddingRight: 0,
+ paddingLeft: 60,
+ backgroundColor: colors.blue20,
+ }),
+ sub_sub_cell__selected: Styles.createViewStyle({
paddingTop: 0,
paddingBottom: 0,
- paddingLeft: 20,
paddingRight: 0,
+ paddingLeft: 60,
backgroundColor: colors.green,
}),
expand_chevron_hover: Styles.createViewStyle({
diff --git a/gui/packages/desktop/src/renderer/containers/ConnectPage.js b/gui/packages/desktop/src/renderer/containers/ConnectPage.js
index a1390fab6f..828d92959e 100644
--- a/gui/packages/desktop/src/renderer/containers/ConnectPage.js
+++ b/gui/packages/desktop/src/renderer/containers/ConnectPage.js
@@ -36,6 +36,15 @@ function getRelayName(
return city.name;
}
}
+ } else if (location.hostname) {
+ const [countryCode, cityCode, hostname] = location.hostname;
+ const country = relayLocations.find(({ code }) => code === countryCode);
+ if (country) {
+ const city = country.cities.find(({ code }) => code === cityCode);
+ if (city) {
+ return `${city.name} (${hostname})`;
+ }
+ }
}
return 'Unknown';
diff --git a/gui/packages/desktop/src/renderer/lib/daemon-rpc.js b/gui/packages/desktop/src/renderer/lib/daemon-rpc.js
index e87f51930b..a712f9b73e 100644
--- a/gui/packages/desktop/src/renderer/lib/daemon-rpc.js
+++ b/gui/packages/desktop/src/renderer/lib/daemon-rpc.js
@@ -70,7 +70,10 @@ export type TunnelState =
| BlockedState;
export type RelayProtocol = 'tcp' | 'udp';
-export type RelayLocation = {| city: [string, string] |} | {| country: string |};
+export type RelayLocation =
+ | {| hostname: [string, string, string] |}
+ | {| city: [string, string] |}
+ | {| country: string |};
type OpenVpnConstraints = {
port: 'any' | { only: number },
@@ -139,6 +142,9 @@ const RelaySettingsSchema = oneOf(
location: constraint(
oneOf(
object({
+ hostname: arrayOf(string),
+ }),
+ object({
city: arrayOf(string),
}),
object({
@@ -185,6 +191,15 @@ export type RelayListCity = {
latitude: number,
longitude: number,
has_active_relays: boolean,
+ relays: Array<RelayListHostname>,
+};
+
+export type RelayListHostname = {
+ hostname: string,
+ ipv4_addr_in: string,
+ ipv4_addr_exit: string,
+ include_in_country: boolean,
+ weight: number,
};
const RelayListSchema = object({
@@ -199,6 +214,15 @@ const RelayListSchema = object({
latitude: number,
longitude: number,
has_active_relays: boolean,
+ relays: arrayOf(
+ object({
+ hostname: string,
+ ipv4_addr_in: string,
+ ipv4_addr_exit: string,
+ include_in_country: boolean,
+ weight: number,
+ }),
+ ),
}),
),
}),
diff --git a/gui/packages/desktop/src/renderer/lib/relay-settings-builder.js b/gui/packages/desktop/src/renderer/lib/relay-settings-builder.js
index 5f6c279e3f..5065a485f6 100644
--- a/gui/packages/desktop/src/renderer/lib/relay-settings-builder.js
+++ b/gui/packages/desktop/src/renderer/lib/relay-settings-builder.js
@@ -49,6 +49,10 @@ class NormalRelaySettingsBuilder {
this._payload.location = { only: { city: [country, city] } };
return this;
},
+ hostname: (country: string, city: string, hostname: string) => {
+ this._payload.location = { only: { hostname: [country, city, hostname] } };
+ return this;
+ },
any: () => {
this._payload.location = 'any';
return this;
@@ -58,6 +62,11 @@ class NormalRelaySettingsBuilder {
return this.any();
}
+ if (location.hostname) {
+ const [country, city, hostname] = location.hostname;
+ return this.hostname(country, city, hostname);
+ }
+
if (location.city) {
const [country, city] = location.city;
return this.city(country, city);
diff --git a/gui/packages/desktop/src/renderer/redux/settings/reducers.js b/gui/packages/desktop/src/renderer/redux/settings/reducers.js
index 9870066edc..096dede5c2 100644
--- a/gui/packages/desktop/src/renderer/redux/settings/reducers.js
+++ b/gui/packages/desktop/src/renderer/redux/settings/reducers.js
@@ -19,12 +19,21 @@ export type RelaySettingsRedux =
},
|};
+export type RelayLocationRelayRedux = {
+ hostname: string,
+ ipv4AddrIn: string,
+ ipv4AddrExit: string,
+ includeInCountry: boolean,
+ weight: number,
+};
+
export type RelayLocationCityRedux = {
name: string,
code: string,
latitude: number,
longitude: number,
hasActiveRelays: boolean,
+ relays: Array<RelayLocationRelayRedux>,
};
export type RelayLocationRedux = {
diff --git a/gui/packages/desktop/test/components/SelectLocation.spec.js b/gui/packages/desktop/test/components/SelectLocation.spec.js
index cd45ff2e76..3d35da0edd 100644
--- a/gui/packages/desktop/test/components/SelectLocation.spec.js
+++ b/gui/packages/desktop/test/components/SelectLocation.spec.js
@@ -29,6 +29,15 @@ describe('components/SelectLocation', () => {
latitude: 0,
longitude: 0,
hasActiveRelays: true,
+ relays: [
+ {
+ hostname: 'fake1.mullvad.net',
+ ipv4AddrIn: '192.168.0.100',
+ ipv4AddrExit: '192.168.1.100',
+ includeInCountry: true,
+ weight: 1,
+ },
+ ],
},
{
name: 'Stockholm',
@@ -36,6 +45,15 @@ describe('components/SelectLocation', () => {
latitude: 0,
longitude: 0,
hasActiveRelays: true,
+ relays: [
+ {
+ hostname: 'fake2.mullvad.net',
+ ipv4AddrIn: '192.168.0.101',
+ ipv4AddrExit: '192.168.1.101',
+ includeInCountry: true,
+ weight: 1,
+ },
+ ],
},
],
},
diff --git a/mullvad-cli/src/cmds/relay.rs b/mullvad-cli/src/cmds/relay.rs
index 4de7571307..57cc934b03 100644
--- a/mullvad-cli/src/cmds/relay.rs
+++ b/mullvad-cli/src/cmds/relay.rs
@@ -24,7 +24,9 @@ impl Command for Relay {
.setting(clap::AppSettings::SubcommandRequired)
.subcommand(
clap::SubCommand::with_name("set")
- .setting(clap::AppSettings::SubcommandRequired)
+ .about(
+ "Set relay server selection parameters. Such as location and port/protocol",
+ ).setting(clap::AppSettings::SubcommandRequired)
.subcommand(
clap::SubCommand::with_name("custom")
.about("Set a custom VPN relay")
@@ -67,6 +69,10 @@ impl Command for Relay {
.help("The three letter city code")
.index(2)
.validator(city_code_validator),
+ ).arg(
+ clap::Arg::with_name("hostname")
+ .help("The relay hostname")
+ .index(3),
),
).subcommand(
clap::SubCommand::with_name("tunnel")
@@ -137,18 +143,32 @@ impl Relay {
fn set_location(&self, matches: &clap::ArgMatches) -> Result<()> {
let country = matches.value_of("country").unwrap();
let city = matches.value_of("city");
+ let hostname = matches.value_of("hostname");
- let location_constraint = match (country, city) {
- ("any", None) => Constraint::Any,
- ("any", _) => clap::Error::with_description(
+ let location_constraint = match (country, city, hostname) {
+ ("any", None, None) => Constraint::Any,
+ ("any", ..) => clap::Error::with_description(
"City can't be given when selecting 'any' country",
clap::ErrorKind::InvalidValue,
).exit(),
- (country, None) => Constraint::Only(LocationConstraint::Country(country.to_owned())),
- (country, Some(city)) => Constraint::Only(LocationConstraint::City(
+ (country, None, None) => {
+ Constraint::Only(LocationConstraint::Country(country.to_owned()))
+ }
+ (country, Some(city), None) => Constraint::Only(LocationConstraint::City(
country.to_owned(),
city.to_owned(),
)),
+ (country, Some(city), Some(hostname)) => {
+ Constraint::Only(LocationConstraint::Hostname(
+ country.to_owned(),
+ city.to_owned(),
+ hostname.to_owned(),
+ ))
+ }
+ (..) => clap::Error::with_description(
+ "Invalid country, city and hostname combination given",
+ clap::ErrorKind::InvalidValue,
+ ).exit(),
};
self.update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate {
diff --git a/mullvad-daemon/src/relays.rs b/mullvad-daemon/src/relays.rs
index 86d4efbc51..fea9bd8340 100644
--- a/mullvad-daemon/src/relays.rs
+++ b/mullvad-daemon/src/relays.rs
@@ -70,8 +70,9 @@ impl ParsedRelays {
let city_code = city.code.clone();
let latitude = city.latitude;
let longitude = city.longitude;
- relays.extend(city.relays.drain(..).map(|mut relay| {
- relay.location = Some(Location {
+ for relay in &mut city.relays {
+ let mut relay_with_location = relay.clone();
+ relay_with_location.location = Some(Location {
country: country_name.clone(),
country_code: country_code.clone(),
city: city_name.clone(),
@@ -79,8 +80,9 @@ impl ParsedRelays {
latitude,
longitude,
});
- relay
- }));
+ relays.push(relay_with_location);
+ relay.tunnels.clear();
+ }
}
}
ParsedRelays {
@@ -259,6 +261,14 @@ impl RelaySelector {
loc.country_code == *country && loc.city_code == *city
})
}
+ Constraint::Only(LocationConstraint::Hostname(ref country, ref city, ref hostname)) => {
+ relay
+ .location
+ .as_ref()
+ .map_or(relay.hostname == *hostname, |loc| {
+ loc.country_code == *country && loc.city_code == *city
+ })
+ }
};
if !matches_location {
return None;
diff --git a/mullvad-types/src/location.rs b/mullvad-types/src/location.rs
index 6c4ed94b00..5d14318da6 100644
--- a/mullvad-types/src/location.rs
+++ b/mullvad-types/src/location.rs
@@ -2,6 +2,7 @@ use std::net::IpAddr;
pub type CountryCode = String;
pub type CityCode = String;
+pub type Hostname = String;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Location {
diff --git a/mullvad-types/src/relay_constraints.rs b/mullvad-types/src/relay_constraints.rs
index f2369ce7fa..f0a56a9d76 100644
--- a/mullvad-types/src/relay_constraints.rs
+++ b/mullvad-types/src/relay_constraints.rs
@@ -1,4 +1,4 @@
-use location::{CityCode, CountryCode};
+use location::{CityCode, CountryCode, Hostname};
use CustomTunnelEndpoint;
use std::fmt;
@@ -96,6 +96,8 @@ pub enum LocationConstraint {
Country(CountryCode),
/// A city is composed of a country code and a city code.
City(CountryCode, CityCode),
+ /// An single hostname in a given city.
+ Hostname(CountryCode, CityCode, Hostname),
}
diff --git a/mullvad-types/src/relay_list.rs b/mullvad-types/src/relay_list.rs
index 17d6e7d5f2..402b4f220b 100644
--- a/mullvad-types/src/relay_list.rs
+++ b/mullvad-types/src/relay_list.rs
@@ -44,6 +44,7 @@ pub struct Relay {
pub ipv4_addr_exit: Ipv4Addr,
pub include_in_country: bool,
pub weight: u64,
+ #[serde(skip_serializing_if = "RelayTunnels::is_empty", default)]
pub tunnels: RelayTunnels,
#[serde(skip)]
pub location: Option<Location>,
@@ -55,3 +56,14 @@ pub struct RelayTunnels {
pub openvpn: Vec<OpenVpnEndpointData>,
pub wireguard: Vec<WireguardEndpointData>,
}
+
+impl RelayTunnels {
+ pub fn is_empty(&self) -> bool {
+ self.openvpn.is_empty() && self.wireguard.is_empty()
+ }
+
+ pub fn clear(&mut self) {
+ self.openvpn.clear();
+ self.wireguard.clear();
+ }
+}