From 4650984b64d0aba0ff4c63aece6844dce3825e4b Mon Sep 17 00:00:00 2001 From: Oskar Nyberg Date: Thu, 30 May 2024 09:20:48 +0200 Subject: [PATCH] Show error and troubleshoot dialog when lacking full disk access --- gui/locales/messages.pot | 18 +- gui/src/main/daemon-rpc.ts | 3 +- gui/src/main/user-interface.ts | 12 +- gui/src/renderer/app.tsx | 4 + .../renderer/components/NotificationArea.tsx | 40 +- gui/src/shared/daemon-rpc-types.ts | 4 +- gui/src/shared/ipc-schema.ts | 1 + gui/src/shared/notifications/error.ts | 421 ++++++++++-------- gui/src/shared/notifications/notification.ts | 6 + 9 files changed, 298 insertions(+), 211 deletions(-) diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot index a2685f87077d..e1a3d61497e0 100644 --- a/gui/locales/messages.pot +++ b/gui/locales/messages.pot @@ -1087,7 +1087,11 @@ msgid "Disconnected and unsecure" msgstr "" msgctxt "notifications" -msgid "Failed to enable split tunneling. Please try again or disable it." +msgid "Failed to enable split tunneling." +msgstr "" + +msgctxt "notifications" +msgid "Failed to enable split tunneling. Please try reconnecting or disable split tunneling." msgstr "" msgctxt "notifications" @@ -1682,6 +1686,14 @@ msgctxt "tray-icon-tooltip" msgid "Connecting. %(location)s" msgstr "" +msgctxt "troubleshoot" +msgid "Enable “Full Disk Access” for “Mullvad VPN” in the macOS system settings." +msgstr "" + +msgctxt "troubleshoot" +msgid "Failed to enable split tunneling. This is because the app is missing system permissions. What you can do:" +msgstr "" + msgctxt "troubleshoot" msgid "If these steps do not work please send a problem report." msgstr "" @@ -1690,6 +1702,10 @@ msgctxt "troubleshoot" msgid "Make sure you have NF tables support." msgstr "" +msgctxt "troubleshoot" +msgid "Open system settings" +msgstr "" + msgctxt "troubleshoot" msgid "This error can happen when something other than Mullvad is actively updating the DNS." msgstr "" diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index c51524f1ee2f..e8f8d96e1806 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -1047,10 +1047,9 @@ function convertFromTunnelStateError(state: grpcTypes.ErrorState.AsObject): Erro cause: ErrorStateCause.splitTunnelError, }; case grpcTypes.ErrorState.Cause.NEED_FULL_DISK_PERMISSIONS: - // TODO: handle correctly return { ...baseError, - cause: ErrorStateCause.splitTunnelError, + cause: ErrorStateCause.needFullDiskPermissions, }; case grpcTypes.ErrorState.Cause.VPN_PERMISSION_DENIED: // VPN_PERMISSION_DENIED is only ever created on Android diff --git a/gui/src/main/user-interface.ts b/gui/src/main/user-interface.ts index 3d6e04f8e7cd..e3dd5a50eda0 100644 --- a/gui/src/main/user-interface.ts +++ b/gui/src/main/user-interface.ts @@ -74,12 +74,20 @@ export default class UserInterface implements WindowControllerDelegate { }); IpcMainEventChannel.app.handleShowLaunchDaemonSettings(async () => { + try { + await execAsync('open x-apple.systempreferences:com.apple.LoginItems-Settings.extension'); + } catch (error) { + log.error(`Failed to open launch daemon settings: ${error}`); + } + }); + + IpcMainEventChannel.app.handleShowFullDiskAccessSettings(async () => { try { await execAsync( - 'open -W x-apple.systempreferences:com.apple.LoginItems-Settings.extension', + 'open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"', ); } catch (error) { - log.error(`Failed to open launch daemon settings: ${error}`); + log.error(`Failed to open Full Disk Access settings: ${error}`); } }); } diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index 3aa73698dc82..e62d65358e24 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -529,6 +529,10 @@ export default class AppRenderer { await IpcRendererEventChannel.app.showLaunchDaemonSettings(); } + public showFullDiskAccessSettings = async () => { + await IpcRendererEventChannel.app.showFullDiskAccessSettings(); + }; + public async sendProblemReport( email: string, message: string, diff --git a/gui/src/renderer/components/NotificationArea.tsx b/gui/src/renderer/components/NotificationArea.tsx index a2a89bcd84b0..79c234f71fab 100644 --- a/gui/src/renderer/components/NotificationArea.tsx +++ b/gui/src/renderer/components/NotificationArea.tsx @@ -43,6 +43,8 @@ interface IProps { } export default function NotificationArea(props: IProps) { + const { showFullDiskAccessSettings } = useAppContext(); + const account = useSelector((state: IReduxState) => state.account); const locale = useSelector((state: IReduxState) => state.userInterface.locale); const tunnelState = useSelector((state: IReduxState) => state.connection.status); @@ -65,7 +67,7 @@ export default function NotificationArea(props: IProps) { blockWhenDisconnected, hasExcludedApps, }), - new ErrorNotificationProvider({ tunnelState, hasExcludedApps }), + new ErrorNotificationProvider({ tunnelState, hasExcludedApps, showFullDiskAccessSettings }), new InconsistentVersionNotificationProvider({ consistent: version.consistent }), new UnsupportedVersionNotificationProvider(version), ]; @@ -168,20 +170,40 @@ function NotificationActionWrapper(props: INotificationActionWrapperProps) { } } + const problemReportButton = troubleshootInfo?.buttons ? ( + + {messages.pgettext('in-app-notifications', 'Send problem report')} + + ) : ( + + {messages.pgettext('in-app-notifications', 'Send problem report')} + + ); + + let buttons = [ + problemReportButton, + + {messages.gettext('Back')} + , + ]; + + if (troubleshootInfo?.buttons) { + const actionButtons = troubleshootInfo.buttons.map((button) => ( + + {button.label} + + )); + + buttons = actionButtons.concat(buttons); + } + return ( <> {actionComponent} - {messages.pgettext('in-app-notifications', 'Send problem report')} - , - - {messages.gettext('Back')} - , - ]} + buttons={buttons} close={closeTroubleshootInfo}> {troubleshootInfo?.details} diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts index 761dd01a4cac..ce0f3a896876 100644 --- a/gui/src/shared/daemon-rpc-types.ts +++ b/gui/src/shared/daemon-rpc-types.ts @@ -48,6 +48,7 @@ export enum ErrorStateCause { tunnelParameterError, isOffline, splitTunnelError, + needFullDiskPermissions, } export enum AuthFailedError { @@ -71,7 +72,8 @@ export type ErrorState = | ErrorStateCause.setDnsError | ErrorStateCause.startTunnelError | ErrorStateCause.isOffline - | ErrorStateCause.splitTunnelError; + | ErrorStateCause.splitTunnelError + | ErrorStateCause.needFullDiskPermissions; blockingError?: FirewallPolicyError; } | { diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts index e33736bcf864..acbb6366d262 100644 --- a/gui/src/shared/ipc-schema.ts +++ b/gui/src/shared/ipc-schema.ts @@ -162,6 +162,7 @@ export const ipcSchema = { openUrl: invoke(), showOpenDialog: invoke(), showLaunchDaemonSettings: invoke(), + showFullDiskAccessSettings: invoke(), getPathBaseName: invoke(), }, tunnel: { diff --git a/gui/src/shared/notifications/error.ts b/gui/src/shared/notifications/error.ts index 1d36b7e1ded0..387ed146a956 100644 --- a/gui/src/shared/notifications/error.ts +++ b/gui/src/shared/notifications/error.ts @@ -22,6 +22,7 @@ import { interface ErrorNotificationContext { tunnelState: TunnelState; hasExcludedApps: boolean; + showFullDiskAccessSettings?: () => void; } export class ErrorNotificationProvider @@ -32,7 +33,7 @@ export class ErrorNotificationProvider public getSystemNotification(): SystemNotification | undefined { if (this.context.tunnelState.state === 'error') { - let message = getMessage(this.context.tunnelState.details); + let message = this.getMessage(this.context.tunnelState.details); if (!this.context.tunnelState.details.blockingError && this.context.hasExcludedApps) { message = `${message} ${sprintf( messages.pgettext( @@ -58,7 +59,7 @@ export class ErrorNotificationProvider public getInAppNotification(): InAppNotification | undefined { if (this.context.tunnelState.state === 'error') { - let subtitle = getMessage(this.context.tunnelState.details); + let subtitle = this.getMessage(this.context.tunnelState.details); if (!this.context.tunnelState.details.blockingError && this.context.hasExcludedApps) { subtitle = `${subtitle} ${sprintf( messages.pgettext( @@ -78,231 +79,259 @@ export class ErrorNotificationProvider ? messages.pgettext('in-app-notifications', 'NETWORK TRAFFIC MIGHT BE LEAKING') : messages.pgettext('in-app-notifications', 'BLOCKING INTERNET'), subtitle, - action: getActions(this.context.tunnelState.details) ?? undefined, + action: this.getActions(this.context.tunnelState.details) ?? undefined, }; } else { return undefined; } } -} - -function getMessage(errorState: ErrorState): string { - if (errorState.blockingError) { - if (errorState.cause === ErrorStateCause.setFirewallPolicyError) { - switch (process.platform ?? window.env.platform) { - case 'win32': - return messages.pgettext( - 'notifications', - 'Unable to block all network traffic. Try temporarily disabling any third-party antivirus or security software or send a problem report.', - ); - case 'linux': - return messages.pgettext( - 'notifications', - 'Unable to block all network traffic. Try updating your kernel or send a problem report.', - ); - } - } - - return messages.pgettext( - 'notifications', - 'Unable to block all network traffic. Please troubleshoot or send a problem report.', - ); - } else { - switch (errorState.cause) { - case ErrorStateCause.authFailed: - switch (errorState.authFailedError) { - case AuthFailedError.invalidAccount: - return messages.pgettext( - 'auth-failure', - 'You are logged in with an invalid account number. Please log out and try another one.', - ); - - case AuthFailedError.expiredAccount: - return messages.pgettext('auth-failure', 'Blocking internet: account is out of time'); - case AuthFailedError.tooManyConnections: - return messages.pgettext( - 'auth-failure', - 'Too many simultaneous connections on this account. Disconnect another device or try connecting again shortly.', - ); - - case AuthFailedError.unknown: - default: - return messages.pgettext( - 'auth-failure', - 'Unable to authenticate account. Please send a problem report.', - ); - } - case ErrorStateCause.ipv6Unavailable: - return messages.pgettext( - 'notifications', - 'Could not configure IPv6. Disable it in the app or enable it on your device.', - ); - case ErrorStateCause.setFirewallPolicyError: + private getMessage(errorState: ErrorState): string { + if (errorState.blockingError) { + if (errorState.cause === ErrorStateCause.setFirewallPolicyError) { switch (process.platform ?? window.env.platform) { case 'win32': return messages.pgettext( 'notifications', - 'Unable to apply firewall rules. Try temporarily disabling any third-party antivirus or security software.', + 'Unable to block all network traffic. Try temporarily disabling any third-party antivirus or security software or send a problem report.', ); case 'linux': return messages.pgettext( 'notifications', - 'Unable to apply firewall rules. Try updating your kernel.', + 'Unable to block all network traffic. Try updating your kernel or send a problem report.', ); - default: - return messages.pgettext('notifications', 'Unable to apply firewall rules.'); } - case ErrorStateCause.setDnsError: - return messages.pgettext( - 'notifications', - 'Unable to set system DNS server. Please send a problem report.', - ); - case ErrorStateCause.startTunnelError: - return messages.pgettext( - 'notifications', - 'Unable to start tunnel connection. Please send a problem report.', - ); - case ErrorStateCause.createTunnelDeviceError: - if (errorState.osError === 4319) { + } + + return messages.pgettext( + 'notifications', + 'Unable to block all network traffic. Please troubleshoot or send a problem report.', + ); + } else { + switch (errorState.cause) { + case ErrorStateCause.authFailed: + switch (errorState.authFailedError) { + case AuthFailedError.invalidAccount: + return messages.pgettext( + 'auth-failure', + 'You are logged in with an invalid account number. Please log out and try another one.', + ); + + case AuthFailedError.expiredAccount: + return messages.pgettext('auth-failure', 'Blocking internet: account is out of time'); + + case AuthFailedError.tooManyConnections: + return messages.pgettext( + 'auth-failure', + 'Too many simultaneous connections on this account. Disconnect another device or try connecting again shortly.', + ); + + case AuthFailedError.unknown: + default: + return messages.pgettext( + 'auth-failure', + 'Unable to authenticate account. Please send a problem report.', + ); + } + case ErrorStateCause.ipv6Unavailable: return messages.pgettext( 'notifications', - 'Unable to start tunnel connection. This could be because of conflicts with VMware, please troubleshoot.', + 'Could not configure IPv6. Disable it in the app or enable it on your device.', ); - } + case ErrorStateCause.setFirewallPolicyError: + switch (process.platform ?? window.env.platform) { + case 'win32': + return messages.pgettext( + 'notifications', + 'Unable to apply firewall rules. Try temporarily disabling any third-party antivirus or security software.', + ); + case 'linux': + return messages.pgettext( + 'notifications', + 'Unable to apply firewall rules. Try updating your kernel.', + ); + default: + return messages.pgettext('notifications', 'Unable to apply firewall rules.'); + } + case ErrorStateCause.setDnsError: + return messages.pgettext( + 'notifications', + 'Unable to set system DNS server. Please send a problem report.', + ); + case ErrorStateCause.startTunnelError: + return messages.pgettext( + 'notifications', + 'Unable to start tunnel connection. Please send a problem report.', + ); + case ErrorStateCause.createTunnelDeviceError: + if (errorState.osError === 4319) { + return messages.pgettext( + 'notifications', + 'Unable to start tunnel connection. This could be because of conflicts with VMware, please troubleshoot.', + ); + } + return messages.pgettext( + 'notifications', + 'Unable to start tunnel connection. Please send a problem report.', + ); + case ErrorStateCause.tunnelParameterError: + return this.getTunnelParameterMessage(errorState.parameterError); + case ErrorStateCause.isOffline: + return messages.pgettext( + 'notifications', + 'Your device is offline. The tunnel will automatically connect once your device is back online.', + ); + case ErrorStateCause.needFullDiskPermissions: + return messages.pgettext('notifications', 'Failed to enable split tunneling.'); + case ErrorStateCause.splitTunnelError: + switch (process.platform ?? window.env.platform) { + case 'darwin': + return messages.pgettext( + 'notifications', + 'Failed to enable split tunneling. Please try reconnecting or disable split tunneling.', + ); + default: + return messages.pgettext( + 'notifications', + 'Unable to communicate with Mullvad kernel driver. Try reconnecting or send a problem report.', + ); + } + } + } + } + + private getTunnelParameterMessage(error: TunnelParameterError): string { + switch (error) { + /// TODO: once bridge constraints can be set, add a more descriptive error message + case TunnelParameterError.noMatchingBridgeRelay: + case TunnelParameterError.noMatchingRelay: return messages.pgettext( 'notifications', - 'Unable to start tunnel connection. Please send a problem report.', + 'No servers match your settings, try changing server or other settings.', + ); + case TunnelParameterError.noWireguardKey: + return sprintf( + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(wireguard)s - will be replaced with "WireGuard" + messages.pgettext( + 'notifications', + 'Valid %(wireguard)s key is missing. Manage keys under Advanced settings.', + ), + { wireguard: strings.wireguard }, ); - case ErrorStateCause.tunnelParameterError: - return getTunnelParameterMessage(errorState.parameterError); - case ErrorStateCause.isOffline: + case TunnelParameterError.customTunnelHostResolutionError: return messages.pgettext( 'notifications', - 'Your device is offline. The tunnel will automatically connect once your device is back online.', + 'Unable to resolve host of custom tunnel. Try changing your settings.', ); - case ErrorStateCause.splitTunnelError: - switch (process.platform ?? window.env.platform) { - case 'darwin': - return messages.pgettext( - 'notifications', - 'Failed to enable split tunneling. Please try again or disable it.', - ); - default: - return messages.pgettext( - 'notifications', - 'Unable to communicate with Mullvad kernel driver. Try reconnecting or send a problem report.', - ); - } } } -} -function getTunnelParameterMessage(error: TunnelParameterError): string { - switch (error) { - /// TODO: once bridge constraints can be set, add a more descriptive error message - case TunnelParameterError.noMatchingBridgeRelay: - case TunnelParameterError.noMatchingRelay: - return messages.pgettext( - 'notifications', - 'No servers match your settings, try changing server or other settings.', - ); - case TunnelParameterError.noWireguardKey: - return sprintf( - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(wireguard)s - will be replaced with "WireGuard" - messages.pgettext( - 'notifications', - 'Valid %(wireguard)s key is missing. Manage keys under Advanced settings.', - ), - { wireguard: strings.wireguard }, - ); - case TunnelParameterError.customTunnelHostResolutionError: - return messages.pgettext( - 'notifications', - 'Unable to resolve host of custom tunnel. Try changing your settings.', - ); - } -} + private getActions(errorState: ErrorState): InAppNotificationAction | void { + const platform = process.platform ?? window.env.platform; + + if (errorState.cause === ErrorStateCause.setFirewallPolicyError && platform === 'linux') { + return { + type: 'troubleshoot-dialog', + troubleshoot: { + details: messages.pgettext('troubleshoot', 'This might be caused by an outdated kernel.'), + steps: [ + messages.pgettext('troubleshoot', 'Update your kernel.'), + messages.pgettext('troubleshoot', 'Make sure you have NF tables support.'), + ], + }, + }; + } else if (errorState.cause === ErrorStateCause.setDnsError) { + const troubleshootSteps = []; + if (platform === 'darwin') { + troubleshootSteps.push( + messages.pgettext( + 'troubleshoot', + 'Try to turn Wi-Fi Calling off in the FaceTime app settings and restart the Mac.', + ), + messages.pgettext( + 'troubleshoot', + 'Uninstall or disable other DNS, networking and ads/website blocking apps.', + ), + ); + } else if (platform === 'win32') { + troubleshootSteps.push( + messages.pgettext( + 'troubleshoot', + 'Uninstall or disable other DNS, networking and ads/website blocking apps.', + ), + ); + } -function getActions(errorState: ErrorState): InAppNotificationAction | void { - const platform = process.platform ?? window.env.platform; + return { + type: 'troubleshoot-dialog', + troubleshoot: { + details: messages.pgettext( + 'troubleshoot', + 'This error can happen when something other than Mullvad is actively updating the DNS.', + ), + steps: troubleshootSteps, + }, + }; + } else if (errorState.cause === ErrorStateCause.needFullDiskPermissions) { + let troubleshootButtons = undefined; + if (this.context.showFullDiskAccessSettings) { + troubleshootButtons = [ + { + label: messages.pgettext('troubleshoot', 'Open system settings'), + action: () => this.context.showFullDiskAccessSettings?.(), + }, + ]; + } - if (errorState.cause === ErrorStateCause.setFirewallPolicyError && platform === 'linux') { - return { - type: 'troubleshoot-dialog', - troubleshoot: { - details: messages.pgettext('troubleshoot', 'This might be caused by an outdated kernel.'), - steps: [ - messages.pgettext('troubleshoot', 'Update your kernel.'), - messages.pgettext('troubleshoot', 'Make sure you have NF tables support.'), - ], - }, - }; - } else if (errorState.cause === ErrorStateCause.setDnsError) { - const troubleshootSteps = []; - if (platform === 'darwin') { - troubleshootSteps.push( - messages.pgettext( - 'troubleshoot', - 'Try to turn Wi-Fi Calling off in the FaceTime app settings and restart the Mac.', - ), - messages.pgettext( - 'troubleshoot', - 'Uninstall or disable other DNS, networking and ads/website blocking apps.', - ), - ); - } else if (platform === 'win32') { - troubleshootSteps.push( - messages.pgettext( - 'troubleshoot', - 'Uninstall or disable other DNS, networking and ads/website blocking apps.', - ), - ); + return { + type: 'troubleshoot-dialog', + troubleshoot: { + details: messages.pgettext( + 'troubleshoot', + 'Failed to enable split tunneling. This is because the app is missing system permissions. What you can do:', + ), + steps: [ + messages.pgettext( + 'troubleshoot', + 'Enable “Full Disk Access” for “Mullvad VPN” in the macOS system settings.', + ), + ], + buttons: troubleshootButtons, + }, + }; + } else if (platform === 'win32' && errorState.cause === ErrorStateCause.splitTunnelError) { + return { + type: 'troubleshoot-dialog', + troubleshoot: { + details: messages.pgettext( + 'troubleshoot', + 'Unable to communicate with Mullvad kernel driver.', + ), + steps: [ + messages.pgettext('troubleshoot', 'Try reconnecting.'), + messages.pgettext('troubleshoot', 'Try restarting your device.'), + ], + }, + }; + } else if ( + errorState.cause === ErrorStateCause.createTunnelDeviceError && + errorState.osError === 4319 + ) { + return { + type: 'troubleshoot-dialog', + troubleshoot: { + details: messages.pgettext( + 'troubleshoot', + 'Unable to start tunnel connection because of a failure when creating the tunnel device. This is often caused by conflicts with the VMware Bridge Protocol.', + ), + steps: [ + messages.pgettext('troubleshoot', 'Try to reinstall VMware.'), + messages.pgettext('troubleshoot', 'Try to uninstall VMware.'), + ], + }, + }; } - - return { - type: 'troubleshoot-dialog', - troubleshoot: { - details: messages.pgettext( - 'troubleshoot', - 'This error can happen when something other than Mullvad is actively updating the DNS.', - ), - steps: troubleshootSteps, - }, - }; - } else if (errorState.cause === ErrorStateCause.splitTunnelError) { - // TODO: macos: handle this and full disk access error - return { - type: 'troubleshoot-dialog', - troubleshoot: { - details: messages.pgettext( - 'troubleshoot', - 'Unable to communicate with Mullvad kernel driver.', - ), - steps: [ - messages.pgettext('troubleshoot', 'Try reconnecting.'), - messages.pgettext('troubleshoot', 'Try restarting your device.'), - ], - }, - }; - } else if ( - errorState.cause === ErrorStateCause.createTunnelDeviceError && - errorState.osError === 4319 - ) { - return { - type: 'troubleshoot-dialog', - troubleshoot: { - details: messages.pgettext( - 'troubleshoot', - 'Unable to start tunnel connection because of a failure when creating the tunnel device. This is often caused by conflicts with the VMware Bridge Protocol.', - ), - steps: [ - messages.pgettext('troubleshoot', 'Try to reinstall VMware.'), - messages.pgettext('troubleshoot', 'Try to uninstall VMware.'), - ], - }, - }; } } diff --git a/gui/src/shared/notifications/notification.ts b/gui/src/shared/notifications/notification.ts index 1957cc07fb0f..87166aab4d57 100644 --- a/gui/src/shared/notifications/notification.ts +++ b/gui/src/shared/notifications/notification.ts @@ -8,6 +8,12 @@ export type NotificationAction = { export interface InAppNotificationTroubleshootInfo { details: string; steps: string[]; + buttons?: Array; +} + +export interface InAppNotificationTroubleshootButton { + label: string; + action: () => void; } export type InAppNotificationAction =