summaryrefslogtreecommitdiffhomepage
path: root/gui
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2023-10-04 11:35:59 +0200
committerOskar Nyberg <oskar@mullvad.net>2023-10-09 10:16:53 +0200
commit469c501f736e98ea6a20f1e76b40550d6ad995cd (patch)
tree88c0fbab003216eff6bcaf2fb87c02d634025558 /gui
parent4e26e4c36345afbca25a1a1e760927cd74d2c1a5 (diff)
downloadmullvadvpn-469c501f736e98ea6a20f1e76b40550d6ad995cd.tar.xz
mullvadvpn-469c501f736e98ea6a20f1e76b40550d6ad995cd.zip
Add custom lists to settings, ipc and rpc calls
Diffstat (limited to 'gui')
-rw-r--r--gui/src/main/daemon-rpc.ts169
-rw-r--r--gui/src/main/default-settings.ts1
-rw-r--r--gui/src/main/index.ts10
-rw-r--r--gui/src/main/settings.ts3
-rw-r--r--gui/src/renderer/app.tsx36
-rw-r--r--gui/src/renderer/components/Filter.tsx4
-rw-r--r--gui/src/renderer/components/select-location/select-location-helpers.ts42
-rw-r--r--gui/src/renderer/redux/settings/actions.ts25
-rw-r--r--gui/src/renderer/redux/settings/reducers.ts13
-rw-r--r--gui/src/shared/daemon-rpc-types.ts87
-rw-r--r--gui/src/shared/ipc-schema.ts7
-rw-r--r--gui/src/shared/relay-location-builder.ts4
-rw-r--r--gui/test/unit/relay-settings-builder.spec.ts6
13 files changed, 301 insertions, 106 deletions
diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts
index fbaa2fe9d9..608bf82241 100644
--- a/gui/src/main/daemon-rpc.ts
+++ b/gui/src/main/daemon-rpc.ts
@@ -17,6 +17,8 @@ import {
BridgeState,
ConnectionConfig,
Constraint,
+ CustomListError,
+ CustomLists,
DaemonEvent,
DeviceEvent,
DeviceState,
@@ -27,6 +29,7 @@ import {
FirewallPolicyErrorType,
IAppVersionInfo,
IBridgeConstraints,
+ ICustomList,
IDevice,
IDeviceRemoval,
IDnsOptions,
@@ -52,6 +55,7 @@ import {
ProxyType,
RelayEndpointType,
RelayLocation,
+ RelayLocationGeographical,
RelayProtocol,
RelaySettings,
RelaySettingsUpdate,
@@ -611,6 +615,39 @@ export class DaemonRpc {
await this.call<grpcTypes.DeviceRemoval, Empty>(this.client.removeDevice, grpcDeviceRemoval);
}
+ public async createCustomList(name: string): Promise<void | CustomListError> {
+ try {
+ await this.callString<Empty>(this.client.createCustomList, name);
+ } catch (e) {
+ const error = e as grpc.ServiceError;
+ if (error.code === 6) {
+ return { type: 'name already exists' };
+ } else {
+ throw error;
+ }
+ }
+ }
+
+ public async deleteCustomList(id: string): Promise<void> {
+ await this.callString<Empty>(this.client.deleteCustomList, id);
+ }
+
+ public async updateCustomList(customList: ICustomList): Promise<void | CustomListError> {
+ try {
+ await this.call<grpcTypes.CustomList, Empty>(
+ this.client.updateCustomList,
+ convertToCustomList(customList),
+ );
+ } catch (e) {
+ const error = e as grpc.ServiceError;
+ if (error.code === 6) {
+ return { type: 'name already exists' };
+ } else {
+ throw error;
+ }
+ }
+ }
+
private subscriptionId(): number {
const current = this.nextSubscriptionId;
this.nextSubscriptionId += 1;
@@ -1062,10 +1099,11 @@ function convertFromSettings(settings: grpcTypes.Settings): ISettings | undefine
const settingsObject = settings.toObject();
const bridgeState = convertFromBridgeState(settingsObject.bridgeState!.state!);
const relaySettings = convertFromRelaySettings(settings.getRelaySettings())!;
- const bridgeSettings = convertFromBridgeSettings(settingsObject.bridgeSettings!);
+ const bridgeSettings = convertFromBridgeSettings(settings.getBridgeSettings()!);
const tunnelOptions = convertFromTunnelOptions(settingsObject.tunnelOptions!);
const splitTunnel = settingsObject.splitTunnel ?? { enableExclusions: false, appsList: [] };
const obfuscationSettings = convertFromObfuscationSettings(settingsObject.obfuscationSettings);
+ const customLists = convertFromCustomListSettings(settings.getCustomLists());
return {
...settings.toObject(),
bridgeState,
@@ -1074,6 +1112,7 @@ function convertFromSettings(settings: grpcTypes.Settings): ISettings | undefine
tunnelOptions,
splitTunnel,
obfuscationSettings,
+ customLists,
};
}
@@ -1110,10 +1149,8 @@ function convertFromRelaySettings(
}
case grpcTypes.RelaySettings.EndpointCase.NORMAL: {
const normal = relaySettings.getNormal()!;
- const grpcLocation = normal.getLocation();
- const location = grpcLocation
- ? { only: convertFromLocation(grpcLocation.toObject()) }
- : 'any';
+ const locationConstraint = convertFromLocationConstraint(normal.getLocation());
+ const location = locationConstraint ? { only: locationConstraint } : 'any';
const tunnelProtocol = convertFromTunnelTypeConstraint(normal.getTunnelType()!);
const providers = normal.getProvidersList();
const ownership = convertFromOwnership(normal.getOwnership());
@@ -1139,13 +1176,14 @@ function convertFromRelaySettings(
}
}
-function convertFromBridgeSettings(
- bridgeSettings: grpcTypes.BridgeSettings.AsObject,
-): BridgeSettings {
- const normalSettings = bridgeSettings.normal;
+function convertFromBridgeSettings(bridgeSettings: grpcTypes.BridgeSettings): BridgeSettings {
+ const bridgeSettingsObject = bridgeSettings.toObject();
+ const normalSettings = bridgeSettingsObject.normal;
if (normalSettings) {
- const grpcLocation = normalSettings.location;
- const location = grpcLocation ? { only: convertFromLocation(grpcLocation) } : 'any';
+ const locationConstraint = convertFromLocationConstraint(
+ bridgeSettings.getNormal()?.getLocation(),
+ );
+ const location = locationConstraint ? { only: locationConstraint } : 'any';
const providers = normalSettings.providersList;
const ownership = convertFromOwnership(normalSettings.ownership);
return {
@@ -1161,7 +1199,7 @@ function convertFromBridgeSettings(
return { custom: settings };
};
- const localSettings = bridgeSettings.local;
+ const localSettings = bridgeSettingsObject.local;
if (localSettings) {
return customSettings({
port: localSettings.port,
@@ -1169,7 +1207,7 @@ function convertFromBridgeSettings(
});
}
- const remoteSettings = bridgeSettings.remote;
+ const remoteSettings = bridgeSettingsObject.remote;
if (remoteSettings) {
return customSettings({
address: remoteSettings.address,
@@ -1177,7 +1215,7 @@ function convertFromBridgeSettings(
});
}
- const shadowsocksSettings = bridgeSettings.shadowsocks!;
+ const shadowsocksSettings = bridgeSettingsObject.shadowsocks!;
return customSettings({
peer: shadowsocksSettings.peer!,
password: shadowsocksSettings.password!,
@@ -1229,23 +1267,32 @@ function convertFromConnectionConfig(
}
}
-function convertFromLocation(location: grpcTypes.LocationConstraint.AsObject): RelayLocation {
- // FIXME: This is a hack that assumes that the LocationConstraint is not a custom list.
- // If it is we just set the country to "any" even if that isn't correct.
- if (location.location == undefined) {
- return { country: 'any' };
- }
- const loc = location.location;
-
- if (loc.hostname) {
- return { hostname: [loc.country, loc.city, loc.hostname] };
+function convertFromLocationConstraint(
+ location?: grpcTypes.LocationConstraint,
+): RelayLocation | undefined {
+ if (location === undefined) {
+ return undefined;
+ } else if (location.getTypeCase() === grpcTypes.LocationConstraint.TypeCase.CUSTOM_LIST) {
+ return { customList: location.getCustomList() };
+ } else {
+ const innerLocation = location.getLocation()?.toObject();
+ return innerLocation && convertFromRelayLocation(innerLocation);
}
+}
- if (loc.city) {
- return { city: [loc.country, loc.city] };
+function convertFromRelayLocation(location: grpcTypes.RelayLocation.AsObject): RelayLocation {
+ if (location.hostname) {
+ return location;
+ } else if (location.city) {
+ return {
+ country: location.country,
+ city: location.city,
+ };
+ } else {
+ return {
+ country: location.country,
+ };
}
-
- return { country: loc.country };
}
function convertFromTunnelOptions(tunnelOptions: grpcTypes.TunnelOptions.AsObject): ITunnelOptions {
@@ -1423,7 +1470,8 @@ function convertFromWireguardConstraints(
const entryLocation = constraints.getEntryLocation();
if (entryLocation) {
- result.entryLocation = { only: convertFromLocation(entryLocation.toObject()) };
+ const location = convertFromLocationConstraint(entryLocation);
+ result.entryLocation = location ? { only: location } : 'any';
}
return result;
@@ -1467,24 +1515,32 @@ function convertToLocation(
constraint: RelayLocation | undefined,
): grpcTypes.LocationConstraint | undefined {
const locationConstraint = new grpcTypes.LocationConstraint();
- const location = new grpcTypes.RelayLocation();
- if (constraint && 'hostname' in constraint) {
- const [countryCode, cityCode, hostname] = constraint.hostname;
- location.setCountry(countryCode);
- location.setCity(cityCode);
- location.setHostname(hostname);
- } else if (constraint && 'city' in constraint) {
- location.setCountry(constraint.city[0]);
- location.setCity(constraint.city[1]);
- } else if (constraint && 'country' in constraint) {
- location.setCountry(constraint.country);
+ if (constraint && 'customList' in constraint && constraint.customList) {
+ locationConstraint.setCustomList(constraint.customList);
} else {
- return undefined;
+ const location = constraint && convertToRelayLocation(constraint);
+ locationConstraint.setLocation(location);
}
- locationConstraint.setLocation(location);
+
return locationConstraint;
}
+function convertToRelayLocation(location: RelayLocation): grpcTypes.RelayLocation {
+ const relayLocation = new grpcTypes.RelayLocation();
+ if ('hostname' in location) {
+ relayLocation.setCountry(location.country);
+ relayLocation.setCity(location.city);
+ relayLocation.setHostname(location.hostname);
+ } else if ('city' in location) {
+ relayLocation.setCountry(location.country);
+ relayLocation.setCity(location.city);
+ } else if ('country' in location) {
+ relayLocation.setCountry(location.country);
+ }
+
+ return relayLocation;
+}
+
function convertToTunnelTypeConstraint(
constraint: Constraint<TunnelType>,
): grpcTypes.TunnelTypeConstraint | undefined {
@@ -1618,6 +1674,35 @@ function convertFromDevice(device: grpcTypes.Device): IDevice {
};
}
+function convertFromCustomListSettings(
+ customListSettings?: grpcTypes.CustomListSettings,
+): CustomLists {
+ return customListSettings ? convertFromCustomLists(customListSettings.getCustomListsList()) : [];
+}
+
+function convertFromCustomLists(customLists: Array<grpcTypes.CustomList>): CustomLists {
+ return customLists.map((list) => ({
+ id: list.getId(),
+ name: list.getName(),
+ locations: list
+ .getLocationsList()
+ .map((location) =>
+ convertFromRelayLocation(location.toObject()),
+ ) as Array<RelayLocationGeographical>,
+ }));
+}
+
+function convertToCustomList(customList: ICustomList): grpcTypes.CustomList {
+ const grpcCustomList = new grpcTypes.CustomList();
+ grpcCustomList.setId(customList.id);
+ grpcCustomList.setName(customList.name);
+
+ const locations = customList.locations.map(convertToRelayLocation);
+ grpcCustomList.setLocationsList(locations);
+
+ return grpcCustomList;
+}
+
function ensureExists<T>(value: T | undefined, errorMessage: string): T {
if (value) {
return value;
diff --git a/gui/src/main/default-settings.ts b/gui/src/main/default-settings.ts
index 40c75042f8..55f420b659 100644
--- a/gui/src/main/default-settings.ts
+++ b/gui/src/main/default-settings.ts
@@ -68,5 +68,6 @@ export function getDefaultSettings(): ISettings {
port: 'any',
},
},
+ customLists: [],
};
}
diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts
index 484f4db4b3..c674ca22d8 100644
--- a/gui/src/main/index.ts
+++ b/gui/src/main/index.ts
@@ -787,6 +787,16 @@ class ApplicationMain
this.navigationHistory = history;
});
+ IpcMainEventChannel.customLists.handleCreateCustomList((name) => {
+ return this.daemonRpc.createCustomList(name);
+ });
+ IpcMainEventChannel.customLists.handleDeleteCustomList((id) => {
+ return this.daemonRpc.deleteCustomList(id);
+ });
+ IpcMainEventChannel.customLists.handleUpdateCustomList((customList) => {
+ return this.daemonRpc.updateCustomList(customList);
+ });
+
problemReport.registerIpcListeners();
this.userInterface!.registerIpcListeners();
this.settings.registerIpcListeners();
diff --git a/gui/src/main/settings.ts b/gui/src/main/settings.ts
index 08871f42ea..3016e75289 100644
--- a/gui/src/main/settings.ts
+++ b/gui/src/main/settings.ts
@@ -132,6 +132,9 @@ export default class Settings implements Readonly<ISettings> {
public get obfuscationSettings() {
return this.settingsValue.obfuscationSettings;
}
+ public get customLists() {
+ return this.settingsValue.customLists;
+ }
public get gui() {
return this.guiSettings;
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index 155922bc4d..7c7df73796 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -12,6 +12,7 @@ import {
DeviceState,
IAccountData,
IAppVersionInfo,
+ ICustomList,
IDevice,
IDeviceRemoval,
IDnsOptions,
@@ -336,6 +337,12 @@ export default class AppRenderer {
public openUrl = (url: string) => IpcRendererEventChannel.app.openUrl(url);
public showOpenDialog = (options: Electron.OpenDialogOptions) =>
IpcRendererEventChannel.app.showOpenDialog(options);
+ public createCustomList = (name: string) =>
+ IpcRendererEventChannel.customLists.createCustomList(name);
+ public deleteCustomList = (id: string) =>
+ IpcRendererEventChannel.customLists.deleteCustomList(id);
+ public updateCustomList = (customList: ICustomList) =>
+ IpcRendererEventChannel.customLists.updateCustomList(customList);
public login = async (accountToken: AccountToken) => {
const actions = this.reduxActions;
@@ -778,6 +785,7 @@ export default class AppRenderer {
reduxSettings.updateDnsOptions(newSettings.tunnelOptions.dns);
reduxSettings.updateSplitTunnelingState(newSettings.splitTunnel.enableExclusions);
reduxSettings.updateObfuscationSettings(newSettings.obfuscationSettings);
+ reduxSettings.updateCustomLists(newSettings.customLists);
this.setRelaySettings(newSettings.relaySettings);
this.setBridgeSettings(newSettings.bridgeSettings);
@@ -1002,20 +1010,11 @@ export default class AppRenderer {
const location = relaySettings.normal.location;
if (location !== 'any' && 'only' in location) {
const constraint = location.only;
-
const relayLocations = state.settings.relayLocations;
- if ('country' in constraint) {
- const country = relayLocations.find(({ code }) => constraint.country === code);
- return { country: country?.name, ...coordinates };
- } else if ('city' in constraint) {
- const country = relayLocations.find(({ code }) => constraint.city[0] === code);
- const city = country?.cities.find(({ code }) => constraint.city[1] === code);
-
- return { country: country?.name, city: city?.name, ...coordinates };
- } else if ('hostname' in constraint) {
- const country = relayLocations.find(({ code }) => constraint.hostname[0] === code);
- const city = country?.cities.find((location) => location.code === constraint.hostname[1]);
+ if ('hostname' in constraint) {
+ const country = relayLocations.find(({ code }) => constraint.country === code);
+ const city = country?.cities.find(({ code }) => constraint.city === code);
let entryHostname: string | undefined;
const multihopConstraint = relaySettings.normal.wireguardConstraints.useMultihop;
@@ -1026,16 +1025,25 @@ export default class AppRenderer {
'hostname' in entryLocationConstraint.only &&
entryLocationConstraint.only.hostname.length === 3
) {
- entryHostname = entryLocationConstraint.only.hostname[2];
+ entryHostname = entryLocationConstraint.only.hostname;
}
return {
country: country?.name,
city: city?.name,
- hostname: constraint.hostname[2],
+ hostname: constraint.hostname,
entryHostname,
...coordinates,
};
+ } else if ('city' in constraint) {
+ const country = relayLocations.find(({ code }) => constraint.country === code);
+ const city = country?.cities.find(({ code }) => constraint.city === code);
+
+ return { country: country?.name, city: city?.name, ...coordinates };
+ } else if ('country' in constraint) {
+ const country = relayLocations.find(({ code }) => constraint.country === code);
+
+ return { country: country?.name, ...coordinates };
}
}
}
diff --git a/gui/src/renderer/components/Filter.tsx b/gui/src/renderer/components/Filter.tsx
index 26ed781396..6b369aa39b 100644
--- a/gui/src/renderer/components/Filter.tsx
+++ b/gui/src/renderer/components/Filter.tsx
@@ -12,7 +12,7 @@ import {
} from '../lib/filter-locations';
import { useHistory } from '../lib/history';
import { useBoolean, useNormalRelaySettings } from '../lib/utilityHooks';
-import { IRelayLocationRedux } from '../redux/settings/reducers';
+import { IRelayLocationCountryRedux } from '../redux/settings/reducers';
import { IReduxState, useSelector } from '../redux/store';
import Accordion from './Accordion';
import * as AppButton from './AppButton';
@@ -158,7 +158,7 @@ function useFilteredFilters(providers: string[], ownership: Ownership) {
}
// Returns all available providers in the provided relay list.
-function providersFromRelays(relays: IRelayLocationRedux[]) {
+function providersFromRelays(relays: IRelayLocationCountryRedux[]) {
const providers = relays.flatMap((country) =>
country.cities.flatMap((city) => city.relays.map((relay) => relay.provider)),
);
diff --git a/gui/src/renderer/components/select-location/select-location-helpers.ts b/gui/src/renderer/components/select-location/select-location-helpers.ts
index 46225645cc..23d059ac0a 100644
--- a/gui/src/renderer/components/select-location/select-location-helpers.ts
+++ b/gui/src/renderer/components/select-location/select-location-helpers.ts
@@ -5,16 +5,20 @@ import {
compareRelayLocationLoose,
LiftedConstraint,
RelayLocation,
+ RelayLocationCity,
+ RelayLocationCountry,
+ RelayLocationCustomList,
+ RelayLocationRelay,
} from '../../../shared/daemon-rpc-types';
import { messages, relayLocations } from '../../../shared/gettext';
import {
IRelayLocationCityRedux,
- IRelayLocationRedux,
+ IRelayLocationCountryRedux,
IRelayLocationRelayRedux,
NormalBridgeSettingsRedux,
NormalRelaySettingsRedux,
} from '../../redux/settings/reducers';
-import { DisabledReason, LocationType } from './select-location-types';
+import { DisabledReason, LocationSpecification, LocationType } from './select-location-types';
export function isSelected(
relayLocation: RelayLocation,
@@ -58,13 +62,10 @@ export function defaultExpandedLocations(
// Expands a relay location and its parents
function expandRelayLocation(location: RelayLocation): RelayLocation[] {
- if ('city' in location) {
- return [{ country: location.city[0] }];
- } else if ('hostname' in location) {
- return [
- { country: location.hostname[0] },
- { city: [location.hostname[0], location.hostname[1]] },
- ];
+ if ('hostname' in location) {
+ return [{ country: location.country }, { country: location.country, city: location.city }];
+ } else if ('city' in location) {
+ return [{ country: location.country }];
} else {
return [];
}
@@ -104,15 +105,12 @@ export function formatRowName(
export function isRelayDisabled(
relay: IRelayLocationRelayRedux,
- location: [string, string, string],
+ location: RelayLocationRelay,
disabledLocation?: { location: RelayLocation; reason: DisabledReason },
): DisabledReason | undefined {
if (!relay.active) {
return DisabledReason.inactive;
- } else if (
- disabledLocation &&
- compareRelayLocation({ hostname: location }, disabledLocation.location)
- ) {
+ } else if (disabledLocation && compareRelayLocation(location, disabledLocation.location)) {
return disabledLocation.reason;
} else {
return undefined;
@@ -121,11 +119,11 @@ export function isRelayDisabled(
export function isCityDisabled(
city: IRelayLocationCityRedux,
- location: [string, string],
+ location: RelayLocationCity,
disabledLocation?: { location: RelayLocation; reason: DisabledReason },
): DisabledReason | undefined {
const relaysDisabled = city.relays.map((relay) =>
- isRelayDisabled(relay, [...location, relay.hostname]),
+ isRelayDisabled(relay, { ...location, hostname: relay.hostname }),
);
if (relaysDisabled.every((status) => status === DisabledReason.inactive)) {
return DisabledReason.inactive;
@@ -144,7 +142,7 @@ export function isCityDisabled(
if (
disabledLocation &&
- compareRelayLocation({ city: location }, disabledLocation.location) &&
+ compareRelayLocation(location, disabledLocation.location) &&
city.relays.filter((relay) => relay.active).length <= 1
) {
return disabledLocation.reason;
@@ -154,11 +152,13 @@ export function isCityDisabled(
}
export function isCountryDisabled(
- country: IRelayLocationRedux,
- location: string,
+ country: IRelayLocationCountryRedux,
+ location: RelayLocationCountry,
disabledLocation?: { location: RelayLocation; reason: DisabledReason },
): DisabledReason | undefined {
- const citiesDisabled = country.cities.map((city) => isCityDisabled(city, [location, city.code]));
+ const citiesDisabled = country.cities.map((city) =>
+ isCityDisabled(city, { ...location, city: city.code }),
+ );
if (citiesDisabled.every((status) => status === DisabledReason.inactive)) {
return DisabledReason.inactive;
}
@@ -175,7 +175,7 @@ export function isCountryDisabled(
if (
disabledLocation &&
- compareRelayLocation({ country: location }, disabledLocation.location) &&
+ compareRelayLocation(location, disabledLocation.location) &&
country.cities.flatMap((city) => city.relays).filter((relay) => relay.active).length <= 1
) {
return disabledLocation.reason;
diff --git a/gui/src/renderer/redux/settings/actions.ts b/gui/src/renderer/redux/settings/actions.ts
index dad71de024..585aad5732 100644
--- a/gui/src/renderer/redux/settings/actions.ts
+++ b/gui/src/renderer/redux/settings/actions.ts
@@ -1,12 +1,13 @@
import { IWindowsApplication } from '../../../shared/application-types';
import {
BridgeState,
+ CustomLists,
IDnsOptions,
IWireguardEndpointData,
ObfuscationSettings,
} from '../../../shared/daemon-rpc-types';
import { IGuiSettingsState } from '../../../shared/gui-settings-state';
-import { BridgeSettingsRedux, IRelayLocationRedux, RelaySettingsRedux } from './reducers';
+import { BridgeSettingsRedux, IRelayLocationCountryRedux, RelaySettingsRedux } from './reducers';
export interface IUpdateGuiSettingsAction {
type: 'UPDATE_GUI_SETTINGS';
@@ -20,7 +21,7 @@ export interface IUpdateRelayAction {
export interface IUpdateRelayLocationsAction {
type: 'UPDATE_RELAY_LOCATIONS';
- relayLocations: IRelayLocationRedux[];
+ relayLocations: IRelayLocationCountryRedux[];
}
export interface IUpdateWireguardEndpointData {
@@ -98,6 +99,11 @@ export interface ISetObfuscationSettings {
obfuscationSettings: ObfuscationSettings;
}
+export interface ISetCustomLists {
+ type: 'SET_CUSTOM_LISTS';
+ customLists: CustomLists;
+}
+
export type SettingsAction =
| IUpdateGuiSettingsAction
| IUpdateRelayAction
@@ -116,7 +122,8 @@ export type SettingsAction =
| IUpdateDnsOptionsAction
| IUpdateSplitTunnelingStateAction
| ISetSplitTunnelingApplicationsAction
- | ISetObfuscationSettings;
+ | ISetObfuscationSettings
+ | ISetCustomLists;
function updateGuiSettings(guiSettings: IGuiSettingsState): IUpdateGuiSettingsAction {
return {
@@ -132,7 +139,9 @@ function updateRelay(relay: RelaySettingsRedux): IUpdateRelayAction {
};
}
-function updateRelayLocations(relayLocations: IRelayLocationRedux[]): IUpdateRelayLocationsAction {
+function updateRelayLocations(
+ relayLocations: IRelayLocationCountryRedux[],
+): IUpdateRelayLocationsAction {
return {
type: 'UPDATE_RELAY_LOCATIONS',
relayLocations,
@@ -254,6 +263,13 @@ function updateObfuscationSettings(
};
}
+function updateCustomLists(customLists: CustomLists): ISetCustomLists {
+ return {
+ type: 'SET_CUSTOM_LISTS',
+ customLists,
+ };
+}
+
export default {
updateGuiSettings,
updateRelay,
@@ -273,4 +289,5 @@ export default {
updateSplitTunnelingState,
setSplitTunnelingApplications,
updateObfuscationSettings,
+ updateCustomLists,
};
diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts
index 2030d70844..b400799095 100644
--- a/gui/src/renderer/redux/settings/reducers.ts
+++ b/gui/src/renderer/redux/settings/reducers.ts
@@ -1,6 +1,7 @@
import { IWindowsApplication } from '../../../shared/application-types';
import {
BridgeState,
+ CustomLists,
IDnsOptions,
IpVersion,
IWireguardEndpointData,
@@ -77,7 +78,7 @@ export interface IRelayLocationCityRedux {
relays: IRelayLocationRelayRedux[];
}
-export interface IRelayLocationRedux {
+export interface IRelayLocationCountryRedux {
name: string;
code: string;
cities: IRelayLocationCityRedux[];
@@ -87,7 +88,7 @@ export interface ISettingsReduxState {
autoStart: boolean;
guiSettings: IGuiSettingsState;
relaySettings: RelaySettingsRedux;
- relayLocations: IRelayLocationRedux[];
+ relayLocations: IRelayLocationCountryRedux[];
wireguardEndpointData: IWireguardEndpointData;
allowLan: boolean;
enableIpv6: boolean;
@@ -106,6 +107,7 @@ export interface ISettingsReduxState {
splitTunneling: boolean;
splitTunnelingApplications: IWindowsApplication[];
obfuscationSettings: ObfuscationSettings;
+ customLists: CustomLists;
}
const initialState: ISettingsReduxState = {
@@ -169,6 +171,7 @@ const initialState: ISettingsReduxState = {
port: 'any',
},
},
+ customLists: [],
};
export default function (
@@ -293,6 +296,12 @@ export default function (
obfuscationSettings: action.obfuscationSettings,
};
+ case 'SET_CUSTOM_LISTS':
+ return {
+ ...state,
+ customLists: action.customLists,
+ };
+
default:
return state;
}
diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts
index 9af927070a..51c1d67c0d 100644
--- a/gui/src/shared/daemon-rpc-types.ts
+++ b/gui/src/shared/daemon-rpc-types.ts
@@ -179,10 +179,28 @@ export type TunnelState =
| { state: 'disconnecting'; details: AfterDisconnect }
| { state: 'error'; details: ErrorState };
-export type RelayLocation =
- | { hostname: [string, string, string] }
- | { city: [string, string] }
- | { country: string };
+export interface RelayLocationCountry extends Partial<RelayLocationCustomList> {
+ country: string;
+}
+
+export interface RelayLocationCity extends RelayLocationCountry {
+ city: string;
+}
+
+export interface RelayLocationRelay extends RelayLocationCity {
+ hostname: string;
+}
+
+export interface RelayLocationCustomList {
+ customList: string;
+}
+
+export type RelayLocationGeographical =
+ | RelayLocationRelay
+ | RelayLocationCountry
+ | RelayLocationCity;
+
+export type RelayLocation = RelayLocationGeographical | RelayLocationCustomList;
export interface IOpenVpnConstraints {
port: Constraint<number>;
@@ -386,6 +404,16 @@ export interface IDeviceRemoval {
deviceId: string;
}
+export type CustomLists = Array<ICustomList>;
+
+export interface ICustomList {
+ id: string;
+ name: string;
+ locations: Array<RelayLocationGeographical>;
+}
+
+export type CustomListError = { type: 'name already exists' };
+
export interface ISettings {
allowLan: boolean;
autoConnect: boolean;
@@ -397,6 +425,7 @@ export interface ISettings {
bridgeState: BridgeState;
splitTunnel: SplitTunnelSettings;
obfuscationSettings: ObfuscationSettings;
+ customLists: CustomLists;
}
export type BridgeState = 'auto' | 'on' | 'off';
@@ -452,25 +481,51 @@ export function parseSocketAddress(socketAddrStr: string): ISocketAddress {
return socketAddress;
}
-export function relayLocationComponents(location: RelayLocation): string[] {
- if ('country' in location) {
- return [location.country];
- } else if ('city' in location) {
- return location.city;
- } else {
- return location.hostname;
+export function compareRelayLocationCount(lhs: RelayLocation, rhs: RelayLocation): boolean {
+ if (
+ ('count' in lhs || 'count' in rhs) &&
+ !('count' in lhs && 'count' in rhs && lhs.count === rhs.count)
+ ) {
+ return false;
}
+
+ return compareRelayLocation(lhs, rhs);
}
export function compareRelayLocation(lhs: RelayLocation, rhs: RelayLocation): boolean {
- const lhsComponents = relayLocationComponents(lhs);
- const rhsComponents = relayLocationComponents(rhs);
+ if (
+ ('customList' in lhs || 'customList' in rhs) &&
+ !('customList' in lhs && 'customList' in rhs && lhs.customList === rhs.customList)
+ ) {
+ return false;
+ }
- if (lhsComponents.length === rhsComponents.length) {
- return lhsComponents.every((value, index) => value === rhsComponents[index]);
- } else {
+ return compareRelayLocationGeographical(lhs, rhs);
+}
+
+export function compareRelayLocationGeographical(lhs: RelayLocation, rhs: RelayLocation): boolean {
+ if (
+ ('country' in lhs || 'country' in rhs) &&
+ !('country' in lhs && 'country' in rhs && lhs.country === rhs.country)
+ ) {
+ return false;
+ }
+
+ if (
+ ('city' in lhs || 'city' in rhs) &&
+ !('city' in lhs && 'city' in rhs && lhs.city === rhs.city)
+ ) {
return false;
}
+
+ if (
+ ('hostname' in lhs || 'hostname' in rhs) &&
+ !('hostname' in lhs && 'hostname' in rhs && lhs.hostname === rhs.hostname)
+ ) {
+ return false;
+ }
+
+ return true;
}
export function compareRelayLocationLoose(lhs?: RelayLocation, rhs?: RelayLocation) {
diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts
index 265d546b5d..946b4fa99a 100644
--- a/gui/src/shared/ipc-schema.ts
+++ b/gui/src/shared/ipc-schema.ts
@@ -6,10 +6,12 @@ import {
AccountToken,
BridgeSettings,
BridgeState,
+ CustomListError,
DeviceEvent,
DeviceState,
IAccountData,
IAppVersionInfo,
+ ICustomList,
IDevice,
IDeviceRemoval,
IDnsOptions,
@@ -133,6 +135,11 @@ export const ipcSchema = {
relays: {
'': notifyRenderer<IRelayListWithEndpointData>(),
},
+ customLists: {
+ createCustomList: invoke<string, void | CustomListError>(),
+ deleteCustomList: invoke<string, void>(),
+ updateCustomList: invoke<ICustomList, void | CustomListError>(),
+ },
currentVersion: {
'': notifyRenderer<ICurrentAppVersionInfo>(),
displayedChangelog: send<void>(),
diff --git a/gui/src/shared/relay-location-builder.ts b/gui/src/shared/relay-location-builder.ts
index 14bf72b65f..7e585f9eaf 100644
--- a/gui/src/shared/relay-location-builder.ts
+++ b/gui/src/shared/relay-location-builder.ts
@@ -18,11 +18,11 @@ export default function makeLocationBuilder<T>(
return context;
},
city: (country: string, city: string) => {
- receiver({ only: { city: [country, city] } });
+ receiver({ only: { country, city } });
return context;
},
hostname: (country: string, city: string, hostname: string) => {
- receiver({ only: { hostname: [country, city, hostname] } });
+ receiver({ only: { country, city, hostname } });
return context;
},
any: () => {
diff --git a/gui/test/unit/relay-settings-builder.spec.ts b/gui/test/unit/relay-settings-builder.spec.ts
index 87a3fd5dae..eeba0828b7 100644
--- a/gui/test/unit/relay-settings-builder.spec.ts
+++ b/gui/test/unit/relay-settings-builder.spec.ts
@@ -16,7 +16,7 @@ describe('Relay settings builder', () => {
normal: {
location: {
only: {
- city: ['se', 'mma'],
+ country: 'se', city: 'mma',
},
},
},
@@ -86,12 +86,12 @@ describe('Relay settings builder', () => {
expect(
RelaySettingsBuilder.normal()
- .location.fromRaw({ city: ['se', 'mma'] })
+ .location.fromRaw({ country: 'se', city: 'mma' })
.build(),
).to.deep.equal({
normal: {
location: {
- only: { city: ['se', 'mma'] },
+ only: { country: 'se', city: 'mma' },
},
},
});