summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2023-02-01 17:40:05 +0100
committerDavid Lönnhager <david.l@mullvad.net>2023-02-01 17:40:05 +0100
commit423d3e7ae0f6a54a6ea4506df4ea9ad2df9b6eab (patch)
tree0b2f6ce30a7e1e374da6a1e92527f545195551f4
parentc646045b9f25e31be377096db91db0f7907510d3 (diff)
parentd921f649f7953b1cd0d930a73461db4909674da8 (diff)
downloadmullvadvpn-423d3e7ae0f6a54a6ea4506df4ea9ad2df9b6eab.tar.xz
mullvadvpn-423d3e7ae0f6a54a6ea4506df4ea9ad2df9b6eab.zip
Merge branch 'macos-fix-daemon-approval'
-rw-r--r--CHANGELOG.md1
-rw-r--r--Cargo.lock19
-rwxr-xr-xdist-assets/pkg-scripts/postinstall2
-rw-r--r--gui/locales/messages.pot7
-rw-r--r--gui/src/main/index.ts35
-rw-r--r--gui/src/main/user-interface.ts14
-rw-r--r--gui/src/renderer/app.tsx13
-rw-r--r--gui/src/renderer/components/ErrorView.tsx29
-rw-r--r--gui/src/renderer/components/Launch.tsx64
-rw-r--r--gui/src/renderer/redux/userinterface/actions.ts14
-rw-r--r--gui/src/renderer/redux/userinterface/reducers.ts5
-rw-r--r--gui/src/shared/ipc-schema.ts3
-rw-r--r--mullvad-daemon/Cargo.toml3
-rw-r--r--mullvad-daemon/src/cli.rs12
-rw-r--r--mullvad-daemon/src/macos_launch_daemon.rs97
-rw-r--r--mullvad-daemon/src/main.rs17
16 files changed, 327 insertions, 8 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 73eaf0959a..f063c9333f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -39,6 +39,7 @@ Line wrap the file at 100 chars. Th
- Improved reliability of the connectivity check workaround by adding an extra captive portal check
domain
- Show "Mullvad VPN" in the Login Items UI instead of "Amagicom AB".
+- Detect whether users need to approve the launch daemon in the Login Items UI.
## [2023.1-beta1] - 2023-01-26
diff --git a/Cargo.lock b/Cargo.lock
index 64a2080e5d..98cb37217c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1478,6 +1478,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd"
[[package]]
+name = "malloc_buf"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
name = "maplit"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1659,6 +1668,7 @@ dependencies = [
"mullvad-types",
"mullvad-version",
"nix 0.23.1",
+ "objc",
"parking_lot 0.11.2",
"rand 0.8.5",
"regex",
@@ -1995,6 +2005,15 @@ dependencies = [
]
[[package]]
+name = "objc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
+dependencies = [
+ "malloc_buf",
+]
+
+[[package]]
name = "object"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/dist-assets/pkg-scripts/postinstall b/dist-assets/pkg-scripts/postinstall
index 3bf5b91f49..716bc2b2e6 100755
--- a/dist-assets/pkg-scripts/postinstall
+++ b/dist-assets/pkg-scripts/postinstall
@@ -10,6 +10,8 @@ exec 2>&1 > $LOG_DIR/postinstall.log
echo "Running postinstall at $(date)"
INSTALL_DIR=$2
+# NOTE: This path must be kept in sync with the path defined
+# in mullvad-daemon/src/macos_launch_daemon.rs
DAEMON_PLIST_PATH="/Library/LaunchDaemons/net.mullvad.daemon.plist"
DAEMON_PLIST=$(cat <<-EOM
diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot
index 049206b2be..d2eef0b96f 100644
--- a/gui/locales/messages.pot
+++ b/gui/locales/messages.pot
@@ -147,6 +147,9 @@ msgstr ""
msgid "Filter"
msgstr ""
+msgid "Go to System Settings"
+msgstr ""
+
#. This is a button which closes a dialog.
msgid "Got it!"
msgstr ""
@@ -619,6 +622,10 @@ msgctxt "launch-view"
msgid "Connecting to Mullvad system service..."
msgstr ""
+msgctxt "launch-view"
+msgid "Permission for the Mullvad VPN service has been revoked. Please go to System Settings and allow Mullvad VPN under the “Allow in the Background” setting."
+msgstr ""
+
#. This is a warning message shown when the app is blocking the users
#. internet connection while logged out.
#. Available placeholder:
diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts
index 7503c24e01..40f9054958 100644
--- a/gui/src/main/index.ts
+++ b/gui/src/main/index.ts
@@ -1,4 +1,4 @@
-import { exec } from 'child_process';
+import { exec, execFile } from 'child_process';
import { app, nativeTheme, session, shell, systemPreferences } from 'electron';
import fs from 'fs';
import * as path from 'path';
@@ -45,6 +45,7 @@ import NotificationController, {
NotificationSender,
} from './notification-controller';
import * as problemReport from './problem-report';
+import { resolveBin } from './proc';
import ReconnectionBackoff from './reconnection-backoff';
import Settings, { SettingsDelegate } from './settings';
import TunnelStateHandler, {
@@ -88,6 +89,7 @@ class ApplicationMain
private reconnectBackoff = new ReconnectionBackoff();
private beforeFirstDaemonConnection = true;
private isPerformingPostUpgrade = false;
+ private daemonAllowed?: boolean;
private quitInitiated = false;
private tunnelStateExpectation?: Expectation;
@@ -384,6 +386,8 @@ class ApplicationMain
systemPreferences.subscribeNotification('AppleShowScrollBarsSettingChanged', async () => {
await this.updateMacOsScrollbarVisibility();
});
+
+ await this.checkMacOsLaunchDaemon();
}
this.userInterface = new UserInterface(
@@ -591,6 +595,9 @@ class ApplicationMain
} else {
log.info('Disconnected from the daemon');
}
+ if (process.platform === 'darwin') {
+ void this.checkMacOsLaunchDaemon();
+ }
};
private connectToDaemon() {
@@ -677,6 +684,7 @@ class ApplicationMain
tunnelState: this.tunnelState.tunnelState,
settings: this.settings.all,
isPerformingPostUpgrade: this.isPerformingPostUpgrade,
+ daemonAllowed: this.daemonAllowed,
deviceState: this.account.deviceState,
relayList: this.relayList,
currentVersion: this.version.currentVersion,
@@ -875,6 +883,31 @@ class ApplicationMain
return this.settings.gui.unpinnedWindow && !this.settings.gui.startMinimized;
}
+ private checkMacOsLaunchDaemon(): Promise<void> {
+ const daemonBin = resolveBin('mullvad-daemon');
+ const args = ['--launch-daemon-status'];
+ return new Promise((resolve, _reject) => {
+ execFile(daemonBin, args, { windowsHide: true }, (error, stdout, stderr) => {
+ if (error) {
+ if (error.code === 2) {
+ IpcMainEventChannel.daemon.notifyDaemonAllowed?.(false);
+ this.daemonAllowed = false;
+ } else {
+ log.error(
+ `Error while checking launch daemon authorization status.
+ Stdout: ${stdout.toString()}
+ Stderr: ${stderr.toString()}`,
+ );
+ }
+ } else {
+ IpcMainEventChannel.daemon.notifyDaemonAllowed?.(true);
+ this.daemonAllowed = true;
+ }
+ resolve();
+ });
+ });
+ }
+
private async updateMacOsScrollbarVisibility(): Promise<void> {
const command =
'defaults read kCFPreferencesAnyApplication AppleShowScrollBars || echo Automatic';
diff --git a/gui/src/main/user-interface.ts b/gui/src/main/user-interface.ts
index 88922f3f75..78af002c58 100644
--- a/gui/src/main/user-interface.ts
+++ b/gui/src/main/user-interface.ts
@@ -1,6 +1,8 @@
+import { exec } from 'child_process';
import { app, BrowserWindow, dialog, Menu, nativeImage, screen, Tray } from 'electron';
import path from 'path';
import { sprintf } from 'sprintf-js';
+import { promisify } from 'util';
import { closeToExpiry, hasExpired } from '../shared/account-expiry';
import { connectEnabled, disconnectEnabled, reconnectEnabled } from '../shared/connect-helper';
@@ -20,6 +22,8 @@ import { isMacOs11OrNewer } from './platform-version';
import TrayIconController, { TrayIconType } from './tray-icon-controller';
import WindowController, { WindowControllerDelegate } from './window-controller';
+const execAsync = promisify(exec);
+
export interface UserInterfaceDelegate {
cancelPendingNotifications(): void;
resetTunnelStateAnnouncements(): void;
@@ -69,6 +73,16 @@ export default class UserInterface implements WindowControllerDelegate {
this.browsingFiles = false;
return response;
});
+
+ IpcMainEventChannel.app.handleShowLaunchDaemonSettings(async () => {
+ try {
+ await execAsync(
+ 'open -W x-apple.systempreferences:com.apple.LoginItems-Settings.extension',
+ );
+ } catch (error) {
+ log.error(`Failed to open launch daemon settings: ${error}`);
+ }
+ });
}
public createTrayIconController(
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index 87bf91123b..14fa75f9cc 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -125,6 +125,10 @@ export default class AppRenderer {
this.setIsPerformingPostUpgrade(isPerformingPostUpgrade);
});
+ IpcRendererEventChannel.daemon.listenDaemonAllowed((daemonAllowed) => {
+ this.reduxActions.userInterface.setDaemonAllowed(daemonAllowed);
+ });
+
IpcRendererEventChannel.account.listen((newAccountData?: IAccountData) => {
this.setAccountExpiry(newAccountData?.expiry);
});
@@ -204,6 +208,10 @@ export default class AppRenderer {
this.setSettings(initialState.settings);
this.setIsPerformingPostUpgrade(initialState.isPerformingPostUpgrade);
+ if (initialState.daemonAllowed !== undefined) {
+ this.reduxActions.userInterface.setDaemonAllowed(initialState.daemonAllowed);
+ }
+
if (initialState.deviceState) {
const deviceState = initialState.deviceState;
this.handleDeviceEvent(
@@ -470,6 +478,10 @@ export default class AppRenderer {
void IpcRendererEventChannel.windowsSplitTunneling.removeApplication(application);
}
+ public async showLaunchDaemonSettings() {
+ await IpcRendererEventChannel.app.showLaunchDaemonSettings();
+ }
+
public async sendProblemReport(
email: string,
message: string,
@@ -617,6 +629,7 @@ export default class AppRenderer {
private onDaemonConnected() {
this.connectedToDaemon = true;
this.reduxActions.userInterface.setConnectedToDaemon(true);
+ this.reduxActions.userInterface.setDaemonAllowed(true);
this.resetNavigation();
}
diff --git a/gui/src/renderer/components/ErrorView.tsx b/gui/src/renderer/components/ErrorView.tsx
index 12ca510396..fead788c24 100644
--- a/gui/src/renderer/components/ErrorView.tsx
+++ b/gui/src/renderer/components/ErrorView.tsx
@@ -1,3 +1,4 @@
+import React from 'react';
import styled from 'styled-components';
import { colors } from '../../config.json';
@@ -10,8 +11,15 @@ const StyledContainer = styled(Container)({
flex: 1,
flexDirection: 'column',
alignItems: 'center',
- justifyContent: 'center',
- marginTop: '-150px',
+ justifyContent: 'end',
+});
+
+const StyledContent = styled.div({
+ display: 'flex',
+ flex: 1,
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'end',
});
const Logo = styled(ImageView)({
@@ -32,8 +40,16 @@ const Subtitle = styled.span({
textAlign: 'center',
});
+const StyledFooterContainer = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'end',
+ minHeight: '241px',
+});
+
interface ErrorViewProps {
settingsUnavailable?: boolean;
+ footer?: React.ReactNode | React.ReactNode[];
children: React.ReactNode | React.ReactNode[];
}
@@ -42,9 +58,12 @@ export default function ErrorView(props: ErrorViewProps) {
<Layout>
<Header>{!props.settingsUnavailable && <HeaderBarSettingsButton />}</Header>
<StyledContainer>
- <Logo height={106} width={106} source="logo-icon" />
- <Title height={18} source="logo-text" />
- <Subtitle role="alert">{props.children}</Subtitle>
+ <StyledContent>
+ <Logo height={106} width={106} source="logo-icon" />
+ <Title height={18} source="logo-text" />
+ <Subtitle role="alert">{props.children}</Subtitle>
+ </StyledContent>
+ <StyledFooterContainer>{props.footer}</StyledFooterContainer>
</StyledContainer>
</Layout>
);
diff --git a/gui/src/renderer/components/Launch.tsx b/gui/src/renderer/components/Launch.tsx
index a3145b8e16..5f12fcce8f 100644
--- a/gui/src/renderer/components/Launch.tsx
+++ b/gui/src/renderer/components/Launch.tsx
@@ -1,10 +1,72 @@
+import { useCallback } from 'react';
+import styled from 'styled-components';
+
+import { colors } from '../../config.json';
import { messages } from '../../shared/gettext';
+import { useAppContext } from '../context';
+import { useSelector } from '../redux/store';
+import * as AppButton from './AppButton';
+import { measurements, tinyText } from './common-styles';
import ErrorView from './ErrorView';
+import { Footer } from './Layout';
export default function Launch() {
+ const daemonAllowed = useSelector((state) => state.userInterface.daemonAllowed);
+ const footer = <SettingsFooter show={daemonAllowed === false} />;
+
return (
- <ErrorView>
+ <ErrorView footer={footer}>
{messages.pgettext('launch-view', 'Connecting to Mullvad system service...')}
</ErrorView>
);
}
+
+const StyledFooter = styled(Footer)({}, (props: { show: boolean }) => ({
+ backgroundColor: colors.blue,
+ padding: `0 14px ${measurements.viewMargin}`,
+ opacity: props.show ? 1 : 0,
+ transition: 'opacity 250ms ease-in-out',
+}));
+
+const StyledSystemSettingsContainer = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ flex: 1,
+ backgroundColor: colors.darkBlue,
+ borderRadius: '8px',
+ margin: 0,
+ padding: '16px',
+});
+
+const StyledLaunchFooterPrompt = styled.span(tinyText, {
+ color: colors.white,
+ margin: `8px 0 ${measurements.buttonVerticalMargin} 0`,
+});
+
+interface ISettingsFooterProps {
+ show: boolean;
+}
+
+function SettingsFooter(props: ISettingsFooterProps) {
+ const { showLaunchDaemonSettings } = useAppContext();
+
+ const openSettings = useCallback(async () => {
+ await showLaunchDaemonSettings();
+ }, []);
+
+ return (
+ <StyledFooter show={props.show}>
+ <StyledSystemSettingsContainer>
+ <StyledLaunchFooterPrompt>
+ {messages.pgettext(
+ 'launch-view',
+ 'Permission for the Mullvad VPN service has been revoked. Please go to System Settings and allow Mullvad VPN under the “Allow in the Background” setting.',
+ )}
+ </StyledLaunchFooterPrompt>
+ <AppButton.BlueButton onClick={openSettings}>
+ {messages.gettext('Go to System Settings')}
+ </AppButton.BlueButton>
+ </StyledSystemSettingsContainer>
+ </StyledFooter>
+ );
+}
diff --git a/gui/src/renderer/redux/userinterface/actions.ts b/gui/src/renderer/redux/userinterface/actions.ts
index 46da6bdc3d..b4b5885370 100644
--- a/gui/src/renderer/redux/userinterface/actions.ts
+++ b/gui/src/renderer/redux/userinterface/actions.ts
@@ -30,6 +30,11 @@ export interface ISetConnectedToDaemon {
connectedToDaemon: boolean;
}
+export interface ISetDaemonAllowed {
+ type: 'SET_DAEMON_ALLOWED';
+ daemonAllowed: boolean;
+}
+
export interface ISetChangelog {
type: 'SET_CHANGELOG';
changelog: IChangelog;
@@ -52,6 +57,7 @@ export type UserInterfaceAction =
| ISetWindowFocusedAction
| ISetMacOsScrollbarVisibility
| ISetConnectedToDaemon
+ | ISetDaemonAllowed
| ISetChangelog
| ISetForceShowChanges
| ISetIsPerformingPostUpgrade;
@@ -99,6 +105,13 @@ function setConnectedToDaemon(connectedToDaemon: boolean): ISetConnectedToDaemon
};
}
+function setDaemonAllowed(daemonAllowed: boolean): ISetDaemonAllowed {
+ return {
+ type: 'SET_DAEMON_ALLOWED',
+ daemonAllowed,
+ };
+}
+
function setChangelog(changelog: IChangelog): ISetChangelog {
return {
type: 'SET_CHANGELOG',
@@ -127,6 +140,7 @@ export default {
setWindowFocused,
setMacOsScrollbarVisibility,
setConnectedToDaemon,
+ setDaemonAllowed,
setChangelog,
setForceShowChanges,
setIsPerformingPostUpgrade,
diff --git a/gui/src/renderer/redux/userinterface/reducers.ts b/gui/src/renderer/redux/userinterface/reducers.ts
index 96d8bc03e6..f9ecc6fdad 100644
--- a/gui/src/renderer/redux/userinterface/reducers.ts
+++ b/gui/src/renderer/redux/userinterface/reducers.ts
@@ -9,6 +9,7 @@ export interface IUserInterfaceReduxState {
windowFocused: boolean;
macOsScrollbarVisibility?: MacOsScrollbarVisibility;
connectedToDaemon: boolean;
+ daemonAllowed?: boolean;
changelog: IChangelog;
forceShowChanges: boolean;
isPerformingPostUpgrade: boolean;
@@ -20,6 +21,7 @@ const initialState: IUserInterfaceReduxState = {
windowFocused: false,
macOsScrollbarVisibility: undefined,
connectedToDaemon: false,
+ daemonAllowed: undefined,
changelog: [],
forceShowChanges: false,
isPerformingPostUpgrade: false,
@@ -48,6 +50,9 @@ export default function (
case 'SET_CONNECTED_TO_DAEMON':
return { ...state, connectedToDaemon: action.connectedToDaemon };
+ case 'SET_DAEMON_ALLOWED':
+ return { ...state, daemonAllowed: action.daemonAllowed };
+
case 'SET_CHANGELOG':
return {
...state,
diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts
index 21d7b6216d..0ea4fdc662 100644
--- a/gui/src/shared/ipc-schema.ts
+++ b/gui/src/shared/ipc-schema.ts
@@ -57,6 +57,7 @@ export interface IAppStateSnapshot {
tunnelState: TunnelState;
settings: ISettings;
isPerformingPostUpgrade: boolean;
+ daemonAllowed?: boolean;
deviceState?: DeviceState;
relayList?: IRelayListWithEndpointData;
currentVersion: ICurrentAppVersionInfo;
@@ -124,6 +125,7 @@ export const ipcSchema = {
},
daemon: {
isPerformingPostUpgrade: notifyRenderer<boolean>(),
+ daemonAllowed: notifyRenderer<boolean>(),
connected: notifyRenderer<void>(),
disconnected: notifyRenderer<void>(),
},
@@ -141,6 +143,7 @@ export const ipcSchema = {
quit: send<void>(),
openUrl: invoke<string, void>(),
showOpenDialog: invoke<Electron.OpenDialogOptions, Electron.OpenDialogReturnValue>(),
+ showLaunchDaemonSettings: invoke<void, void>(),
},
location: {
get: invoke<void, ILocation>(),
diff --git a/mullvad-daemon/Cargo.toml b/mullvad-daemon/Cargo.toml
index d5c5fd3c09..28d00cc9b3 100644
--- a/mullvad-daemon/Cargo.toml
+++ b/mullvad-daemon/Cargo.toml
@@ -51,6 +51,9 @@ simple-signal = "1.1"
[target.'cfg(target_os="linux")'.dependencies]
talpid-dbus = { path = "../talpid-dbus" }
+[target.'cfg(target_os="macos")'.dependencies]
+objc = "0.2.3"
+
[target.'cfg(windows)'.dependencies]
ctrlc = "3.0"
duct = "0.13"
diff --git a/mullvad-daemon/src/cli.rs b/mullvad-daemon/src/cli.rs
index 09412f4196..ad9c5d233b 100644
--- a/mullvad-daemon/src/cli.rs
+++ b/mullvad-daemon/src/cli.rs
@@ -7,6 +7,8 @@ pub struct Config {
pub log_stdout_timestamps: bool,
pub run_as_service: bool,
pub register_service: bool,
+ #[cfg(target_os = "macos")]
+ pub launch_daemon_status: bool,
#[cfg(target_os = "linux")]
pub initialize_firewall_and_exit: bool,
}
@@ -35,6 +37,8 @@ pub fn create_config() -> Config {
cfg!(target_os = "linux") && matches.is_present("initialize-early-boot-firewall");
let run_as_service = cfg!(windows) && matches.is_present("run_as_service");
let register_service = cfg!(windows) && matches.is_present("register_service");
+ #[cfg(target_os = "macos")]
+ let launch_daemon_status = matches.is_present("launch_daemon_status");
Config {
#[cfg(target_os = "linux")]
@@ -44,6 +48,8 @@ pub fn create_config() -> Config {
log_stdout_timestamps,
run_as_service,
register_service,
+ #[cfg(target_os = "macos")]
+ launch_daemon_status,
}
}
@@ -111,5 +117,11 @@ fn create_app() -> App<'static> {
.help("Initialize firewall to be used during early boot and exit"),
)
}
+
+ if cfg!(target_os = "macos") {
+ app = app.arg(Arg::new("launch_daemon_status").long("launch-daemon-status").help(
+ "Checks the status of the launch daemon. The exit code represents the current status",
+ ))
+ }
app
}
diff --git a/mullvad-daemon/src/macos_launch_daemon.rs b/mullvad-daemon/src/macos_launch_daemon.rs
new file mode 100644
index 0000000000..04a562e9a1
--- /dev/null
+++ b/mullvad-daemon/src/macos_launch_daemon.rs
@@ -0,0 +1,97 @@
+//! Provides functions to handle or query the status of the Mullvad launch
+//! daemon/system service on macOS.
+//!
+//! If the service exists but needs to be approved by the user, this status
+//! must be checked so that the user can be directed to approve the launch
+//! daemon in the system settings.
+
+use objc::{class, msg_send, sel, sel_impl};
+use std::ffi::CStr;
+
+type Id = *mut objc::runtime::Object;
+
+// Framework that contains `SMAppService`.
+#[link(name = "ServiceManagement", kind = "framework")]
+extern "C" {}
+
+/// Returned by `[NSProcessInfo operatingSystemVersion]`. Contains the current
+#[repr(C)]
+#[derive(Debug)]
+struct NSOperatingSystemVersion {
+ major_version: libc::c_ulong,
+ minor_version: libc::c_ulong,
+ patch_version: libc::c_ulong,
+}
+
+/// Authorization status of the Mullvad daemon.
+#[repr(i32)]
+pub enum LaunchDaemonStatus {
+ Ok = 0,
+ NotFound = 1,
+ NotAuthorized = 2,
+ Unknown = 3,
+}
+
+/// Return whether the daemon is running, not found, or is not authorized.
+/// NOTE: On macos < 13, this function always returns `LaunchDaemonStatus::Ok`.
+pub fn get_status() -> LaunchDaemonStatus {
+ // `SMAppService` does not exist if the major version is less than 13.
+ if get_os_version().major_version < 13 {
+ return LaunchDaemonStatus::Ok;
+ }
+ get_status_for_url(&daemon_plist_url())
+}
+
+fn get_status_for_url(url: &Object) -> LaunchDaemonStatus {
+ let status: libc::c_long =
+ unsafe { msg_send![class!(SMAppService), statusForLegacyURL: url.0] };
+
+ match status {
+ // SMAppServiceStatusNotRegistered | SMAppServiceStatusNotFound
+ 0 | 3 => LaunchDaemonStatus::NotFound,
+ // SMAppServiceStatusEnabled
+ 1 => LaunchDaemonStatus::Ok,
+ // SMAppServiceStatusRequiresApproval
+ 2 => LaunchDaemonStatus::NotAuthorized,
+ // Unknown status
+ _ => LaunchDaemonStatus::Unknown,
+ }
+}
+
+fn get_os_version() -> NSOperatingSystemVersion {
+ // the object is lazily instantiated, so we don't release it
+ let proc_info: Id = unsafe { msg_send![class!(NSProcessInfo), processInfo] };
+ unsafe { msg_send![proc_info, operatingSystemVersion] }
+}
+
+/// Returns an `NSURL` instance for `DAEMON_PLIST_PATH`.
+fn daemon_plist_url() -> Object {
+ /// Path to the plist that defines the Mullvad launch daemon.
+ /// It must be kept in sync with the path defined in
+ /// `dist-assets/pkg-scripts/postinstall`.
+ const DAEMON_PLIST_PATH: &CStr = unsafe {
+ CStr::from_bytes_with_nul_unchecked(b"/Library/LaunchDaemons/net.mullvad.daemon.plist\0")
+ };
+
+ let nsstr_inst: Id = unsafe { msg_send![class!(NSString), alloc] };
+ let nsstr_inst: Id =
+ unsafe { msg_send![nsstr_inst, initWithUTF8String: DAEMON_PLIST_PATH.as_ptr()] };
+
+ let nsurl_inst: Id = unsafe { msg_send![class!(NSURL), alloc] };
+ let nsurl_inst: Id = unsafe { msg_send![nsurl_inst, initWithString: nsstr_inst] };
+
+ let _: libc::c_void = unsafe { msg_send![nsstr_inst, release] };
+
+ assert!(!nsurl_inst.is_null());
+
+ Object(nsurl_inst)
+}
+
+/// Calls `[self.0 release]` when the wrapped instance is dropped.
+struct Object(Id);
+
+impl Drop for Object {
+ fn drop(&mut self) {
+ let _: libc::c_void = unsafe { msg_send![self.0, release] };
+ }
+}
diff --git a/mullvad-daemon/src/main.rs b/mullvad-daemon/src/main.rs
index ccf3f66fac..2572821e0d 100644
--- a/mullvad-daemon/src/main.rs
+++ b/mullvad-daemon/src/main.rs
@@ -14,6 +14,8 @@ mod cli;
#[cfg(target_os = "linux")]
mod early_boot_firewall;
mod exception_logging;
+#[cfg(target_os = "macos")]
+mod macos_launch_daemon;
#[cfg(windows)]
mod system_service;
@@ -53,6 +55,11 @@ fn init_daemon_logging(config: &cli::Config) -> Result<Option<PathBuf>, String>
return Ok(None);
}
+ #[cfg(target_os = "macos")]
+ if config.launch_daemon_status {
+ return Ok(None);
+ }
+
let log_dir = get_log_dir(config)?;
let log_path = |filename| log_dir.as_ref().map(|dir| dir.join(filename));
@@ -127,7 +134,15 @@ async fn run_platform(config: &cli::Config, log_dir: Option<PathBuf>) -> Result<
run_standalone(log_dir).await
}
-#[cfg(not(any(windows, target_os = "linux")))]
+#[cfg(target_os = "macos")]
+async fn run_platform(config: &cli::Config, log_dir: Option<PathBuf>) -> Result<(), String> {
+ if config.launch_daemon_status {
+ std::process::exit(macos_launch_daemon::get_status() as i32);
+ }
+ run_standalone(log_dir).await
+}
+
+#[cfg(not(any(windows, target_os = "linux", target_os = "macos")))]
async fn run_platform(_config: &cli::Config, log_dir: Option<PathBuf>) -> Result<(), String> {
run_standalone(log_dir).await
}