Skip to content

Commit c852beb

Browse files
committed
Add privileged helper daemon for password-free start/stop on macOS and Linux
- New helper_ipc.rs: Unix domain socket IPC between GUI and root helper - macOS: LaunchDaemon in /Library/LaunchDaemons/ (osascript one-time install) - Linux: systemd service in /etc/systemd/system/ (pkexec one-time install) - GUI falls back to platform elevation if socket connection fails - Add idcflare.com to README and CHANGELOG for v0.1.15
1 parent 93fd455 commit c852beb

9 files changed

Lines changed: 603 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## v0.1.15
4+
5+
- 新增 `idcflare.com` 及其子域的支持,与 `linux.do` 统一纳入代理、hosts 和证书管理范围。
6+
- macOS / Linux 新增特权辅助守护进程:首次安装时输入一次密码(macOS LaunchDaemon / Linux systemd service),之后"开始加速/停止加速"不再需要密码。辅助进程通过 Unix domain socket 与 GUI 通信;socket 连接失败时自动回退到原有的提权方式。
7+
38
## v0.1.14
49

510
- 修复 Windows 上前端仍在运行时被误判为“前端异常退出”的问题;守护进程现在仅在 UI lease 真正过期或持续缺失后才自动停止。

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
- 一键生成并安装本地根证书
3737
- 一键写入和清理 `hosts`
3838
- 本地监听 `127.211.73.84:80/443`
39-
-`linux.do` 及其子域提供本地接管和转发
39+
-`linux.do``idcflare.com` 及其子域提供本地接管和转发
4040
- 同时支持脚本场景下的 `CLI`,以及普通用户可双击使用的桌面 GUI
4141

4242
## Why This Exists
@@ -56,6 +56,7 @@
5656
- 支持缓存,避免每次都重新解析
5757
- 避免把私人 `DoH` 配成系统全局,减少额外负载
5858
- 目前测试可在 `IPv6` 网络环境下加速 `linux.do`
59+
- 同时支持 `idcflare.com` 及其子域的加速
5960

6061
几个关键点:
6162

@@ -298,7 +299,7 @@ macOS 不再走本地交叉编译脚本,而是通过 GitHub Actions 原生构
298299

299300
- 站点专属本地接管,不是系统全局代理
300301
-`HTTP / HTTPS` 为主
301-
- 侧重 `linux.do` 及其关联域名
302+
- 侧重 `linux.do``idcflare.com` 及其关联域名
302303

303304
## Development Notes
304305

src/autostart.rs

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,249 @@ fn xml_escape(value: &str) -> String {
310310
.replace('>', ">")
311311
}
312312

313+
// ---------------------------------------------------------------------------
314+
// macOS privileged helper (LaunchDaemon) management
315+
// ---------------------------------------------------------------------------
316+
317+
#[cfg(target_os = "macos")]
318+
const HELPER_LABEL: &str = "io.linuxdo.accelerator.helper";
319+
320+
#[cfg(target_os = "macos")]
321+
fn helper_plist_path() -> std::path::PathBuf {
322+
std::path::PathBuf::from("/Library/LaunchDaemons").join(format!("{HELPER_LABEL}.plist"))
323+
}
324+
325+
/// Check whether the privileged helper LaunchDaemon is installed and loaded.
326+
#[cfg(target_os = "macos")]
327+
pub fn is_privileged_helper_installed() -> bool {
328+
let plist = helper_plist_path();
329+
if !plist.exists() {
330+
return false;
331+
}
332+
// Also verify launchd knows about it.
333+
std::process::Command::new("launchctl")
334+
.args(["list", HELPER_LABEL])
335+
.output()
336+
.map(|o| o.status.success())
337+
.unwrap_or(false)
338+
}
339+
340+
/// Install the privileged helper LaunchDaemon. Requires administrator privileges
341+
/// (triggers a one-time password prompt via osascript).
342+
#[cfg(target_os = "macos")]
343+
pub fn install_privileged_helper(exe: &std::path::Path, config_path: &std::path::Path) -> Result<()> {
344+
use std::fs;
345+
346+
let plist_path = helper_plist_path();
347+
let exe = absolute_display_path(exe);
348+
let config = absolute_display_path(config_path);
349+
350+
let plist = format!(
351+
r#"<?xml version="1.0" encoding="UTF-8"?>
352+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
353+
<plist version="1.0">
354+
<dict>
355+
<key>Label</key>
356+
<string>{label}</string>
357+
<key>ProgramArguments</key>
358+
<array>
359+
<string>{exe}</string>
360+
<string>--config</string>
361+
<string>{config}</string>
362+
<string>privileged-helper</string>
363+
</array>
364+
<key>RunAtLoad</key>
365+
<true/>
366+
<key>KeepAlive</key>
367+
<true/>
368+
<key>ProcessType</key>
369+
<string>Standard</string>
370+
</dict>
371+
</plist>
372+
"#,
373+
label = HELPER_LABEL,
374+
exe = xml_escape(&exe),
375+
config = xml_escape(&config),
376+
);
377+
378+
// Write the plist and load it via an elevated osascript call (one-time prompt).
379+
let script = format!(
380+
"do shell script \"mkdir -p /Library/LaunchDaemons && cat > {plist_path} <<'PLIST_EOF'\n{plist}\nPLIST_EOF\nlaunchctl unload -w {plist_path} 2>/dev/null; launchctl load -w {plist_path}\" with prompt \"Linux.do Accelerator 需要安装特权辅助进程以实现免密码加速。\" with administrator privileges",
381+
plist_path = plist_path.display(),
382+
plist = plist,
383+
);
384+
385+
// We need to write the plist content through osascript because the target
386+
// directory is root-owned. Use a temp file approach for reliability.
387+
let tmp = std::env::temp_dir().join(format!("{HELPER_LABEL}.plist"));
388+
fs::write(&tmp, &plist).context("failed to write temporary plist")?;
389+
390+
let install_script = format!(
391+
"do shell script \"cp '{tmp}' '{dest}' && launchctl unload -w '{dest}' 2>/dev/null; launchctl load -w '{dest}'\" with prompt \"Linux.do Accelerator 需要安装特权辅助进程以实现免密码加速。\" with administrator privileges",
392+
tmp = tmp.display(),
393+
dest = plist_path.display(),
394+
);
395+
396+
crate::platform::run_command("osascript", &["-e", &install_script])
397+
.context("failed to install privileged helper (osascript)")?;
398+
399+
// Clean up temp file.
400+
let _ = fs::remove_file(&tmp);
401+
402+
// Give launchd a moment to start the helper.
403+
std::thread::sleep(std::time::Duration::from_millis(500));
404+
405+
Ok(())
406+
}
407+
408+
/// Uninstall the privileged helper LaunchDaemon.
409+
#[cfg(target_os = "macos")]
410+
pub fn uninstall_privileged_helper() -> Result<()> {
411+
let plist_path = helper_plist_path();
412+
if !plist_path.exists() {
413+
return Ok(());
414+
}
415+
416+
let script = format!(
417+
"do shell script \"launchctl unload -w '{plist}' 2>/dev/null; rm -f '{plist}'\" with prompt \"Linux.do Accelerator 需要卸载特权辅助进程。\" with administrator privileges",
418+
plist = plist_path.display(),
419+
);
420+
421+
crate::platform::run_command("osascript", &["-e", &script])
422+
.context("failed to uninstall privileged helper")?;
423+
Ok(())
424+
}
425+
426+
// ---------------------------------------------------------------------------
427+
// Linux privileged helper (systemd user service) management
428+
// ---------------------------------------------------------------------------
429+
430+
#[cfg(target_os = "linux")]
431+
const HELPER_SERVICE_NAME: &str = "linuxdo-accelerator-helper";
432+
433+
#[cfg(target_os = "linux")]
434+
fn helper_service_path() -> Result<PathBuf> {
435+
// Use a system-level service so it runs as root without per-user setup.
436+
Ok(PathBuf::from("/etc/systemd/system")
437+
.join(format!("{HELPER_SERVICE_NAME}.service")))
438+
}
439+
440+
/// Check whether the privileged helper systemd service is installed and active.
441+
#[cfg(target_os = "linux")]
442+
pub fn is_privileged_helper_installed() -> bool {
443+
let Ok(path) = helper_service_path() else {
444+
return false;
445+
};
446+
if !path.exists() {
447+
return false;
448+
}
449+
std::process::Command::new("systemctl")
450+
.args(["is-active", "--quiet", HELPER_SERVICE_NAME])
451+
.status()
452+
.map(|s| s.success())
453+
.unwrap_or(false)
454+
}
455+
456+
/// Install the privileged helper as a systemd service. Requires root (pkexec).
457+
#[cfg(target_os = "linux")]
458+
pub fn install_privileged_helper(exe: &std::path::Path, config_path: &std::path::Path) -> Result<()> {
459+
use std::fs;
460+
461+
let service_path = helper_service_path()?;
462+
let exe = exe.canonicalize().unwrap_or_else(|_| exe.to_path_buf());
463+
let config = config_path.canonicalize().unwrap_or_else(|_| config_path.to_path_buf());
464+
465+
let unit = format!(
466+
r#"[Unit]
467+
Description=Linux.do Accelerator Privileged Helper
468+
After=network.target
469+
470+
[Service]
471+
Type=simple
472+
ExecStart={exe} --config {config} privileged-helper
473+
Restart=on-failure
474+
RestartSec=3
475+
476+
[Install]
477+
WantedBy=multi-user.target
478+
"#,
479+
exe = exe.display(),
480+
config = config.display(),
481+
);
482+
483+
// Write the unit file via pkexec (one-time root prompt).
484+
let tmp = std::env::temp_dir().join(format!("{HELPER_SERVICE_NAME}.service"));
485+
fs::write(&tmp, &unit).context("failed to write temporary service file")?;
486+
487+
let dest = service_path.display();
488+
let status = std::process::Command::new("pkexec")
489+
.args([
490+
"bash",
491+
"-c",
492+
&format!(
493+
"cp '{tmp}' '{dest}' && systemctl daemon-reload && systemctl enable --now '{name}'",
494+
tmp = tmp.display(),
495+
dest = dest,
496+
name = HELPER_SERVICE_NAME,
497+
),
498+
])
499+
.status()
500+
.context("failed to execute pkexec")?;
501+
502+
let _ = fs::remove_file(&tmp);
503+
504+
if !status.success() {
505+
bail!("pkexec rejected the request or systemctl failed");
506+
}
507+
508+
// Give systemd a moment to start the helper.
509+
std::thread::sleep(std::time::Duration::from_millis(500));
510+
Ok(())
511+
}
512+
513+
/// Uninstall the privileged helper systemd service.
514+
#[cfg(target_os = "linux")]
515+
pub fn uninstall_privileged_helper() -> Result<()> {
516+
let Ok(path) = helper_service_path() else {
517+
return Ok(());
518+
};
519+
if !path.exists() {
520+
return Ok(());
521+
}
522+
523+
let status = std::process::Command::new("pkexec")
524+
.args([
525+
"bash",
526+
"-c",
527+
&format!(
528+
"systemctl disable --now '{name}' 2>/dev/null; rm -f '{path}' && systemctl daemon-reload",
529+
name = HELPER_SERVICE_NAME,
530+
path = path.display(),
531+
),
532+
])
533+
.status()
534+
.context("failed to execute pkexec")?;
535+
536+
if !status.success() {
537+
bail!("pkexec rejected the request or systemctl failed");
538+
}
539+
Ok(())
540+
}
541+
542+
// Non-Unix stubs (Windows, Android, etc.)
543+
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
544+
pub fn is_privileged_helper_installed() -> bool {
545+
false
546+
}
547+
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
548+
pub fn install_privileged_helper(_exe: &std::path::Path, _config_path: &std::path::Path) -> Result<()> {
549+
bail!("privileged helper is not supported on this platform")
550+
}
551+
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
552+
pub fn uninstall_privileged_helper() -> Result<()> {
553+
bail!("privileged helper is not supported on this platform")
554+
}
555+
313556
#[cfg(target_os = "linux")]
314557
fn platform_enable(exe: &Path, config_path: &Path) -> Result<()> {
315558
use std::fs;

src/cli.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ pub enum Command {
5252
#[command(hide = true)]
5353
Daemon,
5454
#[command(hide = true)]
55+
PrivilegedHelper,
56+
#[command(hide = true)]
5557
TrayShell,
5658
}
5759

@@ -137,6 +139,12 @@ pub fn run(cli: Cli) -> Result<()> {
137139
Some(Command::Daemon) => {
138140
run_async(service::run_foreground(cli.config, false))?;
139141
}
142+
Some(Command::PrivilegedHelper) => {
143+
#[cfg(unix)]
144+
service::run_privileged_helper(cli.config)?;
145+
#[cfg(not(unix))]
146+
anyhow::bail!("privileged-helper is only supported on Unix");
147+
}
140148
Some(Command::TrayShell) => {
141149
#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
142150
{

0 commit comments

Comments
 (0)