diff options
| author | David Lönnhager <david.l@mullvad.net> | 2023-02-01 17:40:05 +0100 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2023-02-01 17:40:05 +0100 |
| commit | 423d3e7ae0f6a54a6ea4506df4ea9ad2df9b6eab (patch) | |
| tree | 0b2f6ce30a7e1e374da6a1e92527f545195551f4 | |
| parent | c646045b9f25e31be377096db91db0f7907510d3 (diff) | |
| parent | d921f649f7953b1cd0d930a73461db4909674da8 (diff) | |
| download | mullvadvpn-423d3e7ae0f6a54a6ea4506df4ea9ad2df9b6eab.tar.xz mullvadvpn-423d3e7ae0f6a54a6ea4506df4ea9ad2df9b6eab.zip | |
Merge branch 'macos-fix-daemon-approval'
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | Cargo.lock | 19 | ||||
| -rwxr-xr-x | dist-assets/pkg-scripts/postinstall | 2 | ||||
| -rw-r--r-- | gui/locales/messages.pot | 7 | ||||
| -rw-r--r-- | gui/src/main/index.ts | 35 | ||||
| -rw-r--r-- | gui/src/main/user-interface.ts | 14 | ||||
| -rw-r--r-- | gui/src/renderer/app.tsx | 13 | ||||
| -rw-r--r-- | gui/src/renderer/components/ErrorView.tsx | 29 | ||||
| -rw-r--r-- | gui/src/renderer/components/Launch.tsx | 64 | ||||
| -rw-r--r-- | gui/src/renderer/redux/userinterface/actions.ts | 14 | ||||
| -rw-r--r-- | gui/src/renderer/redux/userinterface/reducers.ts | 5 | ||||
| -rw-r--r-- | gui/src/shared/ipc-schema.ts | 3 | ||||
| -rw-r--r-- | mullvad-daemon/Cargo.toml | 3 | ||||
| -rw-r--r-- | mullvad-daemon/src/cli.rs | 12 | ||||
| -rw-r--r-- | mullvad-daemon/src/macos_launch_daemon.rs | 97 | ||||
| -rw-r--r-- | mullvad-daemon/src/main.rs | 17 |
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 } |
