Compare commits

..

No commits in common. "v0.4.5-beta.1" and "master" have entirely different histories.

14 changed files with 34 additions and 280 deletions

View File

@ -417,28 +417,6 @@ jobs:
Compress-Archive -Path "$dir/*" -DestinationPath "ostp-windows-gui-${{ matrix.arch }}.zip" -Force
# The installer is what removes the per-connect consent prompt: it runs
# elevated, so its hook can register the helper's Scheduled Task once.
# The portable zip above cannot, and falls back to asking on first connect.
# The sidecar and its config are confined to this step: declaring
# externalBin in an auto-merged tauri.windows.conf.json would force every
# Windows build, down to a bare `cargo check`, to have the helper staged
# first, and fail the build script when it is not.
- name: Build NSIS Installer
working-directory: ostp-gui
run: |
node stage-sidecar.js --release --target ${{ matrix.target }}
npx tauri build --bundles nsis --target ${{ matrix.target }} --config src-tauri/tauri.installer.conf.json
- name: Collect installer
shell: pwsh
run: |
$nsis = Get-ChildItem -Path "ostp-gui/src-tauri/target/${{ matrix.target }}/release/bundle/nsis" -Filter *-setup.exe -ErrorAction SilentlyContinue |
Select-Object -First 1
if (-not $nsis) { Write-Error "NSIS installer was not produced"; exit 1 }
Copy-Item $nsis.FullName "ostp-windows-gui-${{ matrix.arch }}-setup.exe"
Write-Host "installer: $($nsis.Name) -> ostp-windows-gui-${{ matrix.arch }}-setup.exe"
- name: Upload to GitHub Release
uses: softprops/action-gh-release@v2
with:
@ -448,9 +426,7 @@ jobs:
# real stable release.
tag_name: ${{ needs.resolve-channel.outputs.tag_name }}
prerelease: ${{ needs.resolve-channel.outputs.prerelease }}
files: |
ostp-windows-gui-${{ matrix.arch }}.zip
ostp-windows-gui-${{ matrix.arch }}-setup.exe
files: ostp-windows-gui-${{ matrix.arch }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

3
.gitignore vendored
View File

@ -57,6 +57,3 @@ ostp-control/
netstack-smoltcp/
dnstt/
ostp-web/
# Tauri sidecar staging area (copied from target/ at build time)
ostp-gui/src-tauri/binaries/

View File

@ -1,6 +1,6 @@
{
"target_version": "0.4.5",
"branch": "beta",
"target_version": "0.4.4",
"branch": "master",
"alpha_iteration": 0,
"beta_iteration": 1
"beta_iteration": 0
}

12
Cargo.lock generated
View File

@ -1386,7 +1386,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "ostp"
version = "0.4.5"
version = "0.4.4"
dependencies = [
"anyhow",
"base64",
@ -1409,7 +1409,7 @@ dependencies = [
[[package]]
name = "ostp-client"
version = "0.4.5"
version = "0.4.4"
dependencies = [
"anyhow",
"base64",
@ -1440,7 +1440,7 @@ dependencies = [
[[package]]
name = "ostp-core"
version = "0.4.5"
version = "0.4.4"
dependencies = [
"anyhow",
"bytes",
@ -1474,7 +1474,7 @@ dependencies = [
[[package]]
name = "ostp-server"
version = "0.4.5"
version = "0.4.4"
dependencies = [
"anyhow",
"axum",
@ -1507,7 +1507,7 @@ dependencies = [
[[package]]
name = "ostp-tun"
version = "0.4.5"
version = "0.4.4"
dependencies = [
"anyhow",
"libc",
@ -1519,7 +1519,7 @@ dependencies = [
[[package]]
name = "ostp-tun-helper"
version = "0.4.5"
version = "0.4.4"
dependencies = [
"anyhow",
"chrono",

View File

@ -12,7 +12,7 @@ resolver = "2"
[workspace.package]
edition = "2021"
license = "AGPL-3.0"
version = "0.4.5"
version = "0.4.4"
[workspace.dependencies]
anyhow = "1.0"

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 0.4.5+32
version: 0.4.4+31
environment:
sdk: ^3.11.4

View File

@ -1,15 +1,14 @@
{
"name": "ostp-gui",
"private": true,
"version": "0.4.5",
"version": "0.4.4",
"type": "module",
"scripts": {
"tauri": "tauri",
"dev": "cargo build -p ostp-tun-helper && npx tauri dev",
"build": "cargo build -p ostp-tun-helper --release && npx tauri build --no-bundle",
"build:installer": "cargo build -p ostp-tun-helper --release && node stage-sidecar.js --release && npx tauri build --bundles nsis --config src-tauri/tauri.installer.conf.json",
"build:dist": "npm run build && node build_dist.js",
"sidecar": "node stage-sidecar.js"
"build:installer": "cargo build -p ostp-tun-helper --release && npx tauri build",
"build:dist": "npm run build && node build_dist.js"
},
"devDependencies": {
"@tauri-apps/cli": "^2"

View File

@ -2665,7 +2665,7 @@ dependencies = [
[[package]]
name = "ostp-client"
version = "0.4.5"
version = "0.4.4"
dependencies = [
"anyhow",
"base64 0.22.1",
@ -2696,7 +2696,7 @@ dependencies = [
[[package]]
name = "ostp-core"
version = "0.4.5"
version = "0.4.4"
dependencies = [
"anyhow",
"bytes",
@ -2713,7 +2713,7 @@ dependencies = [
[[package]]
name = "ostp-gui"
version = "0.4.5"
version = "0.4.4"
dependencies = [
"anyhow",
"json_comments",
@ -2733,7 +2733,7 @@ dependencies = [
[[package]]
name = "ostp-tun"
version = "0.4.5"
version = "0.4.4"
dependencies = [
"anyhow",
"libc",

View File

@ -1,6 +1,6 @@
[package]
name = "ostp-gui"
version = "0.4.5"
version = "0.4.4"
description = "OSTP desktop GUI"
authors = ["ospab"]
edition = "2021"

View File

@ -864,12 +864,8 @@ fn xml_unescape(s: &str) -> String {
///
/// Queried as XML rather than `/FO LIST /V`: the list format's field labels are
/// localized (on a Russian Windows "Task To Run" is "Задача для запуска"),
/// whereas XML tag names are fixed.
///
/// Encoding depends on where the output goes, which is measured rather than
/// assumed: to a console schtasks writes UTF-16LE with a BOM, but into a
/// redirected pipe — our case — it writes UTF-8 with no BOM. Both are handled,
/// keyed off the BOM, so this keeps working if that ever flips.
/// whereas XML tag names are fixed. schtasks writes UTF-16LE with a BOM here,
/// but tolerate UTF-8 in case that ever changes.
#[cfg(target_os = "windows")]
fn helper_task_command() -> Option<String> {
let out = quiet_command("schtasks")
@ -1017,49 +1013,24 @@ fn install_helper_task(exe: &std::path::Path) -> anyhow::Result<()> {
.args(["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", &ps])
.status();
// 1223 is ERROR_CANCELLED: the consent prompt was declined. Nothing was
// launched, so there is no point waiting for a task to appear.
if let Ok(s) = &status {
if s.code() == Some(1223) {
let _ = std::fs::remove_file(&xml_path);
anyhow::bail!("the consent prompt was declined");
}
}
// The exit code is advisory only, never proof of success. `-Verb RunAs`
// launches through ShellExecute, and a non-elevated parent frequently
// cannot read the elevated child's exit code — `$p.ExitCode` then yields
// $null, and `exit $null` leaves PowerShell reporting 0. A failed
// registration would sail straight through a `s.success()` check.
//
// Worse, -Wait does not reliably block until the elevated process exits.
// Deleting the XML right after the call raced schtasks reading it — the
// exact bug that made the previous attempt fail — so wait for the task
// itself to show up. These queries are windowless, so unlike the earlier
// polling loop they cost the user nothing to watch.
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(15);
let mut registered = false;
while std::time::Instant::now() < deadline {
if helper_task_matches(exe) {
registered = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(250));
}
// Only now is deleting it safe.
// schtasks has exited by now, so this is safe.
let _ = std::fs::remove_file(&xml_path);
if registered {
return Ok(());
}
match status {
Ok(s) if s.success() => {}
Ok(s) => anyhow::bail!(
"the scheduled task did not appear after registration (powershell exit {:?})",
"registering the scheduled task failed (exit code {:?}). A declined consent prompt \
reports 1223.",
s.code()
),
Err(e) => anyhow::bail!("could not run powershell to register the task: {e}"),
}
if helper_task_matches(exe) {
Ok(())
} else {
anyhow::bail!("schtasks reported success but the task does not point at {}", exe.display())
}
}
/// Escape a value for embedding in a PowerShell single-quoted string.

View File

@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ostp-gui",
"version": "0.4.5",
"version": "0.4.4",
"identifier": "com.ospab.ostp",
"build": {
"frontendDist": "../src"

View File

@ -1,13 +0,0 @@
{
"$schema": "https://schema.tauri.app/config/2",
"bundle": {
"externalBin": ["binaries/ostp-tun-helper"],
"resources": { "binaries/wintun.dll": "wintun.dll" },
"windows": {
"nsis": {
"installMode": "perMachine",
"installerHooks": "./windows/hooks.nsh"
}
}
}
}

View File

@ -1,103 +0,0 @@
; Registers the Scheduled Task that lets the GUI start the TUN helper elevated
; without a consent prompt.
;
; This belongs in the installer, not in the app. Registering a task that runs
; with elevated rights is itself a privileged operation, so an unprivileged GUI
; can only get one by raising a UAC prompt — which is the very thing we are
; trying to remove. The installer already runs elevated, so here it costs
; nothing: the user consents once, to the install, and never again per connect.
;
; The task carries no usable trigger (a one-shot dated in the past), because it
; exists solely to be started on demand by the app.
!macro OSTP_WRITE_TASK_XML OUTFILE
; NSIS is built in Unicode mode here, so FileWrite emits UTF-16LE — which is
; what `schtasks /XML` requires. It still needs the byte-order mark itself.
FileOpen $R1 "${OUTFILE}" w
FileWriteWord $R1 65279
FileWrite $R1 '<?xml version="1.0" encoding="UTF-16"?>$\r$\n'
FileWrite $R1 '<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">$\r$\n'
FileWrite $R1 ' <RegistrationInfo>$\r$\n'
FileWrite $R1 ' <Description>Starts the OSTP TUN helper elevated so connecting does not prompt for consent every time.</Description>$\r$\n'
FileWrite $R1 ' </RegistrationInfo>$\r$\n'
FileWrite $R1 ' <Principals>$\r$\n'
FileWrite $R1 ' <Principal id="Author">$\r$\n'
; S-1-5-32-545 is BUILTIN\Users by SID rather than by name: the name is
; localized ("Пользователи" on a Russian Windows) and would not resolve.
; Combined with InteractiveToken this makes the task run as whichever user
; actually launches it, so a machine-wide install still works for every
; account instead of only the one that happened to run the installer.
FileWrite $R1 ' <GroupId>S-1-5-32-545</GroupId>$\r$\n'
FileWrite $R1 ' <LogonType>InteractiveToken</LogonType>$\r$\n'
FileWrite $R1 ' <RunLevel>HighestAvailable</RunLevel>$\r$\n'
FileWrite $R1 ' </Principal>$\r$\n'
FileWrite $R1 ' </Principals>$\r$\n'
FileWrite $R1 ' <Settings>$\r$\n'
; Parallel: reconnecting before a previous helper has fully exited must not
; be silently dropped as a duplicate instance.
FileWrite $R1 ' <MultipleInstancesPolicy>Parallel</MultipleInstancesPolicy>$\r$\n'
; A VPN is most needed on battery, and a tunnel must not be killed on unplug.
FileWrite $R1 ' <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>$\r$\n'
FileWrite $R1 ' <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>$\r$\n'
FileWrite $R1 ' <StartWhenAvailable>false</StartWhenAvailable>$\r$\n'
FileWrite $R1 ' <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>$\r$\n'
; PT0S disables the execution time limit; the default would tear the tunnel
; down after three days.
FileWrite $R1 ' <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>$\r$\n'
FileWrite $R1 ' <Enabled>true</Enabled>$\r$\n'
FileWrite $R1 ' <Hidden>false</Hidden>$\r$\n'
FileWrite $R1 ' <AllowHardTerminate>true</AllowHardTerminate>$\r$\n'
FileWrite $R1 ' </Settings>$\r$\n'
FileWrite $R1 ' <Actions Context="Author">$\r$\n'
FileWrite $R1 ' <Exec>$\r$\n'
FileWrite $R1 ' <Command>$INSTDIR\ostp-tun-helper.exe</Command>$\r$\n'
; The port and auth token change per launch and a task stores a fixed command
; line, so they travel in this file instead. %LOCALAPPDATA% is deliberately
; left unexpanded: Task Scheduler expands it when the task runs, which lands
; on the profile of whoever launched it rather than the installing user's.
FileWrite $R1 ' <Arguments>--args-file "%LOCALAPPDATA%\OSTP\helper-args.json"</Arguments>$\r$\n'
FileWrite $R1 ' </Exec>$\r$\n'
FileWrite $R1 ' </Actions>$\r$\n'
FileWrite $R1 '</Task>$\r$\n'
FileClose $R1
!macroend
!macro NSIS_HOOK_POSTINSTALL
; Bundled resources land in $INSTDIR\resources, but the helper loads wintun
; with a plain LoadLibrary, which searches its own directory — so put a copy
; beside the executables.
DetailPrint "Placing wintun.dll next to the helper..."
CopyFiles /SILENT "$INSTDIR\resources\wintun.dll" "$INSTDIR\wintun.dll"
DetailPrint "Registering the OSTP TUN helper task..."
!insertmacro OSTP_WRITE_TASK_XML "$PLUGINSDIR\ostp-helper-task.xml"
; /F overwrites an existing registration, so reinstalling or upgrading to a
; different directory repoints the task instead of leaving a stale path — the
; app verifies the registered path at runtime and would otherwise have to
; re-register it with a prompt.
nsExec::ExecToLog 'schtasks.exe /Create /TN "OSTP TUN Helper" /XML "$PLUGINSDIR\ostp-helper-task.xml" /F'
Pop $R0
Delete "$PLUGINSDIR\ostp-helper-task.xml"
${If} $R0 == 0
DetailPrint "Helper task registered; connecting will not prompt for consent."
${Else}
; Not fatal. The app keeps a fallback that registers the task itself on
; first connect, at the cost of the one prompt this was meant to avoid.
DetailPrint "Could not register the helper task (schtasks returned $R0)."
DetailPrint "OSTP will still work, but the first connect will ask for consent."
${EndIf}
!macroend
!macro NSIS_HOOK_PREUNINSTALL
; Leaving the task behind would point at a deleted executable, and the app
; treats a mismatched path as grounds to re-register.
DetailPrint "Removing the OSTP TUN helper task..."
nsExec::ExecToLog 'schtasks.exe /Delete /TN "OSTP TUN Helper" /F'
Pop $R0
; This copy was made by the install hook, so the uninstaller does not know
; about it and would otherwise leave it behind.
Delete "$INSTDIR\wintun.dll"
!macroend

View File

@ -1,73 +0,0 @@
// Stages ostp-tun-helper where Tauri expects a sidecar.
//
// tauri.installer.conf.json declares `externalBin: ["binaries/ostp-tun-helper"]`,
// and Tauri resolves that to `binaries/ostp-tun-helper-<target-triple>.exe` at
// build time, failing the build outright when the file is absent. Cargo writes
// the plain name instead, so it has to be copied across first.
//
// Only the installer build needs this. That config is passed explicitly with
// --config rather than being named tauri.windows.conf.json, which Tauri would
// merge into every Windows build automatically — and then even a bare
// `cargo check` would fail on the missing sidecar.
//
// A no-op off Windows: the Linux and macOS GUI builds have no helper sidecar.
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
if (process.platform !== 'win32') {
process.exit(0);
}
// --target may be passed through; fall back to the host triple rustc reports.
const targetFlag = process.argv.indexOf('--target');
const triple =
targetFlag !== -1 && process.argv[targetFlag + 1]
? process.argv[targetFlag + 1]
: execFileSync('rustc', ['-vV'], { encoding: 'utf8' })
.split('\n')
.find((l) => l.startsWith('host:'))
.slice('host:'.length)
.trim();
const profile = process.argv.includes('--release') ? 'release' : 'debug';
const repoRoot = path.resolve(__dirname, '..');
// Cargo drops a --target build under target/<triple>/, and a host build
// straight into target/. CI always passes --target; local builds usually do not.
const candidates = [
path.join(repoRoot, 'target', triple, profile, 'ostp-tun-helper.exe'),
path.join(repoRoot, 'target', profile, 'ostp-tun-helper.exe'),
];
const src = candidates.find((p) => fs.existsSync(p));
if (!src) {
console.error(
'stage-sidecar: ostp-tun-helper.exe not found. Looked in:\n ' +
candidates.join('\n ') +
`\nBuild it first: cargo build -p ostp-tun-helper${profile === 'release' ? ' --release' : ''}`
);
process.exit(1);
}
const destDir = path.join(__dirname, 'src-tauri', 'binaries');
fs.mkdirSync(destDir, { recursive: true });
const dest = path.join(destDir, `ostp-tun-helper-${triple}.exe`);
fs.copyFileSync(src, dest);
console.log(`stage-sidecar: ${path.relative(repoRoot, src)} -> ${path.relative(repoRoot, dest)}`);
// wintun.dll rides along as a bundled resource. It is only fetched by the
// release workflow, so a local build without it should warn rather than fail —
// the installer just ends up unable to bring a tunnel up.
const dllSrc = [
path.join(repoRoot, 'target', triple, profile, 'wintun.dll'),
path.join(repoRoot, 'target', profile, 'wintun.dll'),
].find((p) => fs.existsSync(p));
if (dllSrc) {
fs.copyFileSync(dllSrc, path.join(destDir, 'wintun.dll'));
console.log(`stage-sidecar: ${path.relative(repoRoot, dllSrc)} -> binaries/wintun.dll`);
} else if (fs.existsSync(path.join(destDir, 'wintun.dll'))) {
console.log('stage-sidecar: reusing the previously staged binaries/wintun.dll');
} else {
console.warn('stage-sidecar: WARNING wintun.dll not found; a bundle build will fail on the missing resource');
}