mirror of https://github.com/ospab/ostp.git
feat(gui): TUN mode and autostart on Linux
Brings the Linux GUI up to parity with Windows. Four things blocked it, each
independently sufficient:
- build.rs keyed the Windows-manifest step off cfg(windows), which in a build
script describes the HOST. Cross-compiling the helper from Windows to Linux
therefore took that branch and died with "Can only compile resource file
when target_env is gnu or msvc". Now keyed off CARGO_CFG_TARGET_OS, with
the cfg(windows) gate kept as a second check because winres is a host-
resolved build-dependency and simply does not exist on a Linux host.
- launch_as_admin was bail!("Windows only.") outside Windows. Implemented for
Linux via pkexec, polkit's front-end, which raises a graphical auth prompt;
sudo is unusable from a GUI with no terminal. Missing pkexec now names the
package to install instead of failing opaquely.
- The release workflow only built ostp-tun-helper in the Windows job, so the
Linux package shipped without it. It is now built and placed next to the
GUI binary, where find_helper_exe looks first.
- set_autostart/get_autostart were no-ops off Windows. Implemented via XDG
autostart (~/.config/autostart/ostp.desktop, honouring XDG_CONFIG_HOME),
the direct equivalent of the HKCU Run key.
The helper's own code needed no changes — it already compiled for Linux once
the build script stopped rejecting it. list_running_processes already had a
Linux branch.
The token file is created 0600 on Linux: /tmp is world-readable there, unlike
the Windows temp dir, and that token authorises control of the privileged
helper.
Not verified on a live Linux desktop from here — the Tauri backend cannot be
compiled for Linux on a Windows host (GTK dev libraries), so the cfg(linux)
paths are reviewed rather than built. CI compiles them.
This commit is contained in:
parent
8a1426ecf5
commit
8f0ffd08c0
|
|
@ -477,12 +477,19 @@ jobs:
|
||||||
working-directory: ostp-gui
|
working-directory: ostp-gui
|
||||||
run: |
|
run: |
|
||||||
npm install
|
npm install
|
||||||
|
# TUN mode shells out to this helper, elevated via pkexec. Only the
|
||||||
|
# Windows job used to build it, so the Linux package shipped without
|
||||||
|
# it and TUN could never start.
|
||||||
|
cargo build -p ostp-tun-helper --release --target ${{ matrix.target }} --manifest-path ../Cargo.toml
|
||||||
npx tauri build --no-bundle --target ${{ matrix.target }}
|
npx tauri build --no-bundle --target ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Package Portable Tarball
|
- name: Package Portable Tarball
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
mkdir ostp-linux-gui-${{ matrix.arch }}
|
mkdir ostp-linux-gui-${{ matrix.arch }}
|
||||||
cp ostp-gui/src-tauri/target/${{ matrix.target }}/release/ostp-gui ostp-linux-gui-${{ matrix.arch }}/
|
cp ostp-gui/src-tauri/target/${{ matrix.target }}/release/ostp-gui ostp-linux-gui-${{ matrix.arch }}/
|
||||||
|
# The GUI looks for the helper next to its own executable first.
|
||||||
|
cp target/${{ matrix.target }}/release/ostp-tun-helper ostp-linux-gui-${{ matrix.arch }}/
|
||||||
tar -czf ostp-linux-gui-${{ matrix.arch }}.tar.gz ostp-linux-gui-${{ matrix.arch }}
|
tar -czf ostp-linux-gui-${{ matrix.arch }}.tar.gz ostp-linux-gui-${{ matrix.arch }}
|
||||||
|
|
||||||
- name: Upload to GitHub Release
|
- name: Upload to GitHub Release
|
||||||
|
|
|
||||||
|
|
@ -229,9 +229,47 @@ fn set_autostart(enable: bool) -> Result<(), String> {
|
||||||
.output();
|
.output();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
// XDG autostart: desktop environments launch every .desktop file in
|
||||||
|
// ~/.config/autostart on login. This is the portable equivalent of the
|
||||||
|
// HKCU Run key above and needs no elevation.
|
||||||
|
let path = linux_autostart_path().ok_or("Cannot determine the autostart directory")?;
|
||||||
|
if enable {
|
||||||
|
let exe = std::env::current_exe().map_err(|e| format!("Cannot get exe path: {}", e))?;
|
||||||
|
if let Some(dir) = path.parent() {
|
||||||
|
std::fs::create_dir_all(dir)
|
||||||
|
.map_err(|e| format!("Cannot create {}: {}", dir.display(), e))?;
|
||||||
|
}
|
||||||
|
let entry = format!(
|
||||||
|
"[Desktop Entry]\n\
|
||||||
|
Type=Application\n\
|
||||||
|
Name=OSTP\n\
|
||||||
|
Exec=\"{}\"\n\
|
||||||
|
Terminal=false\n\
|
||||||
|
X-GNOME-Autostart-enabled=true\n",
|
||||||
|
exe.display()
|
||||||
|
);
|
||||||
|
std::fs::write(&path, entry)
|
||||||
|
.map_err(|e| format!("Cannot write {}: {}", path.display(), e))?;
|
||||||
|
} else if path.exists() {
|
||||||
|
std::fs::remove_file(&path)
|
||||||
|
.map_err(|e| format!("Cannot remove {}: {}", path.display(), e))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Path of the XDG autostart entry, honouring XDG_CONFIG_HOME.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn linux_autostart_path() -> Option<PathBuf> {
|
||||||
|
let base = std::env::var_os("XDG_CONFIG_HOME")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.filter(|p| !p.as_os_str().is_empty())
|
||||||
|
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))?;
|
||||||
|
Some(base.join("autostart").join("ostp.desktop"))
|
||||||
|
}
|
||||||
|
|
||||||
/// Checks if the app is currently in Windows startup.
|
/// Checks if the app is currently in Windows startup.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_autostart() -> bool {
|
fn get_autostart() -> bool {
|
||||||
|
|
@ -246,6 +284,12 @@ fn get_autostart() -> bool {
|
||||||
return o.status.success();
|
return o.status.success();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
if let Some(path) = linux_autostart_path() {
|
||||||
|
return path.exists();
|
||||||
|
}
|
||||||
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -625,22 +669,10 @@ async fn start_tun_via_helper(
|
||||||
raw: &ClientConfigRaw,
|
raw: &ClientConfigRaw,
|
||||||
app: tauri::AppHandle,
|
app: tauri::AppHandle,
|
||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
// TUN mode goes through a privileged helper, and the only elevation path
|
// TUN goes through a privileged helper. Elevation is implemented for
|
||||||
// implemented is the Windows UAC one (see launch_as_admin). The helper is
|
// Windows (UAC) and Linux (polkit/pkexec); anywhere else launch_as_admin
|
||||||
// also not built for other platforms by the release workflow. Say that
|
// reports that plainly rather than letting this fail later as a confusing
|
||||||
// plainly and up front: previously this fell through to the helper lookup
|
// missing-file error.
|
||||||
// and surfaced as a missing-file error naming a Windows executable, which
|
|
||||||
// on Linux reads as a packaging mistake rather than an unimplemented
|
|
||||||
// feature.
|
|
||||||
if !cfg!(windows) {
|
|
||||||
return Err(
|
|
||||||
"TUN mode is currently Windows-only: it needs a privileged helper, and elevation \
|
|
||||||
for it is only implemented on Windows. Use proxy mode (SOCKS5/HTTP) on this \
|
|
||||||
platform."
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let port = {
|
let port = {
|
||||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").map_err(|e| format!("Bind error: {}", e))?;
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").map_err(|e| format!("Bind error: {}", e))?;
|
||||||
listener.local_addr().unwrap().port()
|
listener.local_addr().unwrap().port()
|
||||||
|
|
@ -825,8 +857,50 @@ fn launch_as_admin(exe: &std::path::PathBuf, token: &str, port: u16) -> anyhow::
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(target_os = "linux")]
|
||||||
fn launch_as_admin(_exe: &PathBuf, _token: &str, _port: u16) -> Result<()> { anyhow::bail!("Windows only."); }
|
fn launch_as_admin(exe: &PathBuf, token: &str, port: u16) -> Result<()> {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
// Same shape as the Windows path: the token goes through a file rather than
|
||||||
|
// argv, so it never shows up in the process list.
|
||||||
|
let token_file = std::env::temp_dir().join(format!("ostp_auth_{}.tmp", rand::random::<u32>()));
|
||||||
|
std::fs::write(&token_file, token)?;
|
||||||
|
// Unlike Windows, /tmp is world-readable here, and this token authenticates
|
||||||
|
// control of the privileged tunnel helper — restrict it to the owner.
|
||||||
|
let _ = std::fs::set_permissions(&token_file, std::fs::Permissions::from_mode(0o600));
|
||||||
|
|
||||||
|
// pkexec is polkit's front-end: in a desktop session it raises a graphical
|
||||||
|
// authentication dialog. sudo is not an option from a GUI process, which has
|
||||||
|
// no terminal to prompt on.
|
||||||
|
match Command::new("pkexec")
|
||||||
|
.arg(exe)
|
||||||
|
.arg("--port")
|
||||||
|
.arg(port.to_string())
|
||||||
|
.arg("--token-file")
|
||||||
|
.arg(&token_file)
|
||||||
|
.spawn()
|
||||||
|
{
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
let _ = std::fs::remove_file(&token_file);
|
||||||
|
anyhow::bail!(
|
||||||
|
"pkexec was not found, so the TUN helper cannot be granted the privileges it \
|
||||||
|
needs. Install polkit (package \"policykit-1\" on Debian/Ubuntu, \"polkit\" on \
|
||||||
|
Fedora/Arch), or use proxy mode, which needs no elevation."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let _ = std::fs::remove_file(&token_file);
|
||||||
|
Err(e.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
||||||
|
fn launch_as_admin(_exe: &PathBuf, _token: &str, _port: u16) -> Result<()> {
|
||||||
|
anyhow::bail!("TUN mode needs a privileged helper, which is implemented on Windows and Linux only. Use proxy mode on this platform.");
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn show_error_dialog(msg: &str) {
|
fn show_error_dialog(msg: &str) {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,21 @@
|
||||||
// or launched via ShellExecuteW("runas").
|
// or launched via ShellExecuteW("runas").
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
// Key off the TARGET, not the host. In a build script `cfg(windows)`
|
||||||
|
// describes the machine doing the building, so cross-compiling the helper
|
||||||
|
// from Windows to Linux took this branch and failed with "Can only compile
|
||||||
|
// resource file when target_env is gnu or msvc". CARGO_CFG_TARGET_OS is the
|
||||||
|
// target being built for, which is what actually decides whether a Windows
|
||||||
|
// manifest belongs in the binary.
|
||||||
|
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||||
|
if target_os != "windows" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Second gate, on the HOST: winres is declared under
|
||||||
|
// [target.'cfg(windows)'.build-dependencies], and build-dependencies are
|
||||||
|
// resolved against the host triple, so the crate simply does not exist when
|
||||||
|
// building on Linux. Referencing it unconditionally would fail to compile
|
||||||
|
// there even though the target check above already passed.
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
let mut res = winres::WindowsResource::new();
|
let mut res = winres::WindowsResource::new();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue