mirror of https://github.com/ospab/ostp.git
Port CLI subcommands + multi-channel release onto the 0.4.0 rebuild
Brings the useful work from the old master/pre-release lineage (5c2b5a0) onto the clean rebuild, since that lineage never had the multi-server/WSS/ Reality removal or any of the 0.4.0 stability work. This is a manual port, not a cherry-pick — this file's Args/ClientConfig shape had already diverged too much for the patch to apply mechanically. - ostp/src/main.rs: flat Args -> clap subcommands (setup, init, generate-key, links, check, connect, uninstall, update, import, proxy-env, proxy-env-clear), bridged onto the existing ~500 lines of flag-driven dispatch via a LegacyArgs struct so none of that logic had to change. - Fixed a real bug found while porting: GenerateKey's `count` used short='c', colliding with the global `--config` short (also 'c'), which clap validates across the whole command tree on first parse() -- a duplicate short flag there could break every subcommand's parsing, not just generate-key's. - `update` now takes `-b/--branch` and `-v/--version` explicitly (was a bare flag with no way to target a channel or exact version). - Added the UAC elevation step for TUN mode that this lineage's CLI was completely missing (`run_client_directly` went straight to creating the TUN adapter unelevated). Also fixed the elevation check itself: it only tested `ret <= 32`, but ShellExecuteW returns ERROR_CANCELLED (1223) when the user clicks "No" on the UAC prompt -- > 32, so a denied prompt was read as success and the process exited silently without starting the tunnel. Now ret==1223 is reported explicitly, and a genuine failure logs GetLastError() so the real Win32 cause is visible next time. - scripts/install.sh: added -b/--branch alongside the existing -v/--version, and channel-aware release resolution (nightly/pre-release use their own rolling tag; stable resolves via the GitHub API's "latest"). - release.yml: added nightly/pre-release branch-push triggers for rolling prereleases. Fixed the tag_name logic from5c2b5a0, which mapped `master` pushes to a release tagged "nightly" -- backwards from master being the most stable channel. github.ref_name already equals the branch or tag name that triggered the run, so no per-branch remapping is needed at all; master is intentionally left off the branch-push list -- it only ever gets real version tags. Verified: `cargo build -p ostp` succeeds, and `ostp update --help` / `ostp generate-key --help` / `ostp --help` show the expected flags with no clap panic. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
8ca411a64b
commit
b2ee9eb010
|
|
@ -6,6 +6,9 @@ on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "v*"
|
||||||
|
branches:
|
||||||
|
- nightly
|
||||||
|
- pre-release
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|
@ -239,10 +242,17 @@ jobs:
|
||||||
|
|
||||||
# ── Upload ─────────────────────────────────────────────────────────────
|
# ── Upload ─────────────────────────────────────────────────────────────
|
||||||
- name: Upload to GitHub Release
|
- name: Upload to GitHub Release
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
prerelease: ${{ contains(github.ref_name, '-') }}
|
# Version tags (v0.4.1, v0.4.1-beta.N) use their own name as the
|
||||||
|
# release; branch pushes (nightly/pre-release) roll a release named
|
||||||
|
# after the branch itself — no name remapping needed since
|
||||||
|
# github.ref_name is already the tag OR the branch name as-is.
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
# Any branch push is a rolling prerelease; for real version tags,
|
||||||
|
# a hyphenated suffix (-beta.N) marks it prerelease, a bare
|
||||||
|
# semver tag (v0.4.1) is a stable release.
|
||||||
|
prerelease: ${{ !startsWith(github.ref, 'refs/tags/') || contains(github.ref_name, '-') }}
|
||||||
files: ${{ matrix.release_name }}
|
files: ${{ matrix.release_name }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
@ -314,10 +324,17 @@ jobs:
|
||||||
Compress-Archive -Path "$dir/*" -DestinationPath "ostp-windows-gui-${{ matrix.arch }}.zip" -Force
|
Compress-Archive -Path "$dir/*" -DestinationPath "ostp-windows-gui-${{ matrix.arch }}.zip" -Force
|
||||||
|
|
||||||
- name: Upload to GitHub Release
|
- name: Upload to GitHub Release
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
prerelease: ${{ contains(github.ref_name, '-') }}
|
# Version tags (v0.4.1, v0.4.1-beta.N) use their own name as the
|
||||||
|
# release; branch pushes (nightly/pre-release) roll a release named
|
||||||
|
# after the branch itself — no name remapping needed since
|
||||||
|
# github.ref_name is already the tag OR the branch name as-is.
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
# Any branch push is a rolling prerelease; for real version tags,
|
||||||
|
# a hyphenated suffix (-beta.N) marks it prerelease, a bare
|
||||||
|
# semver tag (v0.4.1) is a stable release.
|
||||||
|
prerelease: ${{ !startsWith(github.ref, 'refs/tags/') || contains(github.ref_name, '-') }}
|
||||||
files: ostp-windows-gui-${{ matrix.arch }}.zip
|
files: ostp-windows-gui-${{ matrix.arch }}.zip
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
@ -375,10 +392,17 @@ jobs:
|
||||||
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
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
prerelease: ${{ contains(github.ref_name, '-') }}
|
# Version tags (v0.4.1, v0.4.1-beta.N) use their own name as the
|
||||||
|
# release; branch pushes (nightly/pre-release) roll a release named
|
||||||
|
# after the branch itself — no name remapping needed since
|
||||||
|
# github.ref_name is already the tag OR the branch name as-is.
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
# Any branch push is a rolling prerelease; for real version tags,
|
||||||
|
# a hyphenated suffix (-beta.N) marks it prerelease, a bare
|
||||||
|
# semver tag (v0.4.1) is a stable release.
|
||||||
|
prerelease: ${{ !startsWith(github.ref, 'refs/tags/') || contains(github.ref_name, '-') }}
|
||||||
files: ostp-linux-gui-${{ matrix.arch }}.tar.gz
|
files: ostp-linux-gui-${{ matrix.arch }}.tar.gz
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
@ -433,10 +457,17 @@ jobs:
|
||||||
tar -czf ostp-macos-gui-${{ matrix.arch }}.tar.gz ostp-macos-gui-${{ matrix.arch }}
|
tar -czf ostp-macos-gui-${{ matrix.arch }}.tar.gz ostp-macos-gui-${{ matrix.arch }}
|
||||||
|
|
||||||
- name: Upload to GitHub Release
|
- name: Upload to GitHub Release
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
prerelease: ${{ contains(github.ref_name, '-') }}
|
# Version tags (v0.4.1, v0.4.1-beta.N) use their own name as the
|
||||||
|
# release; branch pushes (nightly/pre-release) roll a release named
|
||||||
|
# after the branch itself — no name remapping needed since
|
||||||
|
# github.ref_name is already the tag OR the branch name as-is.
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
# Any branch push is a rolling prerelease; for real version tags,
|
||||||
|
# a hyphenated suffix (-beta.N) marks it prerelease, a bare
|
||||||
|
# semver tag (v0.4.1) is a stable release.
|
||||||
|
prerelease: ${{ !startsWith(github.ref, 'refs/tags/') || contains(github.ref_name, '-') }}
|
||||||
files: ostp-macos-gui-${{ matrix.arch }}.tar.gz
|
files: ostp-macos-gui-${{ matrix.arch }}.tar.gz
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
@ -502,10 +533,17 @@ jobs:
|
||||||
cp build/app/outputs/flutter-apk/app-release.apk ostp-android-${{ matrix.arch }}.apk
|
cp build/app/outputs/flutter-apk/app-release.apk ostp-android-${{ matrix.arch }}.apk
|
||||||
|
|
||||||
- name: Upload to GitHub Release
|
- name: Upload to GitHub Release
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
prerelease: ${{ contains(github.ref_name, '-') }}
|
# Version tags (v0.4.1, v0.4.1-beta.N) use their own name as the
|
||||||
|
# release; branch pushes (nightly/pre-release) roll a release named
|
||||||
|
# after the branch itself — no name remapping needed since
|
||||||
|
# github.ref_name is already the tag OR the branch name as-is.
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
# Any branch push is a rolling prerelease; for real version tags,
|
||||||
|
# a hyphenated suffix (-beta.N) marks it prerelease, a bare
|
||||||
|
# semver tag (v0.4.1) is a stable release.
|
||||||
|
prerelease: ${{ !startsWith(github.ref, 'refs/tags/') || contains(github.ref_name, '-') }}
|
||||||
files: ostp-flutter/ostp-android-${{ matrix.arch }}.apk
|
files: ostp-flutter/ostp-android-${{ matrix.arch }}.apk
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
|
||||||
242
ostp/src/main.rs
242
ostp/src/main.rs
|
|
@ -9,59 +9,88 @@ use colored::Colorize;
|
||||||
#[command(author, version, about = "OSTP Core - Ospab Stealth Transport Protocol", long_about = None)]
|
#[command(author, version, about = "OSTP Core - Ospab Stealth Transport Protocol", long_about = None)]
|
||||||
struct Args {
|
struct Args {
|
||||||
/// Path to the JSON configuration file
|
/// Path to the JSON configuration file
|
||||||
#[cfg_attr(unix, arg(long, default_value = "/etc/ostp/config.json"))]
|
#[cfg_attr(unix, arg(short, long, default_value = "/etc/ostp/config.json", global = true))]
|
||||||
#[cfg_attr(windows, arg(long, default_value = "config.json"))]
|
#[cfg_attr(windows, arg(short, long, default_value = "config.json", global = true))]
|
||||||
config: PathBuf,
|
config: PathBuf,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Option<Commands>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Subcommand, Debug)]
|
||||||
|
enum Commands {
|
||||||
|
/// Run the interactive setup wizard
|
||||||
|
Setup {
|
||||||
/// Optional mode to initialize the config for (client or server)
|
/// Optional mode to initialize the config for (client or server)
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
init: Option<String>,
|
init: Option<String>,
|
||||||
|
},
|
||||||
/// Run the interactive setup wizard
|
/// Initialize config for client, server, or relay mode
|
||||||
#[arg(long)]
|
Init {
|
||||||
setup: bool,
|
mode: String,
|
||||||
|
},
|
||||||
/// Generate a new secure access key and exit
|
/// Generate a new secure access key
|
||||||
#[arg(short = 'g', long)]
|
GenerateKey {
|
||||||
generate_key: bool,
|
|
||||||
|
|
||||||
/// Format for generated key (hex, base64)
|
/// Format for generated key (hex, base64)
|
||||||
#[arg(long, default_value = "hex")]
|
#[arg(long, default_value = "hex")]
|
||||||
format: String,
|
format: String,
|
||||||
|
|
||||||
/// Number of keys to generate
|
/// Number of keys to generate
|
||||||
#[arg(short = 'c', long, default_value_t = 1)]
|
// NOT short='c' — `--config` is a global arg (propagated into every
|
||||||
|
// subcommand's scope), so a local '-c' here would collide with it.
|
||||||
|
// Clap validates the whole command tree on the first parse() and
|
||||||
|
// panics on a duplicate short flag, breaking the ENTIRE CLI.
|
||||||
|
#[arg(short = 'n', long, default_value_t = 1)]
|
||||||
count: usize,
|
count: usize,
|
||||||
|
},
|
||||||
/// Output ready-to-use client sharing links (ostp://...) from the server configuration
|
/// Output ready-to-use client sharing links (ostp://...) from the server configuration
|
||||||
#[arg(long)]
|
Links,
|
||||||
links: bool,
|
/// Validate configuration file
|
||||||
|
Check,
|
||||||
/// Validate configuration file and exit
|
/// Connect using a share link (ostp://ACCESS_KEY@HOST:PORT)
|
||||||
#[arg(long)]
|
Connect {
|
||||||
check: bool,
|
url: String,
|
||||||
|
},
|
||||||
/// Optional client connection share link (ostp://ACCESS_KEY@HOST:PORT) to run instantly
|
|
||||||
url: Option<String>,
|
|
||||||
|
|
||||||
/// Uninstall OSTP: stop service, remove binary and configuration files
|
/// Uninstall OSTP: stop service, remove binary and configuration files
|
||||||
#[arg(long)]
|
Uninstall,
|
||||||
uninstall: bool,
|
|
||||||
|
|
||||||
/// Update OSTP: re-run the install script to fetch and install the latest version
|
/// Update OSTP: re-run the install script to fetch and install the latest version
|
||||||
#[arg(long)]
|
Update {
|
||||||
|
/// Release branch to update from (stable, pre-release, nightly)
|
||||||
|
#[arg(short = 'b', long, default_value = "stable")]
|
||||||
|
branch: String,
|
||||||
|
/// Exact release version to update to (e.g. 0.4.1 or 0.4.1-beta.3),
|
||||||
|
/// overriding the latest release on the selected branch
|
||||||
|
#[arg(short = 'v', long, value_name = "VERSION")]
|
||||||
|
version: Option<String>,
|
||||||
|
},
|
||||||
|
/// Import a share link (ostp://...) into the configuration file
|
||||||
|
Import {
|
||||||
|
url: String,
|
||||||
|
},
|
||||||
|
/// Output shell export commands for proxy (eval $(ostp proxy-env))
|
||||||
|
ProxyEnv,
|
||||||
|
/// Output shell export commands to clear proxy (eval $(ostp proxy-env-clear))
|
||||||
|
ProxyEnvClear,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bridges the new subcommand-based CLI onto the original flat-flag dispatch
|
||||||
|
/// below, so the ~500 lines of existing command logic don't need to change —
|
||||||
|
/// only how they get populated does.
|
||||||
|
struct LegacyArgs {
|
||||||
|
config: PathBuf,
|
||||||
|
init: Option<String>,
|
||||||
|
setup: bool,
|
||||||
|
generate_key: bool,
|
||||||
|
format: String,
|
||||||
|
count: usize,
|
||||||
|
links: bool,
|
||||||
|
check: bool,
|
||||||
|
url: Option<String>,
|
||||||
|
uninstall: bool,
|
||||||
update: bool,
|
update: bool,
|
||||||
|
update_branch: String,
|
||||||
/// Import a share link (ostp://...) into the configuration file and exit
|
target_version: Option<String>,
|
||||||
#[arg(long)]
|
|
||||||
import: Option<String>,
|
import: Option<String>,
|
||||||
|
|
||||||
/// Output shell export commands for proxy (eval $(ostp --proxy-env))
|
|
||||||
#[arg(long)]
|
|
||||||
proxy_env: bool,
|
proxy_env: bool,
|
||||||
|
|
||||||
/// Output shell export commands to clear proxy (eval $(ostp --proxy-env-clear))
|
|
||||||
#[arg(long)]
|
|
||||||
proxy_env_clear: bool,
|
proxy_env_clear: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1022,14 +1051,48 @@ fn wizard_register_windows_service(config_path: &std::path::Path) -> Result<()>
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_app() -> Result<()> {
|
async fn run_app() -> Result<()> {
|
||||||
let args = Args::parse();
|
let raw_args = Args::parse();
|
||||||
|
let mut args = LegacyArgs {
|
||||||
|
config: raw_args.config.clone(),
|
||||||
|
init: None,
|
||||||
|
setup: false,
|
||||||
|
generate_key: false,
|
||||||
|
format: "hex".to_string(),
|
||||||
|
count: 1,
|
||||||
|
links: false,
|
||||||
|
check: false,
|
||||||
|
url: None,
|
||||||
|
uninstall: false,
|
||||||
|
update: false,
|
||||||
|
update_branch: "stable".to_string(),
|
||||||
|
target_version: None,
|
||||||
|
import: None,
|
||||||
|
proxy_env: false,
|
||||||
|
proxy_env_clear: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(cmd) = raw_args.command {
|
||||||
|
match cmd {
|
||||||
|
Commands::Setup { init } => { args.setup = true; args.init = init; }
|
||||||
|
Commands::Init { mode } => { args.init = Some(mode); }
|
||||||
|
Commands::GenerateKey { format, count } => { args.generate_key = true; args.format = format; args.count = count; }
|
||||||
|
Commands::Links => { args.links = true; }
|
||||||
|
Commands::Check => { args.check = true; }
|
||||||
|
Commands::Connect { url } => { args.url = Some(url); }
|
||||||
|
Commands::Uninstall => { args.uninstall = true; }
|
||||||
|
Commands::Update { branch, version } => { args.update = true; args.update_branch = branch; args.target_version = version; }
|
||||||
|
Commands::Import { url } => { args.import = Some(url); }
|
||||||
|
Commands::ProxyEnv => { args.proxy_env = true; }
|
||||||
|
Commands::ProxyEnvClear => { args.proxy_env_clear = true; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if args.uninstall {
|
if args.uninstall {
|
||||||
return cmd_uninstall();
|
return cmd_uninstall();
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.update {
|
if args.update {
|
||||||
return cmd_update();
|
return cmd_update(args.update_branch, args.target_version);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Setup wizard: explicit flag or first-time (no config) ────────
|
// ── Setup wizard: explicit flag or first-time (no config) ────────
|
||||||
|
|
@ -1577,12 +1640,26 @@ fn cmd_uninstall() -> Result<()> {
|
||||||
// Update command
|
// Update command
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn cmd_update() -> Result<()> {
|
fn cmd_update(branch: String, version: Option<String>) -> Result<()> {
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
println!("[ostp] Updating OSTP...");
|
println!("[ostp] Updating OSTP (branch={branch})...");
|
||||||
|
|
||||||
|
let mut script_args = vec!["-c".to_string()];
|
||||||
|
if let Some(v) = version {
|
||||||
|
script_args.push(format!(
|
||||||
|
"bash <(curl -Ls https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.sh) --branch {} -v {}",
|
||||||
|
branch, v
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
script_args.push(format!(
|
||||||
|
"bash <(curl -Ls https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.sh) --branch {}",
|
||||||
|
branch
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let status = Command::new("bash")
|
let status = Command::new("bash")
|
||||||
.args(["-c", "bash <(curl -Ls https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.sh)"])
|
.args(&script_args)
|
||||||
.status()
|
.status()
|
||||||
.map_err(|e| anyhow!("Failed to run update: {e}"))?;
|
.map_err(|e| anyhow!("Failed to run update: {e}"))?;
|
||||||
|
|
||||||
|
|
@ -1593,14 +1670,91 @@ fn cmd_update() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
fn cmd_update() -> Result<()> {
|
fn cmd_update(_branch: String, _version: Option<String>) -> Result<()> {
|
||||||
anyhow::bail!("The 'update' command is only supported on Linux/Unix systems.");
|
anyhow::bail!("The 'update' command is only supported on Linux/Unix systems.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn ensure_elevated_for_tun() -> Result<()> {
|
||||||
|
#[link(name = "shell32")]
|
||||||
|
extern "system" {
|
||||||
|
fn IsUserAnAdmin() -> i32;
|
||||||
|
fn ShellExecuteW(h: *mut std::ffi::c_void, op: *const u16, f: *const u16, p: *const u16, d: *const u16, s: i32) -> isize;
|
||||||
|
}
|
||||||
|
#[link(name = "kernel32")]
|
||||||
|
extern "system" {
|
||||||
|
fn GetLastError() -> u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_admin = unsafe { IsUserAnAdmin() != 0 };
|
||||||
|
if is_admin {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::os::windows::ffi::OsStrExt;
|
||||||
|
|
||||||
|
let exe = std::env::current_exe()?;
|
||||||
|
let exe_wstr: Vec<u16> = exe.as_os_str().encode_wide().chain(Some(0)).collect();
|
||||||
|
let verb_wstr: Vec<u16> = OsStr::new("runas").encode_wide().chain(Some(0)).collect();
|
||||||
|
|
||||||
|
// Reconstruct arguments so the elevated relaunch runs the same command.
|
||||||
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
|
let params_str = args.iter().map(|s| format!("\"{}\"", s)).collect::<Vec<_>>().join(" ");
|
||||||
|
let params_wstr: Vec<u16> = OsStr::new(¶ms_str).encode_wide().chain(Some(0)).collect();
|
||||||
|
|
||||||
|
let cwd = std::env::current_dir()?;
|
||||||
|
let cwd_wstr: Vec<u16> = cwd.as_os_str().encode_wide().chain(Some(0)).collect();
|
||||||
|
|
||||||
|
println!("{}", "[ostp] TUN mode requires administrator privileges. Requesting elevation...".yellow());
|
||||||
|
|
||||||
|
let ret = unsafe {
|
||||||
|
ShellExecuteW(
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
verb_wstr.as_ptr(),
|
||||||
|
exe_wstr.as_ptr(),
|
||||||
|
params_wstr.as_ptr(),
|
||||||
|
cwd_wstr.as_ptr(),
|
||||||
|
1, // SW_SHOWNORMAL
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ShellExecuteW's return is a pseudo-HINSTANCE: > 32 means the call itself
|
||||||
|
// "succeeded" — but that range INCLUDES ERROR_CANCELLED (1223), which is
|
||||||
|
// exactly what Windows returns when the user clicks "No" on the UAC
|
||||||
|
// prompt. The old check (`ret <= 32` only) treated a user-denied prompt
|
||||||
|
// as success and silently exited without ever starting the tunnel.
|
||||||
|
if ret == 1223 {
|
||||||
|
anyhow::bail!("UAC elevation was denied. TUN mode requires administrator privileges.");
|
||||||
|
}
|
||||||
|
if ret <= 32 {
|
||||||
|
let win_err = unsafe { GetLastError() };
|
||||||
|
anyhow::bail!(
|
||||||
|
"Failed to request UAC elevation (ShellExecuteW ret={}, GetLastError={}). \
|
||||||
|
If this keeps happening, an unsigned binary can be silently blocked by \
|
||||||
|
SmartScreen/antivirus during elevation — try running this as Administrator manually.",
|
||||||
|
ret, win_err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
async fn run_client_directly(client_cfg: ClientConfig) -> Result<()> {
|
async fn run_client_directly(client_cfg: ClientConfig) -> Result<()> {
|
||||||
let is_tun_enabled = client_cfg.tun.as_ref().map(|t| t.enable).unwrap_or(false);
|
let is_tun_enabled = client_cfg.tun.as_ref().map(|t| t.enable).unwrap_or(false);
|
||||||
let mode_str = if is_tun_enabled { "tun" } else { "proxy" };
|
let mode_str = if is_tun_enabled { "tun" } else { "proxy" };
|
||||||
println!("{} Starting client (mode={}, server={})", "[ostp]".cyan().bold(), mode_str.yellow(), client_cfg.server.cyan()); let client_conf = ostp_client::config::ClientConfig {
|
println!("{} Starting client (mode={}, server={})", "[ostp]".cyan().bold(), mode_str.yellow(), client_cfg.server.cyan());
|
||||||
|
|
||||||
|
// TUN mode needs admin rights to create the WinTun adapter. This was
|
||||||
|
// missing entirely before — the CLI would just try to create the
|
||||||
|
// adapter unelevated and fail at the driver level with no UAC prompt
|
||||||
|
// ever shown, which is what "UAC denied regardless of GUI or TUI"
|
||||||
|
// actually was for this code path: TUI never asked for elevation at all.
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
if is_tun_enabled {
|
||||||
|
ensure_elevated_for_tun()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let client_conf = ostp_client::config::ClientConfig {
|
||||||
mode: if is_tun_enabled { "tun".to_string() } else { "proxy".to_string() },
|
mode: if is_tun_enabled { "tun".to_string() } else { "proxy".to_string() },
|
||||||
tun_stack: "native".to_string(),
|
tun_stack: "native".to_string(),
|
||||||
debug: client_cfg.debug.unwrap_or(false),
|
debug: client_cfg.debug.unwrap_or(false),
|
||||||
|
|
|
||||||
|
|
@ -85,10 +85,47 @@ esac
|
||||||
|
|
||||||
echo "Platform: linux/$ARCH"
|
echo "Platform: linux/$ARCH"
|
||||||
|
|
||||||
|
# ── Parse arguments ────────────────────────────────────────────────────
|
||||||
|
TARGET_VERSION=""
|
||||||
|
TARGET_BRANCH="stable"
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-v|--version)
|
||||||
|
TARGET_VERSION="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-b|--branch)
|
||||||
|
TARGET_BRANCH="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
# ── Download binary ──────────────────────────────────────────────────
|
# ── Download binary ──────────────────────────────────────────────────
|
||||||
|
|
||||||
echo "Fetching latest release..."
|
if [ -n "$TARGET_VERSION" ]; then
|
||||||
LATEST_RELEASE=$(curl -s "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
LATEST_RELEASE="$TARGET_VERSION"
|
||||||
|
# Ensure it starts with 'v' if it's supposed to (only for real stable
|
||||||
|
# semver tags — the nightly/pre-release channels use bare tag names).
|
||||||
|
if [[ ! "$LATEST_RELEASE" =~ ^v ]] && [ "$TARGET_BRANCH" == "stable" ]; then
|
||||||
|
LATEST_RELEASE="v$LATEST_RELEASE"
|
||||||
|
fi
|
||||||
|
echo "Fetching requested release $LATEST_RELEASE..."
|
||||||
|
else
|
||||||
|
if [ "$TARGET_BRANCH" == "nightly" ]; then
|
||||||
|
echo "Fetching nightly release..."
|
||||||
|
LATEST_RELEASE="nightly"
|
||||||
|
elif [ "$TARGET_BRANCH" == "pre-release" ]; then
|
||||||
|
echo "Fetching pre-release..."
|
||||||
|
LATEST_RELEASE="pre-release"
|
||||||
|
else
|
||||||
|
echo "Fetching latest stable release..."
|
||||||
|
LATEST_RELEASE=$(curl -s "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -z "$LATEST_RELEASE" ] || [[ "$LATEST_RELEASE" == *"null"* ]]; then
|
if [ -z "$LATEST_RELEASE" ] || [[ "$LATEST_RELEASE" == *"null"* ]]; then
|
||||||
echo "[notice] Could not determine latest release automatically."
|
echo "[notice] Could not determine latest release automatically."
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue