Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 50 additions & 11 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ use codeischeap_desktop_api::{
};
use codeischeap_gateway::{Gateway, GatewayCapture, GatewayCaptureEvent};
use codeischeap_process_attribution::resolve_loopback_client_pid;
#[cfg(not(target_os = "macos"))]
use codeischeap_proxy_recovery::recover_from_journal;
#[cfg(target_os = "macos")]
use codeischeap_proxy_recovery::{
MacOsPrivilegedProxySession, recover_macos_proxy_journal_with_authorization,
};
#[cfg(windows)]
use codeischeap_proxy_recovery::{ProxySession, ProxySettings, WindowsProxyBackend};
use codeischeap_sidecar_runtime::{
Expand Down Expand Up @@ -193,13 +198,17 @@ impl Drop for ProxyRuntime {
enum PlatformProxySession {
#[cfg(windows)]
Windows(ProxySession<WindowsProxyBackend>),
#[cfg(target_os = "macos")]
MacOs(MacOsPrivilegedProxySession),
}

impl PlatformProxySession {
fn restore(self) -> Result<(), String> {
match self {
#[cfg(windows)]
Self::Windows(session) => session.restore().map_err(|error| error.to_string()),
#[cfg(target_os = "macos")]
Self::MacOs(session) => session.restore().map_err(|error| error.to_string()),
}
}
}
Expand Down Expand Up @@ -1038,14 +1047,28 @@ async fn ensure_proxy_recovery(app: &AppHandle, state: &DesktopState) -> Result<
return Ok(());
}
let journal = application_proxy_recovery_journal(app)?;
tauri::async_runtime::spawn_blocking(move || recover_from_journal(&journal))
tauri::async_runtime::spawn_blocking(move || recover_platform_proxy_journal(&journal))
.await
.map_err(|error| format!("proxy recovery task failed: {error}"))?
.map_err(|error| error.to_string())?;
state.proxy_recovery_checked.store(true, Ordering::Release);
Ok(())
}

#[cfg(not(target_os = "macos"))]
fn recover_platform_proxy_journal(
journal: &Path,
) -> Result<bool, codeischeap_proxy_recovery::RecoveryError> {
recover_from_journal(journal)
}

#[cfg(target_os = "macos")]
fn recover_platform_proxy_journal(
journal: &Path,
) -> Result<bool, codeischeap_proxy_recovery::RecoveryError> {
recover_macos_proxy_journal_with_authorization(std::env::current_exe()?, journal)
}

async fn ensure_gateway(app: &AppHandle, state: &DesktopState) -> Result<(), String> {
let mode = *state.mode.lock().await;
let capture_enabled =
Expand Down Expand Up @@ -1294,7 +1317,7 @@ async fn take_failed_proxy_runtime(state: &DesktopState, generation: u64) -> Opt
runtime
}

#[cfg(windows)]
#[cfg(any(windows, target_os = "macos"))]
async fn activate_system_proxy(app: &AppHandle, runtime: &mut ProxyRuntime) -> Result<(), String> {
if runtime.system_proxy.is_some() {
return Ok(());
Expand All @@ -1305,20 +1328,20 @@ async fn activate_system_proxy(app: &AppHandle, runtime: &mut ProxyRuntime) -> R
{
return Ok(());
}
let desired = system_proxy_settings(&runtime.endpoint);
let endpoint = runtime.endpoint.clone();
let journal = application_proxy_recovery_journal(app)?;
let watchdog = std::env::current_exe()
.map_err(|error| format!("proxy watchdog executable is unavailable: {error}"))?;
let executable = std::env::current_exe()
.map_err(|error| format!("proxy helper executable is unavailable: {error}"))?;
let session = tauri::async_runtime::spawn_blocking(move || {
begin_platform_proxy_session(desired, journal, watchdog)
begin_platform_proxy_session(endpoint, journal, executable)
})
.await
.map_err(|error| format!("system proxy task failed: {error}"))??;
runtime.system_proxy = Some(session);
Ok(())
}

#[cfg(not(windows))]
#[cfg(not(any(windows, target_os = "macos")))]
async fn activate_system_proxy(
_app: &AppHandle,
_runtime: &mut ProxyRuntime,
Expand Down Expand Up @@ -1347,12 +1370,28 @@ fn system_proxy_bypass() -> Vec<String> {

#[cfg(windows)]
fn begin_platform_proxy_session(
desired: ProxySettings,
endpoint: String,
journal: PathBuf,
executable: PathBuf,
) -> Result<PlatformProxySession, String> {
ProxySession::begin(
WindowsProxyBackend::system(),
system_proxy_settings(&endpoint),
journal,
executable,
)
.map(PlatformProxySession::Windows)
.map_err(|error| error.to_string())
}

#[cfg(target_os = "macos")]
fn begin_platform_proxy_session(
endpoint: String,
journal: PathBuf,
watchdog: PathBuf,
executable: PathBuf,
) -> Result<PlatformProxySession, String> {
ProxySession::begin(WindowsProxyBackend::system(), desired, journal, watchdog)
.map(PlatformProxySession::Windows)
MacOsPrivilegedProxySession::begin(executable, journal, &endpoint, Duration::from_secs(15))
.map(PlatformProxySession::MacOs)
.map_err(|error| error.to_string())
}

Expand Down
219 changes: 207 additions & 12 deletions apps/desktop/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,215 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use std::ffi::{OsStr, OsString};
use std::path::PathBuf;

enum StartupCommand {
Application,
Watchdog {
journal: PathBuf,
},
#[cfg(target_os = "macos")]
MacOsProxyHelperDaemon {
journal: PathBuf,
status: PathBuf,
socket: PathBuf,
endpoint: String,
owner_pid: u32,
owner_uid: u32,
},
#[cfg(target_os = "macos")]
MacOsProxyHelperRecover {
journal: PathBuf,
owner_uid: u32,
},
}

fn main() {
let mut arguments = std::env::args_os().skip(1);
if arguments.next().as_deref() == Some(std::ffi::OsStr::new("--journal")) {
let Some(journal) = arguments.next() else {
std::process::exit(2);
};
if arguments.next().is_some() {
let command = match parse_startup_command(std::env::args_os().skip(1)) {
Ok(command) => command,
Err(error) => {
eprintln!("CodeIsCheap startup arguments are invalid: {error}");
std::process::exit(2);
}
if let Err(error) = codeischeap_proxy_recovery::run_watchdog(std::path::Path::new(&journal))
{
eprintln!("CodeIsCheap proxy recovery failed: {error}");
std::process::exit(1);
};
match command {
StartupCommand::Application => codeischeap_desktop_lib::run(),
StartupCommand::Watchdog { journal } => {
if let Err(error) = codeischeap_proxy_recovery::run_watchdog(&journal) {
eprintln!("CodeIsCheap proxy recovery failed: {error}");
std::process::exit(1);
}
}
#[cfg(target_os = "macos")]
StartupCommand::MacOsProxyHelperDaemon {
journal,
status,
socket,
endpoint,
owner_pid,
owner_uid,
} => {
if let Err(error) = codeischeap_proxy_recovery::run_macos_privileged_proxy_helper(
journal, status, socket, &endpoint, owner_pid, owner_uid,
) {
eprintln!("CodeIsCheap macOS proxy helper failed: {error}");
std::process::exit(1);
}
}
#[cfg(target_os = "macos")]
StartupCommand::MacOsProxyHelperRecover { journal, owner_uid } => {
match codeischeap_proxy_recovery::run_macos_privileged_proxy_recovery(
journal, owner_uid,
) {
Ok(true) => println!("recovered"),
Ok(false) => println!("clean"),
Err(error) => {
eprintln!("CodeIsCheap macOS proxy recovery failed: {error}");
std::process::exit(1);
}
}
}
}
}

fn parse_startup_command(
arguments: impl IntoIterator<Item = OsString>,
) -> Result<StartupCommand, String> {
let mut arguments = arguments.into_iter();
let Some(first) = arguments.next() else {
return Ok(StartupCommand::Application);
};
match first.as_os_str() {
value if value == OsStr::new("--journal") => {
let journal = required_value(&mut arguments, "--journal")?.into();
ensure_finished(&mut arguments)?;
Ok(StartupCommand::Watchdog { journal })
}
#[cfg(target_os = "macos")]
value if value == OsStr::new("--macos-proxy-helper-daemon") => {
let journal = named_value(&mut arguments, "--journal")?.into();
let status = named_value(&mut arguments, "--status")?.into();
let socket = named_value(&mut arguments, "--socket")?.into();
let endpoint = unicode_value(named_value(&mut arguments, "--endpoint")?, "endpoint")?;
let owner_pid =
numeric_value(named_value(&mut arguments, "--owner-pid")?, "owner PID")?;
let owner_uid =
numeric_value(named_value(&mut arguments, "--owner-uid")?, "owner UID")?;
ensure_finished(&mut arguments)?;
Ok(StartupCommand::MacOsProxyHelperDaemon {
journal,
status,
socket,
endpoint,
owner_pid,
owner_uid,
})
}
#[cfg(target_os = "macos")]
value if value == OsStr::new("--macos-proxy-helper-recover") => {
let journal = named_value(&mut arguments, "--journal")?.into();
let owner_uid =
numeric_value(named_value(&mut arguments, "--owner-uid")?, "owner UID")?;
ensure_finished(&mut arguments)?;
Ok(StartupCommand::MacOsProxyHelperRecover { journal, owner_uid })
}
return;
_ => Ok(StartupCommand::Application),
}
}

fn named_value(
arguments: &mut impl Iterator<Item = OsString>,
expected_name: &str,
) -> Result<OsString, String> {
let name = arguments
.next()
.ok_or_else(|| format!("missing {expected_name}"))?;
if name != OsStr::new(expected_name) {
return Err(format!("expected {expected_name}"));
}
required_value(arguments, expected_name)
}

fn required_value(
arguments: &mut impl Iterator<Item = OsString>,
name: &str,
) -> Result<OsString, String> {
arguments
.next()
.ok_or_else(|| format!("missing value for {name}"))
}

fn unicode_value(value: OsString, name: &str) -> Result<String, String> {
value
.into_string()
.map_err(|_| format!("{name} must be valid Unicode"))
}

fn numeric_value(value: OsString, name: &str) -> Result<u32, String> {
unicode_value(value, name)?
.parse()
.map_err(|_| format!("{name} must be an unsigned 32-bit integer"))
}

fn ensure_finished(arguments: &mut impl Iterator<Item = OsString>) -> Result<(), String> {
if arguments.next().is_some() {
return Err("unexpected trailing arguments".to_owned());
}
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

fn arguments(values: &[&str]) -> Vec<OsString> {
values.iter().map(|value| OsString::from(*value)).collect()
}

#[test]
fn watchdog_arguments_are_strict() {
assert!(matches!(
parse_startup_command(arguments(&["--journal", "recovery.json"])).unwrap(),
StartupCommand::Watchdog { .. }
));
assert!(parse_startup_command(arguments(&["--journal"])).is_err());
assert!(
parse_startup_command(arguments(&["--journal", "recovery.json", "extra"])).is_err()
);
}

#[cfg(target_os = "macos")]
#[test]
fn macos_helper_arguments_are_strict() {
let daemon = [
"--macos-proxy-helper-daemon",
"--journal",
"/private/recovery/proxy-recovery.v0.1.json",
"--status",
"/private/recovery/helper.status",
"--socket",
"/private/tmp/helper.sock",
"--endpoint",
"http://127.0.0.1:43125",
"--owner-pid",
"123",
"--owner-uid",
"501",
];
assert!(matches!(
parse_startup_command(arguments(&daemon)).unwrap(),
StartupCommand::MacOsProxyHelperDaemon { .. }
));
assert!(parse_startup_command(arguments(&daemon[..daemon.len() - 1])).is_err());
assert!(
parse_startup_command(arguments(&[
"--macos-proxy-helper-recover",
"--journal",
"/private/recovery/proxy-recovery.v0.1.json",
"--owner-uid",
"501",
]))
.is_ok()
);
}
codeischeap_desktop_lib::run();
}
3 changes: 3 additions & 0 deletions crates/proxy-recovery/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ libc = "0.2"
[target.'cfg(windows)'.dependencies]
windows-sys = { workspace = true, features = ["Win32_Foundation", "Win32_System_Threading"] }
winreg.workspace = true

[dev-dependencies]
tempfile.workspace = true
2 changes: 1 addition & 1 deletion crates/proxy-recovery/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ This crate owns the transaction state machine used before CodeIsCheap changes sy

The included file backend exists only for deterministic crash injection. Windows WinINet and macOS networksetup backends have both passed real force-kill recovery experiments on temporary GitHub runners.

The macOS privileged helper protocol is versioned and deliberately narrow: it accepts only an explicit loopback proxy endpoint, requires a private user-owned recovery directory, exposes a mode-0600 Unix socket, binds the single control connection to the requesting UID and PID, and delegates crash recovery to a second root watchdog. Authorization launch and desktop lifecycle wiring remain separate integration work.
The macOS privileged helper protocol is versioned and deliberately narrow: it accepts only an explicit loopback proxy endpoint, requires a private user-owned recovery directory, exposes a mode-0600 Unix socket, binds the single control connection to the requesting UID and PID, and delegates crash recovery to a second root watchdog. The desktop starts the same validated executable through a static, shell-quoted AppleScript authorization command and keeps a control connection open for the proxy lifetime. Armed journals are restored on the next startup through a separate authorized recovery command.
4 changes: 3 additions & 1 deletion crates/proxy-recovery/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ pub use windows::WindowsProxyBackend;
pub use macos::MacOsProxyBackend;
#[cfg(target_os = "macos")]
pub use macos_privileged::{
MacOsPrivilegedProxySession, run_macos_privileged_proxy_helper, run_macos_proxy_helper_session,
MacOsPrivilegedProxySession, recover_macos_proxy_journal_with_authorization,
run_macos_privileged_proxy_helper, run_macos_privileged_proxy_recovery,
run_macos_proxy_helper_session,
};

pub const RECOVERY_JOURNAL_VERSION: &str = "0.1";
Expand Down
Loading
Loading