summaryrefslogtreecommitdiffhomepage
path: root/gui/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'gui/src/main')
-rw-r--r--gui/src/main/daemon-rpc.ts149
-rw-r--r--gui/src/main/errors.ts6
-rw-r--r--gui/src/main/index.ts201
3 files changed, 177 insertions, 179 deletions
diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts
index 6638799e2f..2497bceaeb 100644
--- a/gui/src/main/daemon-rpc.ts
+++ b/gui/src/main/daemon-rpc.ts
@@ -36,8 +36,6 @@ import {
TunnelType,
IProxyEndpoint,
ProxyType,
- KeygenEvent,
- IWireguardPublicKey,
ISettings,
ConnectionConfig,
DaemonEvent,
@@ -48,12 +46,16 @@ import {
VoucherResponse,
TunnelProtocol,
IDnsOptions,
+ IDeviceConfig,
+ IDevice,
+ IDeviceRemoval,
+ IDeviceEvent,
} from '../shared/daemon-rpc-types';
import log from '../shared/logging';
import { ManagementServiceClient } from './management_interface/management_interface_grpc_pb';
import * as grpcTypes from './management_interface/management_interface_pb';
-import { CommunicationError, InvalidAccountError } from './errors';
+import { CommunicationError, InvalidAccountError, TooManyDevicesError } from './errors';
const NETWORK_CALL_TIMEOUT = 10000;
const CHANNEL_STATE_TIMEOUT = 1000 * 60 * 60;
@@ -257,8 +259,24 @@ export class DaemonRpc {
return response.getValue();
}
- public async setAccount(accountToken?: AccountToken): Promise<void> {
- await this.callString(this.client.setAccount, accountToken);
+ public async loginAccount(accountToken: AccountToken): Promise<void> {
+ try {
+ await this.callString(this.client.loginAccount, accountToken);
+ } catch (e) {
+ const error = e as grpc.ServiceError;
+ switch (error.code) {
+ case grpc.status.RESOURCE_EXHAUSTED:
+ throw new TooManyDevicesError();
+ case grpc.status.UNAUTHENTICATED:
+ throw new InvalidAccountError();
+ default:
+ throw new CommunicationError();
+ }
+ }
+ }
+
+ public async logoutAccount(): Promise<void> {
+ await this.callEmpty(this.client.logoutAccount);
}
// TODO: Custom tunnel configurations are not supported by the GUI.
@@ -438,19 +456,6 @@ export class DaemonRpc {
return response.getValue();
}
- public async generateWireguardKey(): Promise<KeygenEvent> {
- const response = await this.callEmpty<grpcTypes.KeygenEvent>(this.client.generateWireguardKey);
- return convertFromKeygenEvent(response);
- }
-
- public async getWireguardKey(): Promise<IWireguardPublicKey> {
- const response = await this.callEmpty<grpcTypes.PublicKey>(this.client.getWireguardKey);
- return {
- created: response.getCreated()!.toDate().toISOString(),
- key: convertFromWireguardKey(response.getKey()),
- };
- }
-
public async setDnsOptions(dns: IDnsOptions): Promise<void> {
const dnsOptions = new grpcTypes.DnsOptions();
@@ -473,11 +478,6 @@ export class DaemonRpc {
await this.call<grpcTypes.DnsOptions, Empty>(this.client.setDnsOptions, dnsOptions);
}
- public async verifyWireguardKey(): Promise<boolean> {
- const response = await this.callEmpty<BoolValue>(this.client.verifyWireguardKey);
- return response.getValue();
- }
-
public async getVersionInfo(): Promise<IAppVersionInfo> {
const response = await this.callEmpty<grpcTypes.AppVersionInfo>(this.client.getVersionInfo);
return response.toObject();
@@ -499,6 +499,37 @@ export class DaemonRpc {
await this.callEmpty(this.client.checkVolumes);
}
+ public async getDevice(): Promise<IDeviceConfig | undefined> {
+ try {
+ const response = await this.callEmpty<grpcTypes.DeviceConfig>(this.client.getDevice);
+ return convertFromDeviceConfig(response);
+ } catch (e) {
+ const error = e as grpc.ServiceError;
+ if (error.code === grpc.status.NOT_FOUND) {
+ return undefined;
+ } else {
+ throw error;
+ }
+ }
+ }
+
+ public async listDevices(accountToken: AccountToken): Promise<Array<IDevice>> {
+ const response = await this.callString<grpcTypes.DeviceList>(
+ this.client.listDevices,
+ accountToken,
+ );
+
+ return response.getDevicesList().map(convertFromDevice);
+ }
+
+ public async removeDevice(deviceRemoval: IDeviceRemoval): Promise<void> {
+ const grpcDeviceRemoval = new grpcTypes.DeviceRemoval();
+ grpcDeviceRemoval.setAccountToken(deviceRemoval.accountToken);
+ grpcDeviceRemoval.setDeviceId(deviceRemoval.deviceId);
+
+ await this.call<grpcTypes.DeviceRemoval, Empty>(this.client.removeDevice, grpcDeviceRemoval);
+ }
+
private subscriptionId(): number {
const current = this.nextSubscriptionId;
this.nextSubscriptionId += 1;
@@ -1123,36 +1154,26 @@ function convertFromDaemonEvent(data: grpcTypes.DaemonEvent): DaemonEvent {
};
}
- const keygenEvent = data.getKeyEvent();
- if (keygenEvent !== undefined) {
- return {
- wireguardKey: convertFromKeygenEvent(keygenEvent),
- };
+ const deviceConfig = data.getDevice();
+ if (deviceConfig !== undefined) {
+ return { device: convertFromDeviceEvent(deviceConfig) };
}
- return {
- appVersionInfo: data.getVersionInfo()!.toObject(),
- };
-}
+ const deviceRemoval = data.getRemoveDevice();
+ if (deviceRemoval !== undefined) {
+ return { deviceRemoval: convertFromDeviceRemoval(deviceRemoval) };
+ }
-function convertFromKeygenEvent(data: grpcTypes.KeygenEvent): KeygenEvent {
- switch (data.getEvent()) {
- case grpcTypes.KeygenEvent.KeygenEvent.TOO_MANY_KEYS:
- return 'too_many_keys';
- case grpcTypes.KeygenEvent.KeygenEvent.NEW_KEY: {
- const newKey = data.getNewKey();
- return newKey
- ? {
- newKey: {
- created: newKey.getCreated()!.toDate().toISOString(),
- key: convertFromWireguardKey(newKey.getKey()),
- },
- }
- : 'generation_failure';
- }
- case grpcTypes.KeygenEvent.KeygenEvent.GENERATION_FAILURE:
- return 'generation_failure';
+ const versionInfo = data.getVersionInfo();
+ if (versionInfo !== undefined) {
+ return { appVersionInfo: versionInfo.toObject() };
}
+
+ // Handle unknown daemon events
+ const keys = Object.entries(data.toObject())
+ .filter(([, value]) => value !== undefined)
+ .map(([key]) => key);
+ throw new Error(`Unknown daemon event received containing ${keys}`);
}
function convertFromOpenVpnConstraints(
@@ -1350,6 +1371,36 @@ function convertToTransportProtocol(protocol: RelayProtocol): grpcTypes.Transpor
}
}
+function convertFromDeviceEvent(deviceEvent: grpcTypes.DeviceEvent): IDeviceEvent {
+ return {
+ deviceConfig: convertFromDeviceConfig(deviceEvent.getDevice()),
+ remote: deviceEvent.getRemote(),
+ };
+}
+
+function convertFromDeviceConfig(deviceConfig?: grpcTypes.DeviceConfig): IDeviceConfig | undefined {
+ const device = deviceConfig?.getDevice();
+ return (
+ deviceConfig && {
+ accountToken: deviceConfig.getAccountToken(),
+ device: device ? convertFromDevice(device) : undefined,
+ }
+ );
+}
+
+function convertFromDeviceRemoval(deviceRemoval: grpcTypes.RemoveDeviceEvent): Array<IDevice> {
+ return deviceRemoval.getNewDeviceListList().map(convertFromDevice);
+}
+
+function convertFromDevice(device: grpcTypes.Device): IDevice {
+ const asObject = device.toObject();
+
+ return {
+ ...asObject,
+ ports: asObject.portsList.map((port) => port.id),
+ };
+}
+
function ensureExists<T>(value: T | undefined, errorMessage: string): T {
if (value) {
return value;
diff --git a/gui/src/main/errors.ts b/gui/src/main/errors.ts
index 261ee7a164..b7cc82c365 100644
--- a/gui/src/main/errors.ts
+++ b/gui/src/main/errors.ts
@@ -15,3 +15,9 @@ export class CommunicationError extends Error {
super('api.mullvad.net is blocked, please check your firewall');
}
}
+
+export class TooManyDevicesError extends Error {
+ constructor() {
+ super('Too many devices');
+ }
+}
diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts
index 8cebf9511b..c53456cc65 100644
--- a/gui/src/main/index.ts
+++ b/gui/src/main/index.ts
@@ -27,15 +27,16 @@ import {
DaemonEvent,
IAccountData,
IAppVersionInfo,
+ IDeviceConfig,
+ IDeviceRemoval,
IDnsOptions,
IRelayList,
ISettings,
- IWireguardPublicKey,
- KeygenEvent,
liftConstraint,
RelaySettings,
RelaySettingsUpdate,
TunnelState,
+ IDeviceEvent,
} from '../shared/daemon-rpc-types';
import { messages, relayLocations } from '../shared/gettext';
import { SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state';
@@ -88,8 +89,6 @@ const windowsSplitTunneling = process.platform === 'win32' && require('./windows
const DAEMON_RPC_PATH =
process.platform === 'win32' ? 'unix:////./pipe/Mullvad VPN' : 'unix:///var/run/mullvad-vpn';
-const AUTO_CONNECT_FALLBACK_DELAY = 6000;
-
const GUI_VERSION = app.getVersion().replace('.0', '');
/// Mirrors the beta check regex in the daemon. Matches only well formed beta versions
const IS_BETA = /^(\d{4})\.(\d+)-beta(\d+)$/;
@@ -108,8 +107,6 @@ enum AppQuitStage {
ready,
}
-type AccountVerification = { status: 'verified' } | { status: 'deferred'; error: Error };
-
class ApplicationMain {
private notificationController = new NotificationController({
openApp: () => this.windowController?.show(),
@@ -145,7 +142,6 @@ class ApplicationMain {
private tunnelStateFallbackScheduler = new Scheduler();
private settings: ISettings = {
- accountToken: undefined,
allowLan: false,
autoConnect: false,
blockWhenDisconnected: false,
@@ -201,6 +197,7 @@ class ApplicationMain {
},
},
};
+ private deviceConfig?: IDeviceConfig;
private guiSettings = new GuiSettings();
private tunnelStateExpectation?: Expectation;
@@ -221,8 +218,6 @@ class ApplicationMain {
// The UI locale which is set once from onReady handler
private locale = 'en';
- private wireguardPublicKey?: IWireguardPublicKey;
-
private accountExpiryNotificationScheduler = new Scheduler();
private accountDataCache = new AccountDataCache(
@@ -240,9 +235,6 @@ class ApplicationMain {
},
);
- private autoConnectOnWireguardKeyEvent = false;
- private autoConnectFallbackScheduler = new Scheduler();
-
private blurNavigationResetScheduler = new Scheduler();
private backgroundThrottleScheduler = new Scheduler();
@@ -648,6 +640,16 @@ class ApplicationMain {
return this.handleBootstrapError(error);
}
+ // fetch device
+ try {
+ this.setDeviceConfig({ deviceConfig: await this.daemonRpc.getDevice() });
+ } catch (e) {
+ const error = e as Error;
+ log.error(`Failed to fetch device: ${error.message}`);
+
+ return this.handleBootstrapError(error);
+ }
+
// fetch settings
try {
this.setSettings(await this.daemonRpc.getSettings());
@@ -705,7 +707,7 @@ class ApplicationMain {
}
// show window when account is not set
- if (!this.settings.accountToken) {
+ if (!this.deviceConfig) {
this.windowController?.show();
}
};
@@ -722,7 +724,6 @@ class ApplicationMain {
this.daemonEventListener = undefined;
this.tunnelStateFallback = undefined;
- this.autoConnectFallbackScheduler.cancel();
if (wasConnected) {
this.connectedToDaemon = false;
@@ -775,15 +776,21 @@ class ApplicationMain {
this.settings.relaySettings,
this.settings.bridgeState,
);
- } else if ('wireguardKey' in daemonEvent) {
- this.handleWireguardKeygenEvent(daemonEvent.wireguardKey);
} else if ('appVersionInfo' in daemonEvent) {
this.setLatestVersion(daemonEvent.appVersionInfo);
+ } else if ('device' in daemonEvent) {
+ this.setDeviceConfig(daemonEvent.device);
+ } else if ('deviceRemoval' in daemonEvent) {
+ if (this.windowController) {
+ IpcMainEventChannel.account.notifyDevices(
+ this.windowController.webContents,
+ daemonEvent.deviceRemoval,
+ );
+ }
}
},
(error: Error) => {
log.error(`Cannot deserialize the daemon event: ${error.message}`);
- log.error(error.stack);
},
);
@@ -794,7 +801,11 @@ class ApplicationMain {
private connectTunnel = async (): Promise<void> => {
if (
- connectEnabled(this.connectedToDaemon, this.settings.accountToken, this.tunnelState.state)
+ connectEnabled(
+ this.connectedToDaemon,
+ this.deviceConfig?.accountToken,
+ this.tunnelState.state,
+ )
) {
this.setOptimisticTunnelState('connecting');
await this.daemonRpc.connectTunnel();
@@ -803,7 +814,11 @@ class ApplicationMain {
private reconnectTunnel = async (): Promise<void> => {
if (
- reconnectEnabled(this.connectedToDaemon, this.settings.accountToken, this.tunnelState.state)
+ reconnectEnabled(
+ this.connectedToDaemon,
+ this.deviceConfig?.accountToken,
+ this.tunnelState.state,
+ )
) {
this.setOptimisticTunnelState('connecting');
await this.daemonRpc.reconnectTunnel();
@@ -825,37 +840,6 @@ class ApplicationMain {
}
}
- private setWireguardKey(wireguardKey?: IWireguardPublicKey) {
- this.wireguardPublicKey = wireguardKey;
- if (this.windowController) {
- IpcMainEventChannel.wireguardKeys.notifyPublicKey(
- this.windowController.webContents,
- wireguardKey,
- );
- }
-
- if (wireguardKey) {
- this.wireguardKeygenEventAutoConnect();
- }
- }
-
- private handleWireguardKeygenEvent(event: KeygenEvent) {
- switch (event) {
- case 'too_many_keys':
- case 'generation_failure':
- this.wireguardPublicKey = undefined;
- break;
- default:
- this.wireguardPublicKey = event.newKey;
- }
-
- if (this.windowController) {
- IpcMainEventChannel.wireguardKeys.notifyKeygenEvent(this.windowController.webContents, event);
- }
-
- this.wireguardKeygenEventAutoConnect();
- }
-
// This function sets a new tunnel state as an assumed next state and saves the current state as
// fallback. The fallback is used if the assumed next state isn't reached.
private setOptimisticTunnelState(state: 'connecting' | 'disconnecting') {
@@ -924,14 +908,6 @@ class ApplicationMain {
this.updateTrayIcon(this.tunnelState, newSettings.blockWhenDisconnected);
- // make sure to invalidate the account data cache when account tokens change
- this.updateAccountDataOnAccountChange(oldSettings.accountToken, newSettings.accountToken);
-
- if (oldSettings.accountToken !== newSettings.accountToken) {
- void this.updateAccountHistory();
- void this.fetchWireguardKey();
- }
-
if (oldSettings.showBetaReleases !== newSettings.showBetaReleases) {
this.setLatestVersion(this.upgradeVersion);
}
@@ -1135,6 +1111,23 @@ class ApplicationMain {
}
}
+ private setDeviceConfig(deviceEvent: IDeviceEvent) {
+ const oldDeviceConfig = this.deviceConfig;
+ this.deviceConfig = deviceEvent.deviceConfig;
+
+ // make sure to invalidate the account data cache when account tokens change
+ this.updateAccountDataOnAccountChange(
+ oldDeviceConfig?.accountToken,
+ deviceEvent.deviceConfig?.accountToken,
+ );
+
+ void this.updateAccountHistory();
+
+ if (this.windowController) {
+ IpcMainEventChannel.account.notifyDevice(this.windowController.webContents, deviceEvent);
+ }
+ }
+
private trayIconType(tunnelState: TunnelState, blockWhenDisconnected: boolean): TrayIconType {
switch (tunnelState.state) {
case 'connected':
@@ -1216,6 +1209,7 @@ class ApplicationMain {
accountHistory: this.accountHistory,
tunnelState: this.tunnelState,
settings: this.settings,
+ deviceConfig: this.deviceConfig,
relayListPair: {
relays: this.processRelaysForPresentation(this.relays, this.settings.relaySettings),
bridges: this.processBridgesForPresentation(this.relays, this.settings.bridgeState),
@@ -1223,7 +1217,6 @@ class ApplicationMain {
currentVersion: this.currentVersion,
upgradeVersion: this.upgradeVersion,
guiSettings: this.guiSettings.state,
- wireguardPublicKey: this.wireguardPublicKey,
translations: this.translations,
windowsSplitTunnelingApplications: this.windowsSplitTunnelingApplications,
macOsScrollbarVisibility: this.macOsScrollbarVisibility,
@@ -1306,7 +1299,7 @@ class ApplicationMain {
IpcMainEventChannel.account.handleLogout(() => this.logout());
IpcMainEventChannel.account.handleGetWwwAuthToken(() => this.daemonRpc.getWwwAuthToken());
IpcMainEventChannel.account.handleSubmitVoucher(async (voucherCode: string) => {
- const currentAccountToken = this.settings.accountToken;
+ const currentAccountToken = this.deviceConfig?.accountToken;
const response = await this.daemonRpc.submitVoucher(voucherCode);
if (currentAccountToken) {
@@ -1317,20 +1310,22 @@ class ApplicationMain {
});
IpcMainEventChannel.account.handleUpdateData(() => this.updateAccountData());
+ IpcMainEventChannel.account.handleGetDevice(async () => {
+ const deviceConfig = await this.daemonRpc.getDevice();
+ return deviceConfig?.device;
+ });
+ IpcMainEventChannel.account.handleListDevices((accountToken: AccountToken) => {
+ return this.daemonRpc.listDevices(accountToken);
+ });
+ IpcMainEventChannel.account.handleRemoveDevice((deviceRemoval: IDeviceRemoval) => {
+ return this.daemonRpc.removeDevice(deviceRemoval);
+ });
+
IpcMainEventChannel.accountHistory.handleClear(async () => {
await this.daemonRpc.clearAccountHistory();
void this.updateAccountHistory();
});
- IpcMainEventChannel.wireguardKeys.handleGenerateKey(async () => {
- try {
- return await this.daemonRpc.generateWireguardKey();
- } catch {
- return 'generation_failure';
- }
- });
- IpcMainEventChannel.wireguardKeys.handleVerifyKey(() => this.daemonRpc.verifyWireguardKey());
-
IpcMainEventChannel.linuxSplitTunneling.handleGetApplications(() => {
if (linuxSplitTunneling) {
return linuxSplitTunneling.getApplications(this.locale);
@@ -1484,28 +1479,11 @@ class ApplicationMain {
private async login(accountToken: AccountToken): Promise<void> {
try {
- const verification = await this.verifyAccount(accountToken);
-
- if (verification.status === 'deferred') {
- log.warn(`Failed to get account data, logging in anyway: ${verification.error.message}`);
- }
-
- this.autoConnectOnWireguardKeyEvent = this.guiSettings.autoConnect;
- await this.daemonRpc.setAccount(accountToken);
-
- // Fallback if daemon doesn't send event.
- if (this.autoConnectOnWireguardKeyEvent) {
- this.autoConnectFallbackScheduler.schedule(
- () => this.wireguardKeygenEventAutoConnect(),
- AUTO_CONNECT_FALLBACK_DELAY,
- );
- }
+ await this.daemonRpc.loginAccount(accountToken);
} catch (e) {
const error = e as Error;
log.error(`Failed to login: ${error.message}`);
- this.autoConnectOnWireguardKeyEvent = false;
-
if (error instanceof InvalidAccountError) {
throw Error(messages.gettext('Invalid account number'));
} else {
@@ -1514,21 +1492,10 @@ class ApplicationMain {
}
}
- private wireguardKeygenEventAutoConnect() {
- if (this.autoConnectOnWireguardKeyEvent) {
- this.autoConnectOnWireguardKeyEvent = false;
- this.autoConnectFallbackScheduler.cancel();
- void this.autoConnect();
- }
- }
-
private async autoConnect() {
if (process.env.NODE_ENV === 'development') {
log.info('Skip autoconnect in development');
- } else if (
- this.settings.accountToken &&
- (!this.accountData || !hasExpired(this.accountData.expiry))
- ) {
+ } else if (this.deviceConfig && (!this.accountData || !hasExpired(this.accountData.expiry))) {
if (this.guiSettings.autoConnect) {
try {
log.info('Autoconnect the tunnel');
@@ -1548,9 +1515,8 @@ class ApplicationMain {
private async logout(): Promise<void> {
try {
- await this.daemonRpc.setAccount();
+ await this.daemonRpc.logoutAccount();
- this.autoConnectFallbackScheduler.cancel();
this.accountExpiryNotificationScheduler.cancel();
} catch (e) {
const error = e as Error;
@@ -1560,22 +1526,6 @@ class ApplicationMain {
}
}
- private verifyAccount(accountToken: AccountToken): Promise<AccountVerification> {
- return new Promise((resolve, reject) => {
- this.accountDataCache.invalidate();
- this.accountDataCache.fetch(accountToken, {
- onFinish: () => resolve({ status: 'verified' }),
- onError: (error) => {
- if (error instanceof InvalidAccountError) {
- reject(error);
- } else {
- resolve({ status: 'deferred', error });
- }
- },
- });
- });
- }
-
private updateAccountDataOnAccountChange(oldAccount?: string, newAccount?: string) {
if (oldAccount && !newAccount) {
this.accountDataCache.invalidate();
@@ -1589,8 +1539,8 @@ class ApplicationMain {
}
private updateAccountData() {
- if (this.connectedToDaemon && this.settings.accountToken) {
- this.accountDataCache.fetch(this.settings.accountToken);
+ if (this.connectedToDaemon && this.deviceConfig) {
+ this.accountDataCache.fetch(this.deviceConfig.accountToken);
}
}
@@ -1641,15 +1591,6 @@ class ApplicationMain {
}
}
- private async fetchWireguardKey(): Promise<void> {
- try {
- this.setWireguardKey(await this.daemonRpc.getWireguardKey());
- } catch (e) {
- const error = e as Error;
- log.error(`Failed to fetch wireguard key: ${error.message}`);
- }
- }
-
private updateDaemonsAutoConnect() {
const daemonAutoConnect = this.guiSettings.autoConnect && getOpenAtLogin();
if (daemonAutoConnect !== this.settings.autoConnect) {
@@ -1984,7 +1925,7 @@ class ApplicationMain {
this.tray?.on('right-click', () =>
this.trayIconController?.popUpContextMenu(
this.connectedToDaemon,
- this.settings.accountToken,
+ this.deviceConfig?.accountToken,
this.tunnelState,
),
);
@@ -2022,7 +1963,7 @@ class ApplicationMain {
private setTrayContextMenu() {
this.trayIconController?.setContextMenu(
this.connectedToDaemon,
- this.settings.accountToken,
+ this.deviceConfig?.accountToken,
this.tunnelState,
);
}