summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--desktop/packages/mullvad-vpn/locales/messages.pot20
-rw-r--r--desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts10
-rw-r--r--desktop/packages/mullvad-vpn/src/main/index.ts4
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/app.tsx3
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/SearchBar.tsx13
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/LinuxSettings.tsx40
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/LinuxSettingsContext.tsx25
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/HeaderDescription.tsx59
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/hooks/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/hooks/use-show-unsupported-dialog.ts13
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/index.ts2
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/LinuxApplicationList.tsx8
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/components/linux-application-row/hooks/use-disabled.ts5
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/components/linux-application-row/hooks/use-launch-application.ts15
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/OpenFilePickerButton.tsx5
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/hooks/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/hooks/use-disabled.ts9
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/unsupported-dialog/UnsupportedDialog.tsx45
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/unsupported-dialog/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/shared/ipc-schema.ts1
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/mocked/split-tunneling/split-tunneling.spec.ts92
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/route-object-models/routes-object-model.ts3
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/route-object-models/settings/selectors.ts1
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/route-object-models/settings/settings-route-object-model.ts5
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/route-object-models/split-tunneling-settings/index.ts2
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/route-object-models/split-tunneling-settings/selectors.ts17
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/route-object-models/split-tunneling-settings/split-tunneling-settings-route-object-model.ts37
28 files changed, 416 insertions, 22 deletions
diff --git a/desktop/packages/mullvad-vpn/locales/messages.pot b/desktop/packages/mullvad-vpn/locales/messages.pot
index 3eafee0b0e..ba8742fb5d 100644
--- a/desktop/packages/mullvad-vpn/locales/messages.pot
+++ b/desktop/packages/mullvad-vpn/locales/messages.pot
@@ -2128,6 +2128,13 @@ msgctxt "split-tunneling-view"
msgid "%(applicationName)s is problematic and can’t be excluded from the VPN tunnel."
msgstr ""
+#. Information about split tunneling not being supported on the system.
+#. Available placeholders:
+#. %(splitTunneling)s - will be replaced with Split tunneling
+msgctxt "split-tunneling-view"
+msgid "%(splitTunneling)s is not supported by your system."
+msgstr ""
+
msgctxt "split-tunneling-view"
msgid "Add"
msgstr ""
@@ -2140,6 +2147,11 @@ msgctxt "split-tunneling-view"
msgid "Choose the apps you want to exclude from the VPN tunnel."
msgstr ""
+#. Link for learning more
+msgctxt "split-tunneling-view"
+msgid "Click here to learn more"
+msgstr ""
+
msgctxt "split-tunneling-view"
msgid "Click on an app to launch it. Its traffic will bypass the VPN tunnel until you close it."
msgstr ""
@@ -2187,6 +2199,14 @@ msgctxt "split-tunneling-view"
msgid "Restart Mullvad Service"
msgstr ""
+#. Information about split tunneling being unavailable due to
+#. missing support in the user's operating system.
+#. Available placeholders:
+#. %(splitTunneling)s - will be replaced with Split tunneling
+msgctxt "split-tunneling-view"
+msgid "To use %(splitTunneling)s, please change to a Linux kernel version that supports cgroup v1."
+msgstr ""
+
msgctxt "split-tunneling-view"
msgid "To use split tunneling please enable “Full disk access” for “Mullvad VPN” in the macOS system settings."
msgstr ""
diff --git a/desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts b/desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts
index ab76aba907..5c8e6de6e1 100644
--- a/desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts
+++ b/desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts
@@ -498,9 +498,13 @@ export class DaemonRpc extends GrpcClient {
await this.callBool(this.client.setSplitTunnelState, enabled);
}
- public async splitTunnelIsEnabled(): Promise<boolean> {
- const isEnabled = await this.callEmpty<BoolValue>(this.client.splitTunnelIsEnabled);
- return isEnabled.getValue();
+ public async linuxSplitTunnelIsSupported(): Promise<boolean> {
+ try {
+ const isEnabled = await this.callEmpty<BoolValue>(this.client.splitTunnelIsEnabled);
+ return isEnabled.getValue();
+ } catch {
+ return false;
+ }
}
public async needFullDiskPermissions(): Promise<boolean> {
diff --git a/desktop/packages/mullvad-vpn/src/main/index.ts b/desktop/packages/mullvad-vpn/src/main/index.ts
index 9c354f4dd4..0b582c3cd6 100644
--- a/desktop/packages/mullvad-vpn/src/main/index.ts
+++ b/desktop/packages/mullvad-vpn/src/main/index.ts
@@ -848,6 +848,10 @@ class ApplicationMain
return Promise.resolve(this.translations);
});
+ IpcMainEventChannel.linuxSplitTunneling.handleIsSplitTunnelingSupported(() => {
+ return this.daemonRpc.linuxSplitTunnelIsSupported();
+ });
+
IpcMainEventChannel.linuxSplitTunneling.handleGetApplications(() => {
return this.linuxSplitTunneling!.getApplications(this.locale);
});
diff --git a/desktop/packages/mullvad-vpn/src/renderer/app.tsx b/desktop/packages/mullvad-vpn/src/renderer/app.tsx
index 44ee6d8084..69ae19338d 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/app.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/app.tsx
@@ -445,6 +445,9 @@ export default class AppRenderer {
public daemonPrepareRestart = (shutdown: boolean): void => {
IpcRendererEventChannel.daemon.prepareRestart(shutdown);
};
+ public getLinuxSplitTunnelingSupported = () => {
+ return IpcRendererEventChannel.linuxSplitTunneling.isSplitTunnelingSupported();
+ };
public getAppUpgradeCacheDir = () => IpcRendererEventChannel.app.getUpgradeCacheDir();
public tryStartDaemon = () => {
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/SearchBar.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/SearchBar.tsx
index e7a355c8b4..de0c36902e 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/SearchBar.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/SearchBar.tsx
@@ -29,6 +29,12 @@ export const StyledSearchInput = styled.input.attrs({ type: 'text' })({
color: colors.blue,
backgroundColor: colors.white,
},
+ '&&:disabled': {
+ backgroundColor: colors.whiteOnDarkBlue5,
+ '&&::placeholder': {
+ color: colors.whiteAlpha20,
+ },
+ },
});
export const StyledClearButton = styled(IconButton)({
@@ -53,17 +59,21 @@ export const StyledSearchIcon = styled(Icon)({
[`${StyledSearchInput}:focus ~ &&`]: {
backgroundColor: colors.blue,
},
+ [`${StyledSearchInput}:disabled ~ &&`]: {
+ backgroundColor: colors.whiteAlpha20,
+ },
});
export interface ISearchBarProps {
searchTerm: string;
+ disabled?: boolean;
onSearch: (searchTerm: string) => void;
className?: string;
disableAutoFocus?: boolean;
}
export default function SearchBar(props: ISearchBarProps) {
- const { onSearch } = props;
+ const { disabled, onSearch } = props;
const inputRef = useStyledRef<HTMLInputElement>();
@@ -96,6 +106,7 @@ export default function SearchBar(props: ISearchBarProps) {
return (
<StyledSearchContainer className={props.className}>
<StyledSearchInput
+ disabled={disabled}
ref={inputRef}
value={props.searchTerm}
onInput={onInput}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/LinuxSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/LinuxSettings.tsx
index fc9a7c3ca3..50a4d2d617 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/LinuxSettings.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/LinuxSettings.tsx
@@ -1,7 +1,6 @@
import { useEffect } from 'react';
import { strings } from '../../../../../../shared/constants';
-import { messages } from '../../../../../../shared/gettext';
import { useAppContext } from '../../../../../context';
import { Flex, Spinner } from '../../../../../lib/components';
import { FlexColumn } from '../../../../../lib/components/flex-column';
@@ -10,19 +9,36 @@ import { useEffectEvent } from '../../../../../lib/utility-hooks';
import SettingsHeader, { HeaderSubTitle, HeaderTitle } from '../../../../SettingsHeader';
import { ApplicationSearchBar } from '../application-search-bar';
import { ApplicationSearchNoResult } from '../application-search-no-result';
-import { LaunchErrorDialog, LinuxApplicationList, OpenFilePickerButton } from './components';
+import {
+ HeaderDescription,
+ LaunchErrorDialog,
+ LinuxApplicationList,
+ OpenFilePickerButton,
+ UnsupportedDialog,
+} from './components';
import { useShowLinuxApplicationList, useShowNoSearchResult, useShowSpinner } from './hooks';
import { LinuxSettingsContextProvider, useLinuxSettingsContext } from './LinuxSettingsContext';
function LinuxSettingsInner() {
- const { getLinuxSplitTunnelingApplications } = useAppContext();
- const { searchTerm, setApplications, setSearchTerm } = useLinuxSettingsContext();
+ const { getLinuxSplitTunnelingSupported, getLinuxSplitTunnelingApplications } = useAppContext();
+ const {
+ splitTunnelingSupported,
+ searchTerm,
+ setApplications,
+ setSearchTerm,
+ setSplitTunnelingSupported,
+ } = useLinuxSettingsContext();
const runAfterTransition = useAfterTransition();
const showLinuxApplicationList = useShowLinuxApplicationList();
const showNoSearchResult = useShowNoSearchResult();
const showSpinner = useShowSpinner();
- const updateApplications = useEffectEvent(() => {
+ const onMount = useEffectEvent(() => {
+ runAfterTransition(async () => {
+ const linuxSplitTunnelingSupported = await getLinuxSplitTunnelingSupported();
+ setSplitTunnelingSupported(linuxSplitTunnelingSupported);
+ });
+
runAfterTransition(async () => {
const applications = await getLinuxSplitTunnelingApplications();
setApplications(applications);
@@ -34,20 +50,21 @@ function LinuxSettingsInner() {
// Enable these rules again when eslint can lint useEffectEvent properly.
// eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
- useEffect(() => void updateApplications(), []);
+ useEffect(() => void onMount(), []);
return (
<>
<SettingsHeader>
<HeaderTitle>{strings.splitTunneling}</HeaderTitle>
<HeaderSubTitle>
- {messages.pgettext(
- 'split-tunneling-view',
- 'Click on an app to launch it. Its traffic will bypass the VPN tunnel until you close it.',
- )}
+ <HeaderDescription />
</HeaderSubTitle>
</SettingsHeader>
- <ApplicationSearchBar searchTerm={searchTerm} onSearch={setSearchTerm} />
+ <ApplicationSearchBar
+ disabled={!splitTunnelingSupported}
+ searchTerm={searchTerm}
+ onSearch={setSearchTerm}
+ />
{showNoSearchResult && <ApplicationSearchNoResult searchTerm={searchTerm} />}
<FlexColumn $gap="medium">
{showLinuxApplicationList && <LinuxApplicationList />}
@@ -61,6 +78,7 @@ function LinuxSettingsInner() {
</Flex>
</FlexColumn>
<LaunchErrorDialog />
+ <UnsupportedDialog />
</>
);
}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/LinuxSettingsContext.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/LinuxSettingsContext.tsx
index 5af8fa2277..5e041c0141 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/LinuxSettingsContext.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/LinuxSettingsContext.tsx
@@ -13,6 +13,10 @@ type LinuxSettingsContext = {
setApplications: (value: ILinuxSplitTunnelingApplication[]) => void;
setBrowseError: (value?: string) => void;
setSearchTerm: (value: string) => void;
+ setShowUnsupportedDialog: (value: boolean) => void;
+ setSplitTunnelingSupported: (value: boolean) => void;
+ showUnsupportedDialog: boolean;
+ splitTunnelingSupported?: boolean;
};
const LinuxSettingsContext = React.createContext<LinuxSettingsContext | undefined>(undefined);
@@ -29,6 +33,10 @@ export function LinuxSettingsContextProvider({ children }: LinuxSettingsContextP
const [applications, setApplications] = useState<ILinuxSplitTunnelingApplication[]>();
const [browseError, setBrowseError] = useState<string>();
const [searchTerm, setSearchTerm] = useState('');
+ const [splitTunnelingSupported, setSplitTunnelingSupported] = useState<boolean | undefined>(
+ undefined,
+ );
+ const [showUnsupportedDialog, setShowUnsupportedDialog] = useState(false);
const value = useMemo(
() => ({
@@ -38,8 +46,23 @@ export function LinuxSettingsContextProvider({ children }: LinuxSettingsContextP
setApplications,
setBrowseError,
setSearchTerm,
+ setShowUnsupportedDialog,
+ setSplitTunnelingSupported,
+ showUnsupportedDialog,
+ splitTunnelingSupported,
}),
- [applications, browseError, searchTerm, setApplications, setBrowseError, setSearchTerm],
+ [
+ applications,
+ browseError,
+ searchTerm,
+ setApplications,
+ setBrowseError,
+ setSearchTerm,
+ setShowUnsupportedDialog,
+ setSplitTunnelingSupported,
+ showUnsupportedDialog,
+ splitTunnelingSupported,
+ ],
);
return <LinuxSettingsContext value={value}>{children}</LinuxSettingsContext>;
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/HeaderDescription.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/HeaderDescription.tsx
new file mode 100644
index 0000000000..bb6a602731
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/HeaderDescription.tsx
@@ -0,0 +1,59 @@
+import { sprintf } from 'sprintf-js';
+
+import { strings } from '../../../../../../../../shared/constants';
+import { messages } from '../../../../../../../../shared/gettext';
+import { Icon, Link } from '../../../../../../../lib/components';
+import { FlexColumn } from '../../../../../../../lib/components/flex-column';
+import { FlexRow } from '../../../../../../../lib/components/flex-row';
+import { useLinuxSettingsContext } from '../../LinuxSettingsContext';
+import { useShowUnsupportedDialog } from './hooks';
+
+export function HeaderDescription() {
+ const { splitTunnelingSupported } = useLinuxSettingsContext();
+ const message = sprintf(
+ // TRANSLATORS: Information about split tunneling not being supported on the system.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(splitTunneling)s - will be replaced with Split tunneling
+ messages.pgettext(
+ 'split-tunneling-view',
+ '%(splitTunneling)s is not supported by your system.',
+ ),
+ {
+ splitTunneling: strings.splitTunneling,
+ },
+ );
+ const showUnsupportedDialog = useShowUnsupportedDialog();
+
+ if (splitTunnelingSupported === false) {
+ return (
+ <FlexRow>
+ <FlexColumn $justifyContent="center" $margin={{ right: 'small' }}>
+ <Icon size="small" color="whiteAlpha60" icon="info-circle" />
+ </FlexColumn>
+ <FlexColumn>
+ <span>
+ {message}
+ &nbsp;
+ <Link
+ aria-description={message}
+ as="button"
+ onClick={showUnsupportedDialog}
+ variant="labelTiny">
+ <Link.Text>
+ {
+ // TRANSLATORS: Link for learning more
+ messages.pgettext('split-tunneling-view', 'Click here to learn more')
+ }
+ </Link.Text>
+ </Link>
+ </span>
+ </FlexColumn>
+ </FlexRow>
+ );
+ }
+
+ return messages.pgettext(
+ 'split-tunneling-view',
+ 'Click on an app to launch it. Its traffic will bypass the VPN tunnel until you close it.',
+ );
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/hooks/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/hooks/index.ts
new file mode 100644
index 0000000000..4b2b16cffb
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/hooks/index.ts
@@ -0,0 +1 @@
+export * from './use-show-unsupported-dialog';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/hooks/use-show-unsupported-dialog.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/hooks/use-show-unsupported-dialog.ts
new file mode 100644
index 0000000000..fd4bbc531a
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/hooks/use-show-unsupported-dialog.ts
@@ -0,0 +1,13 @@
+import { useCallback } from 'react';
+
+import { useLinuxSettingsContext } from '../../../LinuxSettingsContext';
+
+export function useShowUnsupportedDialog() {
+ const { setShowUnsupportedDialog } = useLinuxSettingsContext();
+
+ const showUnsupportedDialog = useCallback(() => {
+ setShowUnsupportedDialog(true);
+ }, [setShowUnsupportedDialog]);
+
+ return showUnsupportedDialog;
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/index.ts
new file mode 100644
index 0000000000..52c084e8ff
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/header-description/index.ts
@@ -0,0 +1 @@
+export * from './HeaderDescription';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/index.ts
index 6dd9b7e1cb..15401396b3 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/index.ts
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/index.ts
@@ -1,3 +1,5 @@
+export * from './header-description';
export * from './launch-error-dialog';
export * from './linux-application-list';
export * from './open-file-picker-button';
+export * from './unsupported-dialog';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/LinuxApplicationList.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/LinuxApplicationList.tsx
index 817c6e10c2..31fc18409d 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/LinuxApplicationList.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/LinuxApplicationList.tsx
@@ -17,5 +17,11 @@ export function LinuxApplicationList() {
const filteredApplications = useFilteredApplications();
- return <ApplicationList applications={filteredApplications} rowRenderer={rowRenderer} />;
+ return (
+ <ApplicationList
+ data-testid="linux-applications"
+ applications={filteredApplications}
+ rowRenderer={rowRenderer}
+ />
+ );
}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/components/linux-application-row/hooks/use-disabled.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/components/linux-application-row/hooks/use-disabled.ts
index 052c610b3a..0086cbe977 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/components/linux-application-row/hooks/use-disabled.ts
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/components/linux-application-row/hooks/use-disabled.ts
@@ -1,9 +1,12 @@
+import { useLinuxSettingsContext } from '../../../../../LinuxSettingsContext';
import { useApplication } from './use-application';
export function useDisabled() {
+ const { splitTunnelingSupported } = useLinuxSettingsContext();
const application = useApplication();
- const disabled = application.warning === 'launches-elsewhere';
+ const disabled =
+ splitTunnelingSupported === false || application.warning === 'launches-elsewhere';
return disabled;
}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/components/linux-application-row/hooks/use-launch-application.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/components/linux-application-row/hooks/use-launch-application.ts
index 1b91848e6b..593f55c6bb 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/components/linux-application-row/hooks/use-launch-application.ts
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/linux-application-list/components/linux-application-row/hooks/use-launch-application.ts
@@ -1,20 +1,31 @@
import { useCallback } from 'react';
+import { useLinuxSettingsContext } from '../../../../../LinuxSettingsContext';
import { useLinuxApplicationRowContext } from '../LinuxApplicationRowContext';
import { useHasApplicationWarning } from './use-has-application-warning';
export function useLaunchApplication() {
const { application, onSelect, setShowWarningDialog } = useLinuxApplicationRowContext();
+ const { setShowUnsupportedDialog, splitTunnelingSupported } = useLinuxSettingsContext();
const hasApplicationWarning = useHasApplicationWarning();
const launchApplication = useCallback(() => {
- if (hasApplicationWarning) {
+ if (splitTunnelingSupported === false) {
+ setShowUnsupportedDialog(true);
+ } else if (hasApplicationWarning) {
setShowWarningDialog(true);
} else {
setShowWarningDialog(false);
onSelect?.(application);
}
- }, [application, hasApplicationWarning, onSelect, setShowWarningDialog]);
+ }, [
+ application,
+ hasApplicationWarning,
+ onSelect,
+ setShowUnsupportedDialog,
+ setShowWarningDialog,
+ splitTunnelingSupported,
+ ]);
return launchApplication;
}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/OpenFilePickerButton.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/OpenFilePickerButton.tsx
index 0b7fe7bec1..5b570f10f2 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/OpenFilePickerButton.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/OpenFilePickerButton.tsx
@@ -1,12 +1,13 @@
import { messages } from '../../../../../../../../shared/gettext';
import { Button } from '../../../../../../../lib/components';
-import { useLaunchWithFilePicker } from './hooks';
+import { useDisabled, useLaunchWithFilePicker } from './hooks';
export function OpenFilePickerButton() {
+ const disabled = useDisabled();
const launchWithFilePicker = useLaunchWithFilePicker();
return (
- <Button onClick={launchWithFilePicker}>
+ <Button disabled={disabled} onClick={launchWithFilePicker}>
<Button.Text>
{
// TRANSLATORS: Button label for browsing applications with split tunneling.
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/hooks/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/hooks/index.ts
index 502bc77f87..cc11951cd4 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/hooks/index.ts
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/hooks/index.ts
@@ -1 +1,2 @@
+export * from './use-disabled';
export * from './use-launch-with-file-picker';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/hooks/use-disabled.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/hooks/use-disabled.ts
new file mode 100644
index 0000000000..ea1146ab10
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/open-file-picker-button/hooks/use-disabled.ts
@@ -0,0 +1,9 @@
+import { useLinuxSettingsContext } from '../../../LinuxSettingsContext';
+
+export function useDisabled() {
+ const { splitTunnelingSupported } = useLinuxSettingsContext();
+
+ const disabled = splitTunnelingSupported === false;
+
+ return disabled;
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/unsupported-dialog/UnsupportedDialog.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/unsupported-dialog/UnsupportedDialog.tsx
new file mode 100644
index 0000000000..220596e4bb
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/unsupported-dialog/UnsupportedDialog.tsx
@@ -0,0 +1,45 @@
+import { useCallback } from 'react';
+import { sprintf } from 'sprintf-js';
+
+import { strings } from '../../../../../../../../shared/constants';
+import { messages } from '../../../../../../../../shared/gettext';
+import { Button } from '../../../../../../../lib/components';
+import { ModalAlert, ModalAlertType } from '../../../../../../Modal';
+import { useLinuxSettingsContext } from '../../LinuxSettingsContext';
+
+export function UnsupportedDialog() {
+ const { showUnsupportedDialog, setShowUnsupportedDialog } = useLinuxSettingsContext();
+ const hideUnsupportedDialog = useCallback(() => {
+ setShowUnsupportedDialog(false);
+ }, [setShowUnsupportedDialog]);
+
+ const unsupportedMessage = sprintf(
+ // TRANSLATORS: Information about split tunneling being unavailable due to
+ // TRANSLATORS: missing support in the user's operating system.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(splitTunneling)s - will be replaced with Split tunneling
+ messages.pgettext(
+ 'split-tunneling-view',
+ 'To use %(splitTunneling)s, please change to a Linux kernel version that supports cgroup v1.',
+ ),
+ {
+ splitTunneling: strings.splitTunneling,
+ },
+ );
+
+ const buttons = [
+ <Button key="cancel" onClick={hideUnsupportedDialog}>
+ <Button.Text>{messages.gettext('Got it!')}</Button.Text>
+ </Button>,
+ ];
+
+ return (
+ <ModalAlert
+ isOpen={showUnsupportedDialog}
+ type={ModalAlertType.info}
+ message={unsupportedMessage}
+ buttons={buttons}
+ close={hideUnsupportedDialog}
+ />
+ );
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/unsupported-dialog/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/unsupported-dialog/index.ts
new file mode 100644
index 0000000000..499647a433
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/split-tunneling/components/linux-settings/components/unsupported-dialog/index.ts
@@ -0,0 +1 @@
+export * from './UnsupportedDialog';
diff --git a/desktop/packages/mullvad-vpn/src/shared/ipc-schema.ts b/desktop/packages/mullvad-vpn/src/shared/ipc-schema.ts
index 9a9580fc13..cd33b994fe 100644
--- a/desktop/packages/mullvad-vpn/src/shared/ipc-schema.ts
+++ b/desktop/packages/mullvad-vpn/src/shared/ipc-schema.ts
@@ -253,6 +253,7 @@ export const ipcSchema = {
log: send<ILogEntry>(),
},
linuxSplitTunneling: {
+ isSplitTunnelingSupported: invoke<void, boolean>(),
getApplications: invoke<void, ILinuxSplitTunnelingApplication[]>(),
launchApplication: invoke<ILinuxSplitTunnelingApplication | string, LaunchApplicationResult>(),
},
diff --git a/desktop/packages/mullvad-vpn/test/e2e/mocked/split-tunneling/split-tunneling.spec.ts b/desktop/packages/mullvad-vpn/test/e2e/mocked/split-tunneling/split-tunneling.spec.ts
new file mode 100644
index 0000000000..ee776261f0
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/test/e2e/mocked/split-tunneling/split-tunneling.spec.ts
@@ -0,0 +1,92 @@
+import { expect, test } from '@playwright/test';
+import { Page } from 'playwright';
+
+import { RoutePath } from '../../../../src/shared/routes';
+import { RoutesObjectModel } from '../../route-object-models';
+import { MockedTestUtils, startMockedApp } from '../mocked-utils';
+
+let page: Page;
+let util: MockedTestUtils;
+let routes: RoutesObjectModel;
+
+test.describe('Split tunneling', () => {
+ test.beforeAll(async () => {
+ ({ page, util } = await startMockedApp());
+ routes = new RoutesObjectModel(page, util);
+
+ await util.waitForRoute(RoutePath.main);
+ await routes.main.gotoSettings();
+ await routes.settings.gotoSplitTunnelingSettings();
+ });
+
+ test.afterAll(async () => {
+ await page.close();
+ });
+
+ test.describe('Linux Split tunneling unsupported', () => {
+ if (process.platform !== 'linux') {
+ test.skip();
+ }
+
+ test.beforeAll(async () => {
+ await util.ipc.linuxSplitTunneling.isSplitTunnelingSupported.handle(false);
+ await util.ipc.linuxSplitTunneling.getApplications.handle([
+ {
+ absolutepath: '/app',
+ exec: 'app',
+ name: 'app',
+ type: 'app',
+ icon: '',
+ warning: undefined,
+ },
+ {
+ absolutepath: '/launches-elsewhere',
+ exec: 'launches-elsewhere',
+ name: 'launches-elsewhere',
+ type: 'launches-elsewhere',
+ icon: '',
+ warning: 'launches-elsewhere',
+ },
+ {
+ absolutepath: '/launches-in-existing-process',
+ exec: 'launches-in-existing-process',
+ name: 'launches-in-existing-process',
+ type: 'launches-in-existing-process',
+ icon: '',
+ warning: 'launches-in-existing-process',
+ },
+ ]);
+ });
+
+ test('App should show unsupported dialog when link in header is clicked', async () => {
+ // Open the unsupported dialog
+ await routes.splitTunnelingSettings.openUnsupportedDialog();
+ const unsupportedText =
+ routes.splitTunnelingSettings.getSplitTunnelingUnsupportedDialogText();
+ await expect(unsupportedText).toBeVisible();
+
+ // Close the unsupported dialog
+ await routes.splitTunnelingSettings.closeUnsupportedDialog();
+ await expect(unsupportedText).not.toBeVisible();
+ });
+
+ test('App list items should be shown even when split tunneling is unsupported', async () => {
+ // Apps should be shown if split tunneling is unsupported
+ const linuxApplications = routes.splitTunnelingSettings.getLinuxApplications();
+ await expect(linuxApplications).toHaveCount(3);
+ });
+
+ test('App list items should show unsupported dialog when clicked', async () => {
+ // Ensure clicking an application in the list makes the unsupported dialog visible
+ const linuxApplications = routes.splitTunnelingSettings.getLinuxApplications();
+ await linuxApplications.first().click();
+ const unsupportedText =
+ routes.splitTunnelingSettings.getSplitTunnelingUnsupportedDialogText();
+ await expect(unsupportedText).toBeVisible();
+
+ // Close the unsupported dialog
+ await routes.splitTunnelingSettings.closeUnsupportedDialog();
+ await expect(unsupportedText).not.toBeVisible();
+ });
+ });
+});
diff --git a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/routes-object-model.ts b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/routes-object-model.ts
index ec58260dd3..9cf32e5caf 100644
--- a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/routes-object-model.ts
+++ b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/routes-object-model.ts
@@ -10,6 +10,7 @@ import { MultihopSettingsRouteObjectModel } from './multihop-settings';
import { SelectLanguageRouteObjectModel } from './select-language';
import { SelectLocationRouteObjectModel } from './select-location';
import { SettingsRouteObjectModel } from './settings/settings-route-object-model';
+import { SplitTunnelingSettingsRouteObjectModel } from './split-tunneling-settings';
import { UdpOverTcpSettingsRouteObjectModel } from './udp-over-tcp-settings';
import { UserInterfaceSettingsRouteObjectModel } from './user-interface-settings';
import { VpnSettingsRouteObjectModel } from './vpn-settings';
@@ -29,6 +30,7 @@ export class RoutesObjectModel {
readonly udpOverTcpSettings: UdpOverTcpSettingsRouteObjectModel;
readonly multihopSettings: MultihopSettingsRouteObjectModel;
readonly daitaSettings: DaitaSettingsRouteObjectModel;
+ readonly splitTunnelingSettings: SplitTunnelingSettingsRouteObjectModel;
constructor(page: Page, utils: TestUtils) {
this.selectLanguage = new SelectLanguageRouteObjectModel(page, utils);
@@ -44,5 +46,6 @@ export class RoutesObjectModel {
this.udpOverTcpSettings = new UdpOverTcpSettingsRouteObjectModel(page, utils);
this.multihopSettings = new MultihopSettingsRouteObjectModel(page, utils);
this.daitaSettings = new DaitaSettingsRouteObjectModel(page, utils);
+ this.splitTunnelingSettings = new SplitTunnelingSettingsRouteObjectModel(page, utils);
}
}
diff --git a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/settings/selectors.ts b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/settings/selectors.ts
index abf5527eda..8cc021fdcc 100644
--- a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/settings/selectors.ts
+++ b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/settings/selectors.ts
@@ -5,4 +5,5 @@ export const createSelectors = (page: Page) => ({
daitaSettingsButton: () => page.getByRole('button', { name: 'Daita' }),
userInterfaceButton: () => page.getByRole('button', { name: 'User interface settings' }),
vpnSettingsButton: () => page.getByRole('button', { name: 'VPN settings' }),
+ splitTunnelingSettingsButton: () => page.getByRole('button', { name: 'Split tunneling' }),
});
diff --git a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/settings/settings-route-object-model.ts b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/settings/settings-route-object-model.ts
index 59b8609260..13692a2b27 100644
--- a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/settings/settings-route-object-model.ts
+++ b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/settings/settings-route-object-model.ts
@@ -34,4 +34,9 @@ export class SettingsRouteObjectModel {
await this.selectors.daitaSettingsButton().click();
await this.utils.waitForRoute(RoutePath.daitaSettings);
}
+
+ async gotoSplitTunnelingSettings() {
+ await this.selectors.splitTunnelingSettingsButton().click();
+ await this.utils.waitForRoute(RoutePath.splitTunneling);
+ }
}
diff --git a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/split-tunneling-settings/index.ts b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/split-tunneling-settings/index.ts
new file mode 100644
index 0000000000..1a647dd9e5
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/split-tunneling-settings/index.ts
@@ -0,0 +1,2 @@
+export * from './split-tunneling-settings-route-object-model';
+export * from './selectors';
diff --git a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/split-tunneling-settings/selectors.ts b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/split-tunneling-settings/selectors.ts
new file mode 100644
index 0000000000..f7208eab3a
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/split-tunneling-settings/selectors.ts
@@ -0,0 +1,17 @@
+import { type Page } from 'playwright';
+
+export const createSelectors = (page: Page) => ({
+ splitTunnelingUnsupportedDialogOpenLink: () =>
+ page.getByRole('button', {
+ name: 'Click here to learn more',
+ }),
+ splitTunnelingUnsupportedDialogCloseButton: () =>
+ page.getByRole('button', {
+ name: 'Got it!',
+ }),
+ splitTunnelingUnsupportedDialogText: () =>
+ page.getByText(
+ 'To use Split tunneling, please change to a Linux kernel version that supports cgroup v1.',
+ ),
+ linuxApplications: () => page.getByTestId('linux-applications').locator('button'),
+});
diff --git a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/split-tunneling-settings/split-tunneling-settings-route-object-model.ts b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/split-tunneling-settings/split-tunneling-settings-route-object-model.ts
new file mode 100644
index 0000000000..65589304cb
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/split-tunneling-settings/split-tunneling-settings-route-object-model.ts
@@ -0,0 +1,37 @@
+import { Page } from 'playwright';
+
+import { RoutePath } from '../../../../src/shared/routes';
+import { type TestUtils } from '../../utils';
+import { createSelectors } from './selectors';
+
+export class SplitTunnelingSettingsRouteObjectModel {
+ readonly page: Page;
+ readonly utils: TestUtils;
+ readonly selectors: ReturnType<typeof createSelectors>;
+
+ constructor(page: Page, utils: TestUtils) {
+ this.page = page;
+ this.utils = utils;
+ this.selectors = createSelectors(page);
+ }
+
+ async waitForRoute() {
+ await this.utils.waitForRoute(RoutePath.splitTunneling);
+ }
+
+ getLinuxApplications() {
+ return this.selectors.linuxApplications();
+ }
+
+ getSplitTunnelingUnsupportedDialogText() {
+ return this.selectors.splitTunnelingUnsupportedDialogText();
+ }
+
+ closeUnsupportedDialog() {
+ return this.selectors.splitTunnelingUnsupportedDialogCloseButton().click();
+ }
+
+ openUnsupportedDialog() {
+ return this.selectors.splitTunnelingUnsupportedDialogOpenLink().click();
+ }
+}