diff options
29 files changed, 415 insertions, 8 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 42ca200024..b50a80fa84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,10 @@ Line wrap the file at 100 chars. Th #### Windows - Add experimental support for Windows ARM64. +#### macOS +- Add "Apple services bypass" toggle that let's users unblock certain Apple-owned networks. + This is a temporary fix to the MacOS 15 issues where some Apple services are being blocked. + ### Changed - Never use OpenVPN as a fallback protocol when any of the following features is enabled: multihop, quantum-resistant tunnels, or DAITA. diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot index 59745be8b0..a64416dca1 100644 --- a/gui/locales/messages.pot +++ b/gui/locales/messages.pot @@ -1528,10 +1528,30 @@ msgid "App version" msgstr "" msgctxt "settings-view" +msgid "Apple services bypass" +msgstr "" + +msgctxt "settings-view" +msgid "Attention: this traffic will go outside of the VPN tunnel. Any application that tries to can bypass the VPN tunnel and send traffic to these Apple networks." +msgstr "" + +msgctxt "settings-view" +msgid "Enabling this setting allows traffic to specific Apple-owned networks to go outside of the VPN tunnel, allowing services like iMessage and FaceTime to work whilst using Mullvad." +msgstr "" + +msgctxt "settings-view" +msgid "Some Apple services have an issue where the network settings set by Mullvad get ignored, this in turn blocks certain apps." +msgstr "" + +msgctxt "settings-view" msgid "Support" msgstr "" msgctxt "settings-view" +msgid "This a temporary fix and we are currently working on a long-term solution." +msgstr "" + +msgctxt "settings-view" msgid "Update available. Install the latest app version to stay up to date." msgstr "" diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index 1c96be12bb..c09ee1b40a 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -546,6 +546,10 @@ export class DaemonRpc { await this.call<grpcTypes.DnsOptions, Empty>(this.client.setDnsOptions, dnsOptions); } + public async setAppleServicesBypass(enabled: boolean): Promise<void> { + await this.callBool<Empty>(this.client.setAppleServicesBypass, enabled); + } + public async getVersionInfo(): Promise<IAppVersionInfo> { const response = await this.callEmpty<grpcTypes.AppVersionInfo>(this.client.getVersionInfo); return response.toObject(); diff --git a/gui/src/main/default-settings.ts b/gui/src/main/default-settings.ts index e11a7434e1..62f60cf65b 100644 --- a/gui/src/main/default-settings.ts +++ b/gui/src/main/default-settings.ts @@ -81,6 +81,7 @@ export function getDefaultSettings(): ISettings { customLists: [], apiAccessMethods: getDefaultApiAccessMethods(), relayOverrides: [], + appleServicesBypass: false, }; } diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index be6c6609a0..2401e70b69 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -55,7 +55,7 @@ import NotificationController, { NotificationControllerDelegate, NotificationSender, } from './notification-controller'; -import { isMacOs13OrNewer } from './platform-version'; +import { isMacOs13OrNewer, isMacOs14p6OrNewer } from './platform-version'; import * as problemReport from './problem-report'; import { resolveBin } from './proc'; import ReconnectionBackoff from './reconnection-backoff'; @@ -774,6 +774,7 @@ class ApplicationMain navigationHistory: this.navigationHistory, currentApiAccessMethod: this.currentApiAccessMethod, isMacOs13OrNewer: isMacOs13OrNewer(), + isMacOs14p6OrNewer: isMacOs14p6OrNewer(), })); IpcMainEventChannel.map.handleGetData(async () => ({ diff --git a/gui/src/main/platform-version.ts b/gui/src/main/platform-version.ts index 027434d8d9..9b483fed65 100644 --- a/gui/src/main/platform-version.ts +++ b/gui/src/main/platform-version.ts @@ -1,24 +1,31 @@ import os from 'os'; -export function isMacOs11OrNewer() { +export function isMacOs11OrNewer(): boolean { const [major] = parseVersion(); return process.platform === 'darwin' && major >= 20; } -export function isMacOs13OrNewer() { +export function isMacOs13OrNewer(): boolean { const [major] = parseVersion(); return process.platform === 'darwin' && major >= 22; } +export function isMacOs14p6OrNewer(): boolean { + const [major, minor] = parseVersion(); + const darwin24 = major >= 24; + const darwin236 = major == 23 && minor >= 6; // 23.6 is used by macOS 14.6 + return process.platform === 'darwin' && (darwin236 || darwin24); +} + // Windows 11 has the internal version 10.0.22000+. -export function isWindows11OrNewer() { +export function isWindows11OrNewer(): boolean { const [major, minor, patch] = parseVersion(); return ( process.platform === 'win32' && (major > 10 || (major === 10 && (minor > 0 || patch >= 22000))) ); } -function parseVersion() { +function parseVersion(): number[] { return os .release() .split('.') diff --git a/gui/src/main/settings.ts b/gui/src/main/settings.ts index 03537ba581..d53219ec6d 100644 --- a/gui/src/main/settings.ts +++ b/gui/src/main/settings.ts @@ -72,6 +72,9 @@ export default class Settings implements Readonly<ISettings> { IpcMainEventChannel.settings.handleSetDnsOptions((dns) => { return this.daemonRpc.setDnsOptions(dns); }); + IpcMainEventChannel.settings.handleSetAppleServicesBypass((enabled) => { + return this.daemonRpc.setAppleServicesBypass(enabled); + }); IpcMainEventChannel.autoStart.handleSet((autoStart: boolean) => { return this.setAutoStart(autoStart); }); @@ -187,6 +190,9 @@ export default class Settings implements Readonly<ISettings> { public get relayOverrides() { return this.settingsValue.relayOverrides; } + public get appleServicesBypass() { + return this.settingsValue.appleServicesBypass; + } public get gui() { return this.guiSettings; diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index c961d07258..563899000e 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -245,6 +245,7 @@ export default class AppRenderer { this.setChangelog(initialState.changelog, initialState.forceShowChanges); this.setCurrentApiAccessMethod(initialState.currentApiAccessMethod); this.reduxActions.userInterface.setIsMacOs13OrNewer(initialState.isMacOs13OrNewer); + this.reduxActions.userInterface.setIsMacOs14p6OrNewer(initialState.isMacOs14p6OrNewer); if (initialState.macOsScrollbarVisibility !== undefined) { this.reduxActions.userInterface.setMacOsScrollbarVisibility( @@ -321,6 +322,8 @@ export default class AppRenderer { IpcRendererEventChannel.settings.updateBridgeSettings(bridgeSettings); public setDnsOptions = (dnsOptions: IDnsOptions) => IpcRendererEventChannel.settings.setDnsOptions(dnsOptions); + public setAppleServicesBypass = (enabled: boolean) => + IpcRendererEventChannel.settings.setAppleServicesBypass(enabled); public clearAccountHistory = () => IpcRendererEventChannel.accountHistory.clear(); public setAutoConnect = (value: boolean) => IpcRendererEventChannel.guiSettings.setAutoConnect(value); @@ -826,6 +829,7 @@ export default class AppRenderer { reduxSettings.updateWireguardDaita(newSettings.tunnelOptions.wireguard.daita); reduxSettings.updateBridgeState(newSettings.bridgeState); reduxSettings.updateDnsOptions(newSettings.tunnelOptions.dns); + reduxSettings.updateAppleServicesBypass(newSettings.appleServicesBypass); reduxSettings.updateSplitTunnelingState(newSettings.splitTunnel.enableExclusions); reduxSettings.updateObfuscationSettings(newSettings.obfuscationSettings); reduxSettings.updateCustomLists(newSettings.customLists); diff --git a/gui/src/renderer/components/Settings.tsx b/gui/src/renderer/components/Settings.tsx index 211679a76c..98d36fb9fd 100644 --- a/gui/src/renderer/components/Settings.tsx +++ b/gui/src/renderer/components/Settings.tsx @@ -7,10 +7,20 @@ import { useAppContext } from '../context'; import { useHistory } from '../lib/history'; import { RoutePath } from '../lib/routes'; import { useSelector } from '../redux/store'; -import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup'; +import { + AriaDescribed, + AriaDescription, + AriaDescriptionGroup, + AriaDetails, + AriaInput, + AriaInputGroup, + AriaLabel, +} from './AriaGroup'; import * as Cell from './cell'; +import InfoButton from './InfoButton'; import { BackAction } from './KeyboardNavigation'; import { Layout, SettingsContainer } from './Layout'; +import { ModalMessage } from './Modal'; import { NavigationBar, NavigationContainer, NavigationItems, TitleBarItem } from './NavigationBar'; import SettingsHeader, { HeaderTitle } from './SettingsHeader'; import { @@ -28,6 +38,8 @@ export default function Support() { const connectedToDaemon = useSelector((state) => state.userInterface.connectedToDaemon); const isMacOs13OrNewer = useSelector((state) => state.userInterface.isMacOs13OrNewer); + const isMacOs14p6OrNewer = useSelector((state) => state.userInterface.isMacOs14p6OrNewer); + const showSubSettings = loginState.type === 'ok' && connectedToDaemon; const showSplitTunneling = window.env.platform !== 'darwin' || isMacOs13OrNewer; @@ -77,6 +89,12 @@ export default function Support() { <ApiAccessMethodsButton /> </Cell.Group> + {isMacOs14p6OrNewer ? ( + <Cell.Group> + <AppleServicesBypass /> + </Cell.Group> + ) : null} + <Cell.Group> <SupportButton /> <AppVersionButton /> @@ -227,6 +245,54 @@ function SupportButton() { ); } +function AppleServicesBypass() { + const { setAppleServicesBypass } = useAppContext(); + const appleServicesBypass = useSelector((state) => state.settings.appleServicesBypass); + + return ( + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('settings-view', 'Apple services bypass')} + </Cell.InputLabel> + </AriaLabel> + <AriaDetails> + <InfoButton> + <ModalMessage> + {messages.pgettext( + 'settings-view', + 'Some Apple services have an issue where the network settings set by Mullvad get ignored, this in turn blocks certain apps.', + )} + </ModalMessage> + <ModalMessage> + {messages.pgettext( + 'settings-view', + 'Enabling this setting allows traffic to specific Apple-owned networks to go outside of the VPN tunnel, allowing services like iMessage and FaceTime to work whilst using Mullvad.', + )} + </ModalMessage> + <ModalMessage> + {messages.pgettext( + 'settings-view', + 'Attention: this traffic will go outside of the VPN tunnel. Any application that tries to can bypass the VPN tunnel and send traffic to these Apple networks.', + )} + </ModalMessage> + <ModalMessage> + {messages.pgettext( + 'settings-view', + 'This a temporary fix and we are currently working on a long-term solution.', + )} + </ModalMessage> + </InfoButton> + </AriaDetails> + <AriaInput> + <Cell.Switch isOn={appleServicesBypass} onChange={setAppleServicesBypass} /> + </AriaInput> + </Cell.Container> + </AriaInputGroup> + ); +} + function DebugButton() { const history = useHistory(); const navigate = useCallback(() => history.push(RoutePath.debug), [history]); diff --git a/gui/src/renderer/redux/settings/actions.ts b/gui/src/renderer/redux/settings/actions.ts index d2a3fb1c4a..80ac3707c1 100644 --- a/gui/src/renderer/redux/settings/actions.ts +++ b/gui/src/renderer/redux/settings/actions.ts @@ -93,6 +93,11 @@ export interface IUpdateDnsOptionsAction { dns: IDnsOptions; } +export interface ISetAppleServicesBypass { + type: 'SET_APPLE_SERVICES_BYPASS'; + enabled: boolean; +} + export interface IUpdateSplitTunnelingStateAction { type: 'UPDATE_SPLIT_TUNNELING_STATE'; enabled: boolean; @@ -145,6 +150,7 @@ export type SettingsAction = | IUpdateWireguardDaitaAction | IUpdateAutoStartAction | IUpdateDnsOptionsAction + | ISetAppleServicesBypass | IUpdateSplitTunnelingStateAction | ISetSplitTunnelingApplicationsAction | ISetObfuscationSettings @@ -273,6 +279,13 @@ function updateDnsOptions(dns: IDnsOptions): IUpdateDnsOptionsAction { }; } +function updateAppleServicesBypass(enabled: boolean): ISetAppleServicesBypass { + return { + type: 'SET_APPLE_SERVICES_BYPASS', + enabled, + }; +} + function updateSplitTunnelingState(enabled: boolean): IUpdateSplitTunnelingStateAction { return { type: 'UPDATE_SPLIT_TUNNELING_STATE', @@ -343,6 +356,7 @@ export default { updateWireguardDaita, updateAutoStart, updateDnsOptions, + updateAppleServicesBypass, updateSplitTunnelingState, setSplitTunnelingApplications, updateObfuscationSettings, diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts index 6eb595467b..7f806c248c 100644 --- a/gui/src/renderer/redux/settings/reducers.ts +++ b/gui/src/renderer/redux/settings/reducers.ts @@ -114,6 +114,7 @@ export interface ISettingsReduxState { daita?: IDaitaSettings; }; dns: IDnsOptions; + appleServicesBypass: boolean; splitTunneling: boolean; splitTunnelingApplications: ISplitTunnelingApplication[]; obfuscationSettings: ObfuscationSettings; @@ -181,6 +182,7 @@ const initialState: ISettingsReduxState = { addresses: [], }, }, + appleServicesBypass: false, splitTunneling: false, splitTunnelingApplications: [], obfuscationSettings: { @@ -310,6 +312,12 @@ export default function ( dns: action.dns, }; + case 'SET_APPLE_SERVICES_BYPASS': + return { + ...state, + appleServicesBypass: action.enabled, + }; + case 'UPDATE_SPLIT_TUNNELING_STATE': return { ...state, diff --git a/gui/src/renderer/redux/userinterface/actions.ts b/gui/src/renderer/redux/userinterface/actions.ts index 238835318e..a5ee138464 100644 --- a/gui/src/renderer/redux/userinterface/actions.ts +++ b/gui/src/renderer/redux/userinterface/actions.ts @@ -61,6 +61,11 @@ export interface ISetIsMacOs13OrNewer { isMacOs13OrNewer: boolean; } +export interface ISetIsMacOs14p6OrNewer { + type: 'SET_IS_MACOS14_6_OR_NEWER'; + isMacOs14p6OrNewer: boolean; +} + export type UserInterfaceAction = | IUpdateLocaleAction | IUpdateWindowArrowPositionAction @@ -73,7 +78,8 @@ export type UserInterfaceAction = | ISetForceShowChanges | ISetIsPerformingPostUpgrade | ISetSelectLocationView - | ISetIsMacOs13OrNewer; + | ISetIsMacOs13OrNewer + | ISetIsMacOs14p6OrNewer; function updateLocale(locale: string): IUpdateLocaleAction { return { @@ -160,6 +166,13 @@ function setIsMacOs13OrNewer(isMacOs13OrNewer: boolean): ISetIsMacOs13OrNewer { }; } +function setIsMacOs14p6OrNewer(isMacOs14p6OrNewer: boolean): ISetIsMacOs14p6OrNewer { + return { + type: 'SET_IS_MACOS14_6_OR_NEWER', + isMacOs14p6OrNewer, + }; +} + export default { updateLocale, updateWindowArrowPosition, @@ -173,4 +186,5 @@ export default { setIsPerformingPostUpgrade, setSelectLocationView, setIsMacOs13OrNewer, + setIsMacOs14p6OrNewer, }; diff --git a/gui/src/renderer/redux/userinterface/reducers.ts b/gui/src/renderer/redux/userinterface/reducers.ts index 89427e9b06..b245522cec 100644 --- a/gui/src/renderer/redux/userinterface/reducers.ts +++ b/gui/src/renderer/redux/userinterface/reducers.ts @@ -16,6 +16,7 @@ export interface IUserInterfaceReduxState { isPerformingPostUpgrade: boolean; selectLocationView: LocationType; isMacOs13OrNewer: boolean; + isMacOs14p6OrNewer: boolean; } const initialState: IUserInterfaceReduxState = { @@ -30,6 +31,7 @@ const initialState: IUserInterfaceReduxState = { isPerformingPostUpgrade: false, selectLocationView: LocationType.exit, isMacOs13OrNewer: true, + isMacOs14p6OrNewer: true, }; export default function ( @@ -88,6 +90,12 @@ export default function ( isMacOs13OrNewer: action.isMacOs13OrNewer, }; + case 'SET_IS_MACOS14_6_OR_NEWER': + return { + ...state, + isMacOs14p6OrNewer: action.isMacOs14p6OrNewer, + }; + default: return state; } diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts index 33ff6bb8ce..4943bee933 100644 --- a/gui/src/shared/daemon-rpc-types.ts +++ b/gui/src/shared/daemon-rpc-types.ts @@ -439,6 +439,7 @@ export interface ISettings { customLists: CustomLists; apiAccessMethods: ApiAccessMethodSettings; relayOverrides: Array<RelayOverride>; + appleServicesBypass: boolean; } export type BridgeState = 'auto' | 'on' | 'off'; diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts index 4e08e7109d..8fa7c8c306 100644 --- a/gui/src/shared/ipc-schema.ts +++ b/gui/src/shared/ipc-schema.ts @@ -77,6 +77,7 @@ export interface IAppStateSnapshot { navigationHistory?: IHistoryObject; currentApiAccessMethod?: AccessMethodSetting; isMacOs13OrNewer: boolean; + isMacOs14p6OrNewer: boolean; } // The different types of requests are: @@ -186,6 +187,7 @@ export const ipcSchema = { setRelaySettings: invoke<RelaySettings, void>(), updateBridgeSettings: invoke<BridgeSettings, void>(), setDnsOptions: invoke<IDnsOptions, void>(), + setAppleServicesBypass: invoke<boolean, void>(), setObfuscationSettings: invoke<ObfuscationSettings, void>(), addApiAccessMethod: invoke<NewAccessMethodSetting, string>(), updateApiAccessMethod: invoke<AccessMethodSetting, void>(), diff --git a/gui/test/e2e/setup/main.ts b/gui/test/e2e/setup/main.ts index 0dd8a4f1f0..8b1d6afcfe 100644 --- a/gui/test/e2e/setup/main.ts +++ b/gui/test/e2e/setup/main.ts @@ -175,6 +175,7 @@ class ApplicationMain { navigationHistory: undefined, scrollPositions: {}, isMacOs13OrNewer: true, + isMacOs14p6OrNewer: true, })); IpcMainEventChannel.guiSettings.handleSetPreferredLocale((locale) => { diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index b0e9ea7d58..c2e38f11b6 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -380,6 +380,9 @@ pub enum DaemonCommand { ExportJsonSettings(ResponseTx<String, settings::patch::Error>), /// Request the current feature indicators. GetFeatureIndicators(oneshot::Sender<FeatureIndicators>), + /// Set if we should leak traffic to Apple services. + #[cfg(target_os = "macos")] + SetAppleServicesBypass(ResponseTx<(), settings::Error>, bool), } /// All events that can happen in the daemon. Sent from various threads and exposed interfaces. @@ -767,6 +770,8 @@ impl Daemon { reset_firewall: *target_state != TargetState::Secured, #[cfg(any(windows, target_os = "android", target_os = "macos"))] exclude_paths, + #[cfg(target_os = "macos")] + apple_services_bypass: settings.apple_services_bypass, }, parameters_generator.clone(), log_dir, @@ -1334,6 +1339,10 @@ impl Daemon { ApplyJsonSettings(tx, blob) => self.on_apply_json_settings(tx, blob).await, ExportJsonSettings(tx) => self.on_export_json_settings(tx), GetFeatureIndicators(tx) => self.on_get_feature_indicators(tx), + #[cfg(target_os = "macos")] + SetAppleServicesBypass(tx, enabled) => { + self.on_set_apple_services_bypass(tx, enabled).await + } } } @@ -2799,6 +2808,12 @@ impl Daemon { let (tx, _rx) = oneshot::channel(); self.send_tunnel_command(TunnelCommand::AllowLan(self.settings.allow_lan, tx)); + #[cfg(target_os = "macos")] + self.send_tunnel_command(TunnelCommand::AppleServicesBypass( + oneshot::channel().0, + self.settings.apple_services_bypass, + )); + let (tx, _rx) = oneshot::channel(); let dns = dns::addresses_from_options(&self.settings.tunnel_options.dns_options); self.send_tunnel_command(TunnelCommand::Dns(dns, tx)); @@ -2943,6 +2958,37 @@ impl Daemon { Self::oneshot_send(tx, feature_indicators, "get_feature_indicators response"); } + #[cfg(target_os = "macos")] + async fn on_set_apple_services_bypass( + &mut self, + tx: ResponseTx<(), settings::Error>, + enabled: bool, + ) { + let result = self + .settings + .update(|settings| settings.apple_services_bypass = enabled) + .await; + + match result { + Ok(settings_changed) => { + if settings_changed { + self.send_tunnel_command(TunnelCommand::AppleServicesBypass( + oneshot_map(tx, |tx, ()| { + Self::oneshot_send(tx, Ok(()), "set_apple_services_bypass response"); + }), + enabled, + )); + } else { + Self::oneshot_send(tx, Ok(()), "set_apple_services_bypass response"); + } + } + Err(e) => { + log::error!("{}", e.display_chain_with_msg("Unable to save settings")); + Self::oneshot_send(tx, Err(e), "set_apple_services_bypass response"); + } + } + } + /// Set the target state of the client. If it changed trigger the operations needed to /// progress towards that state. /// Returns a bool representing whether a state change was initiated. diff --git a/mullvad-daemon/src/management_interface.rs b/mullvad-daemon/src/management_interface.rs index 331754d1f2..2d2c81ff89 100644 --- a/mullvad-daemon/src/management_interface.rs +++ b/mullvad-daemon/src/management_interface.rs @@ -1070,6 +1070,26 @@ impl ManagementService for ManagementServiceImpl { Ok(Response::new(feature_indicators)) } + + #[cfg(not(daita))] + async fn set_enable_daita(&self, _: Request<bool>) -> ServiceResult<()> { + Ok(Response::new(())) + } + + #[cfg(not(target_os = "macos"))] + async fn set_apple_services_bypass(&self, _: Request<bool>) -> ServiceResult<()> { + Ok(Response::new(())) + } + + #[cfg(target_os = "macos")] + async fn set_apple_services_bypass(&self, request: Request<bool>) -> ServiceResult<()> { + log::debug!("set_apple_services_bypass"); + let enabled = request.into_inner(); + let (tx, rx) = oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::SetAppleServicesBypass(tx, enabled))?; + self.wait_for_result(rx).await??; + Ok(Response::new(())) + } } impl ManagementServiceImpl { diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto index d701fc8e4f..eb84f6a021 100644 --- a/mullvad-management-interface/proto/management_interface.proto +++ b/mullvad-management-interface/proto/management_interface.proto @@ -124,6 +124,9 @@ service ManagementService { // Get current feature indicators rpc GetFeatureIndicators(google.protobuf.Empty) returns (FeatureIndicators) {} + + // Set if we should leak traffic to Apple services. + rpc SetAppleServicesBypass(google.protobuf.BoolValue) returns (google.protobuf.Empty) {} } message UUID { string value = 1; } @@ -452,6 +455,7 @@ message Settings { CustomListSettings custom_lists = 11; ApiAccessMethodSettings api_access_methods = 12; repeated RelayOverride relay_overrides = 13; + bool apple_services_bypass = 14; } message RelayOverride { diff --git a/mullvad-management-interface/src/types/conversions/settings.rs b/mullvad-management-interface/src/types/conversions/settings.rs index d3be7f75a2..262e9f4233 100644 --- a/mullvad-management-interface/src/types/conversions/settings.rs +++ b/mullvad-management-interface/src/types/conversions/settings.rs @@ -54,6 +54,11 @@ impl From<&mullvad_types::settings::Settings> for proto::Settings { .cloned() .map(proto::RelayOverride::from) .collect(), + + #[cfg(target_os = "macos")] + apple_services_bypass: settings.apple_services_bypass, + #[cfg(not(target_os = "macos"))] + apple_services_bypass: false, } } } @@ -198,6 +203,8 @@ impl TryFrom<proto::Settings> for mullvad_types::settings::Settings { api_access_methods: mullvad_types::access_method::Settings::try_from( api_access_methods_settings, )?, + #[cfg(target_os = "macos")] + apple_services_bypass: settings.apple_services_bypass, }) } } diff --git a/mullvad-types/src/settings/mod.rs b/mullvad-types/src/settings/mod.rs index 038b0db803..8a90aa1f23 100644 --- a/mullvad-types/src/settings/mod.rs +++ b/mullvad-types/src/settings/mod.rs @@ -81,6 +81,9 @@ pub struct Settings { pub api_access_methods: access_method::Settings, /// If the daemon should allow communication with private (LAN) networks. pub allow_lan: bool, + /// If the daemon should allow communication with apple push notification services. + #[cfg(target_os = "macos")] + pub apple_services_bypass: bool, /// Extra level of kill switch. When this setting is on, the disconnected state will block /// the firewall to not allow any traffic in or out. pub block_when_disconnected: bool, @@ -208,6 +211,8 @@ impl Default for Settings { #[cfg(any(windows, target_os = "android", target_os = "macos"))] split_tunnel: SplitTunnelSettings::default(), settings_version: CURRENT_SETTINGS_VERSION, + #[cfg(target_os = "macos")] + apple_services_bypass: false, } } } diff --git a/talpid-core/src/firewall/macos.rs b/talpid-core/src/firewall/macos.rs index 78f72ab647..ef26ebb97f 100644 --- a/talpid-core/src/firewall/macos.rs +++ b/talpid-core/src/firewall/macos.rs @@ -1,7 +1,7 @@ use super::{FirewallArguments, FirewallPolicy}; use ipnetwork::IpNetwork; use libc::{c_int, sysctlbyname}; -use pfctl::{DropAction, FilterRuleAction, Uid}; +use pfctl::{DropAction, Endpoint, FilterRuleAction, Uid}; use std::{ env, io, net::{IpAddr, Ipv4Addr}, @@ -138,6 +138,7 @@ impl Firewall { new_filter_rules.append(&mut self.get_allow_loopback_rules()?); new_filter_rules.append(&mut self.get_allow_dhcp_client_rules()?); new_filter_rules.append(&mut self.get_allow_ndp_rules()?); + new_filter_rules.append(&mut self.get_policy_specific_rules(policy)?); let return_out_rule = self @@ -202,6 +203,7 @@ impl Firewall { allowed_endpoint, allowed_tunnel_traffic, redirect_interface, + apple_services_bypass, } => { let mut rules = vec![self.get_allow_relay_rule(peer_endpoint)?]; rules.push(self.get_allowed_endpoint_rule(allowed_endpoint)?); @@ -238,6 +240,10 @@ impl Firewall { rules.append(&mut self.get_allow_lan_rules()?); } + if *apple_services_bypass { + rules.append(&mut self.get_apple_services_bypass_rules()?); + } + Ok(rules) } FirewallPolicy::Connected { @@ -246,6 +252,7 @@ impl Firewall { allow_lan, dns_config, redirect_interface, + apple_services_bypass, } => { let mut rules = vec![]; @@ -270,6 +277,10 @@ impl Firewall { rules.append(&mut self.get_allow_lan_rules()?); } + if *apple_services_bypass { + rules.append(&mut self.get_apple_services_bypass_rules()?); + } + if let Some(redirect_interface) = redirect_interface { enable_forwarding(); @@ -288,6 +299,7 @@ impl Firewall { FirewallPolicy::Blocked { allow_lan, allowed_endpoint, + apple_services_bypass, .. } => { let mut rules = Vec::new(); @@ -301,6 +313,10 @@ impl Firewall { rules.append(&mut self.get_allow_lan_rules()?); } + if *apple_services_bypass { + rules.append(&mut self.get_apple_services_bypass_rules()?); + } + Ok(rules) } } @@ -566,6 +582,45 @@ impl Firewall { Ok(rules) } + /// Generate rules that allow traffic to the networks required for Apple push notification + /// services to work. This is a hack to get around the fact that apple services in MacOS 15 has + /// a bug where they don't respect the routing table. + /// + /// All allowed networks are part of apple-owned IP subnets. + fn get_apple_services_bypass_rules(&self) -> Result<Vec<pfctl::FilterRule>> { + // https://support.apple.com/en-us/102266 + let apple_networks: &[IpNetwork] = &[ + "17.249.0.0/16".parse().unwrap(), + "17.252.0.0/16".parse().unwrap(), + "17.57.144.0/22".parse().unwrap(), + "17.188.128.0/18".parse().unwrap(), + "17.188.20.0/23".parse().unwrap(), + "2620:149:a44::/48".parse().unwrap(), + "2403:300:a42::/48".parse().unwrap(), + "2403:300:a51::/48".parse().unwrap(), + "2a01:b740:a42::/48".parse().unwrap(), + ]; + + let apple_ports: &[u16] = &[443, 2197, 5223]; + + let mut rules = vec![]; + for &net in apple_networks { + for &port in apple_ports { + let mut rule_builder = self.create_rule_builder(FilterRuleAction::Pass); + rule_builder.quick(true); + let allow_out = rule_builder + .quick(true) + .direction(pfctl::Direction::Out) + .from(pfctl::Ip::Any) + .to(Endpoint::new(pfctl::Ip::from(net), port)) + .keep_state(pfctl::StatePolicy::Keep) + .build()?; + rules.push(allow_out); + } + } + Ok(rules) + } + fn get_split_tunnel_rules( &self, from_interface: &str, diff --git a/talpid-core/src/firewall/mod.rs b/talpid-core/src/firewall/mod.rs index 8f36e905ff..2596f330e3 100644 --- a/talpid-core/src/firewall/mod.rs +++ b/talpid-core/src/firewall/mod.rs @@ -94,6 +94,10 @@ pub enum FirewallPolicy { /// Interface to redirect (VPN tunnel) traffic to #[cfg(target_os = "macos")] redirect_interface: Option<String>, + + /// Flag setting if we should leak traffic to apple services. + #[cfg(target_os = "macos")] + apple_services_bypass: bool, }, /// Allow traffic only to server and over tunnel interface @@ -110,6 +114,10 @@ pub enum FirewallPolicy { /// Interface to redirect (VPN tunnel) traffic to #[cfg(target_os = "macos")] redirect_interface: Option<String>, + + /// Flag setting if we should leak traffic to apple services. + #[cfg(target_os = "macos")] + apple_services_bypass: bool, }, /// Block all network traffic in and out from the computer. @@ -122,6 +130,10 @@ pub enum FirewallPolicy { /// be redirected to `127.0.0.1:$dns_redirect_port`. #[cfg(target_os = "macos")] dns_redirect_port: u16, + + /// Flag setting if we should leak traffic to apple services. + #[cfg(target_os = "macos")] + apple_services_bypass: bool, }, } diff --git a/talpid-core/src/tunnel_state_machine/connected_state.rs b/talpid-core/src/tunnel_state_machine/connected_state.rs index a5ca3395a4..abb97d282d 100644 --- a/talpid-core/src/tunnel_state_machine/connected_state.rs +++ b/talpid-core/src/tunnel_state_machine/connected_state.rs @@ -142,6 +142,8 @@ impl ConnectedState { dns_config: Self::resolve_dns(&self.metadata, shared_values), #[cfg(target_os = "macos")] redirect_interface, + #[cfg(target_os = "macos")] + apple_services_bypass: shared_values.apple_services_bypass, } } @@ -371,6 +373,25 @@ impl ConnectedState { } SameState(self) } + + #[cfg(target_os = "macos")] + Some(TunnelCommand::AppleServicesBypass(complete_tx, apple_services_bypass)) => { + let consequence = if shared_values.set_apple_services_bypass(apple_services_bypass) + { + match self.set_firewall_policy(shared_values) { + Ok(()) => SameState(self), + Err(error) => self.disconnect( + shared_values, + AfterDisconnect::Block(ErrorStateCause::SetFirewallPolicyError(error)), + ), + } + } else { + SameState(self) + }; + + let _ = complete_tx.send(()); + consequence + } } } diff --git a/talpid-core/src/tunnel_state_machine/connecting_state.rs b/talpid-core/src/tunnel_state_machine/connecting_state.rs index 09ba4f18e1..e5d9dcb6cb 100644 --- a/talpid-core/src/tunnel_state_machine/connecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/connecting_state.rs @@ -172,6 +172,8 @@ impl ConnectingState { allowed_tunnel_traffic, #[cfg(target_os = "macos")] redirect_interface, + #[cfg(target_os = "macos")] + apple_services_bypass: shared_values.apple_services_bypass, }; shared_values .firewall @@ -544,6 +546,17 @@ impl ConnectingState { } SameState(self) } + #[cfg(target_os = "macos")] + Some(TunnelCommand::AppleServicesBypass(complete_tx, apple_services_bypass)) => { + let consequence = if shared_values.set_apple_services_bypass(apple_services_bypass) + { + self.reset_firewall(shared_values) + } else { + SameState(self) + }; + let _ = complete_tx.send(()); + consequence + } } } diff --git a/talpid-core/src/tunnel_state_machine/disconnected_state.rs b/talpid-core/src/tunnel_state_machine/disconnected_state.rs index 9da6520f61..f7bc5e712b 100644 --- a/talpid-core/src/tunnel_state_machine/disconnected_state.rs +++ b/talpid-core/src/tunnel_state_machine/disconnected_state.rs @@ -78,6 +78,8 @@ impl DisconnectedState { allowed_endpoint: Some(shared_values.allowed_endpoint.clone()), #[cfg(target_os = "macos")] dns_redirect_port: shared_values.filtering_resolver.listening_port(), + #[cfg(target_os = "macos")] + apple_services_bypass: shared_values.apple_services_bypass, }; shared_values.firewall.apply_policy(policy).map_err(|e| { @@ -230,6 +232,14 @@ impl TunnelState for DisconnectedState { let _ = result_tx.send(shared_values.set_exclude_paths(paths).map(|_| ())); SameState(self) } + #[cfg(target_os = "macos")] + Some(TunnelCommand::AppleServicesBypass(complete_tx, apple_services_bypass)) => { + if shared_values.set_apple_services_bypass(apple_services_bypass) { + Self::set_firewall_policy(shared_values, false); + } + let _ = complete_tx.send(()); + SameState(self) + } None => { Self::reset_dns(shared_values); Finished diff --git a/talpid-core/src/tunnel_state_machine/disconnecting_state.rs b/talpid-core/src/tunnel_state_machine/disconnecting_state.rs index 4a108788e1..16bed626e1 100644 --- a/talpid-core/src/tunnel_state_machine/disconnecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/disconnecting_state.rs @@ -92,6 +92,12 @@ impl DisconnectingState { let _ = result_tx.send(shared_values.set_exclude_paths(paths).map(|_| ())); AfterDisconnect::Nothing } + #[cfg(target_os = "macos")] + Some(TunnelCommand::AppleServicesBypass(complete_tx, apple_services_bypass)) => { + let _ = shared_values.set_apple_services_bypass(apple_services_bypass); + let _ = complete_tx.send(()); + AfterDisconnect::Nothing + } }, AfterDisconnect::Block(reason) => match command { Some(TunnelCommand::AllowLan(allow_lan, complete_tx)) => { @@ -149,6 +155,12 @@ impl DisconnectingState { let _ = result_tx.send(shared_values.set_exclude_paths(paths).map(|_| ())); AfterDisconnect::Block(reason) } + #[cfg(target_os = "macos")] + Some(TunnelCommand::AppleServicesBypass(complete_tx, apple_services_bypass)) => { + let _ = shared_values.set_apple_services_bypass(apple_services_bypass); + let _ = complete_tx.send(()); + AfterDisconnect::Block(reason) + } None => AfterDisconnect::Block(reason), }, AfterDisconnect::Reconnect(retry_attempt) => match command { @@ -207,6 +219,12 @@ impl DisconnectingState { let _ = result_tx.send(shared_values.set_exclude_paths(paths).map(|_| ())); AfterDisconnect::Reconnect(retry_attempt) } + #[cfg(target_os = "macos")] + Some(TunnelCommand::AppleServicesBypass(complete_tx, apple_services_bypass)) => { + let _ = shared_values.set_apple_services_bypass(apple_services_bypass); + let _ = complete_tx.send(()); + AfterDisconnect::Reconnect(retry_attempt) + } }, }; diff --git a/talpid-core/src/tunnel_state_machine/error_state.rs b/talpid-core/src/tunnel_state_machine/error_state.rs index eeaf48956b..14885b0a60 100644 --- a/talpid-core/src/tunnel_state_machine/error_state.rs +++ b/talpid-core/src/tunnel_state_machine/error_state.rs @@ -78,6 +78,8 @@ impl ErrorState { allowed_endpoint: Some(shared_values.allowed_endpoint.clone()), #[cfg(target_os = "macos")] dns_redirect_port: shared_values.filtering_resolver.listening_port(), + #[cfg(target_os = "macos")] + apple_services_bypass: shared_values.apple_services_bypass, }; #[cfg(target_os = "linux")] @@ -235,6 +237,18 @@ impl TunnelState for ErrorState { let _ = result_tx.send(shared_values.set_exclude_paths(paths).map(|_| ())); SameState(self) } + #[cfg(target_os = "macos")] + Some(TunnelCommand::AppleServicesBypass(complete_tx, apple_services_bypass)) => { + let consequence = if shared_values.set_apple_services_bypass(apple_services_bypass) + { + let _ = Self::set_firewall_policy(shared_values); + SameState(self) + } else { + SameState(self) + }; + let _ = complete_tx.send(()); + consequence + } } } } diff --git a/talpid-core/src/tunnel_state_machine/mod.rs b/talpid-core/src/tunnel_state_machine/mod.rs index e0ef07850d..052dd74a49 100644 --- a/talpid-core/src/tunnel_state_machine/mod.rs +++ b/talpid-core/src/tunnel_state_machine/mod.rs @@ -104,6 +104,9 @@ pub struct InitialTunnelState { /// Apps to exclude from the tunnel. #[cfg(target_os = "android")] pub exclude_paths: Vec<String>, + /// Whether we should leak traffic to Apple services. + #[cfg(target_os = "macos")] + pub apple_services_bypass: bool, } /// Identifiers for various network resources that should be unique to a given instance of a tunnel @@ -214,6 +217,9 @@ pub enum TunnelCommand { oneshot::Sender<Result<(), split_tunnel::Error>>, Vec<String>, ), + /// Set if we should leak traffic to Apple services. + #[cfg(target_os = "macos")] + AppleServicesBypass(oneshot::Sender<()>, bool), } type TunnelCommandReceiver = stream::Fuse<mpsc::UnboundedReceiver<TunnelCommand>>; @@ -383,6 +389,8 @@ impl TunnelStateMachine { connectivity_check_was_enabled: None, #[cfg(target_os = "macos")] filtering_resolver, + #[cfg(target_os = "macos")] + apple_services_bypass: args.settings.apple_services_bypass, }; tokio::task::spawn_blocking(move || { @@ -486,6 +494,9 @@ struct SharedTunnelStateValues { /// Filtering resolver handle #[cfg(target_os = "macos")] filtering_resolver: crate::resolver::ResolverHandle, + + #[cfg(target_os = "macos")] + apple_services_bypass: bool, } impl SharedTunnelStateValues { @@ -652,6 +663,16 @@ impl SharedTunnelStateValues { } } } + + #[cfg(target_os = "macos")] + pub fn set_apple_services_bypass(&mut self, apple_services_bypass: bool) -> bool { + if self.apple_services_bypass != apple_services_bypass { + self.apple_services_bypass = apple_services_bypass; + true + } else { + false + } + } } /// Asynchronous result of an attempt to progress a state. |
