Compare commits

...

6 Commits

Author SHA1 Message Date
ospab 61091b6d56 chore(release): 0.4.7 + fix resolve-channel prerelease labelling
resolve-channel treated EVERY v* tag as stable, so v0.4.6-beta got published
as a non-prerelease "Latest" release, sitting on top of the release line. Now
a pushed tag is used as-is and its suffix decides the channel: v*-alpha / v*-beta
are prereleases, only a bare vX.Y.Z is stable. (A tag is never recomputed from
Cargo.toml, so the release can't upload to a different tag than the one pushed.)

Version bumped 0.4.5 -> 0.4.7 (0.4.6 is already taken by the mislabelled beta).
This commit's tip is what gets tagged v0.4.7-beta to cut the beta build.
2026-07-09 14:58:40 +03:00
ospab aa1c4ccd52 chore: ignore AI agent instructions directory 2026-07-09 14:53:11 +03:00
ospab 5ab6833eab feat(core): time-rotating junk marker — kill the static per-user fingerprint
The junk marker was a per-key CONSTANT sent in plaintext at a fixed offset in
junk frames. Junk is meant to look like random noise (zapret-style), but a
constant prefix is a recognizable per-user structure: an on-path observer
watching one user sees the same 4 bytes on every junk packet, i.e. an OSTP
fingerprint. (The earlier fix only removed the GLOBAL constant.)

Now the marker rotates every 60s window: junk_marker = HKDF(key, ver, 0x04 ||
window). To an observer the prefix changes each window (no fixed signature),
and a captured marker is only valid for ~1 window — the "bit of protection"
against a leaked marker. Only a key holder can compute it, so an outsider still
can't forge a silently-dropped junk packet (and silent-drop is cheaper than
normal processing anyway, so junk spam was never a DoS lever to begin with).

- core: derive_junk_marker(key, window) + current_junk_window() (60s window),
  same version-gated HKDF scheme; junk_marker dropped from DerivedSecrets.
- client: stamps junk with the current window's marker.
- server: checks current AND previous window per key (absorbs ~1 window of
  clock skew) before falling through to unauthorized-probe handling.
- Not a wire break: only junk framing changes; real handshake/data untouched.
  During mixed rollout, unmatched junk merely logs as a probe (cosmetic).
2026-07-09 14:43:40 +03:00
ospab 5dc3a60017 refactor(logging): consolidate all logs into one ostp.log, Windows clears on start
Every process (CLI daemon, GUI, TUN helper) and every subsystem (tracing, the
core event logger, the helper IPC, panic hook) wrote its own file: ostp-cli.log
+ ostp-core.log + ostp-helper.log + ostp-crash.log — a pile per run. Now they
all funnel into a single ostp.log next to the exe.

- logging: LOG_FILE_NAME/log_file_path() as the one source of truth; init_tracing
  gains a `truncate` arg. Truncation is gated twice: Windows-only (cfg!(windows))
  AND daemon-only. One-shot commands (gk/check/init/-V/...) and the elevated TUN
  helper pass truncate=false so they can never wipe a running daemon's log;
  invocation_is_daemon() detects the daemon from argv. On Linux the server always
  appends (history kept, OS-rotated) as requested.
- runner/helper manual writers + panic hook now target log_file_path(), so their
  output lands in the same ostp.log instead of separate files.
2026-07-09 14:32:08 +03:00
ospab a33e5d3874 ci: fix invalid rust toolchain name 2026-07-09 14:30:23 +03:00
ospab e21acc2ee1 docs: update references from nightly to alpha 2026-07-09 02:49:33 +03:00
19 changed files with 229 additions and 76 deletions

View File

@ -73,19 +73,27 @@ jobs:
BASE_VERSION=$(grep -m1 '^version' Cargo.toml | sed -E 's/version *= *"([^"]+)"/\1/')
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
CHANNEL="stable"
# A pushed tag is authoritative — use it AS-IS (never recompute it
# from Cargo.toml, or the release would upload to a different tag than
# the one that triggered this run). The channel, and thus prerelease,
# is decided by the tag's suffix: v0.4.7-beta / v0.4.7-alpha are
# prereleases; a bare vX.Y.Z is the only thing that becomes stable.
TAG="${{ github.ref_name }}"
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
case "$TAG" in
*-alpha*) CHANNEL="alpha" ;;
*-beta*) CHANNEL="beta" ;;
*) CHANNEL="stable" ;;
esac
else
# No tag (workflow_dispatch, or a legacy branch push): pick the
# channel, then synthesize the rolling tag from Cargo.toml's version.
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
CHANNEL="${{ github.event.inputs.channel }}"
elif [ "${{ github.ref_name }}" = "alpha" ]; then
CHANNEL="alpha"
elif [ "${{ github.ref_name }}" = "pre-release" ]; then
CHANNEL="beta"
else
CHANNEL="alpha"
fi
if [ "$CHANNEL" != "stable" ]; then
TAG="v${BASE_VERSION}-${CHANNEL}"
fi
@ -203,7 +211,7 @@ jobs:
artifact_name: ostp
release_name: ostp-linux-mipsle.tar.gz
use_cross: true
toolchain: alpha
toolchain: nightly
- os: ubuntu-latest
target: riscv64gc-unknown-linux-gnu

2
.gitignore vendored
View File

@ -43,3 +43,5 @@ ostp-brain/
# Management panel built assets (built separately; dummy dist created for rust-embed build)
ostp-control/
.agents/

12
Cargo.lock generated
View File

@ -1384,7 +1384,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "ostp"
version = "0.4.4"
version = "0.4.7"
dependencies = [
"anyhow",
"base64",
@ -1406,7 +1406,7 @@ dependencies = [
[[package]]
name = "ostp-client"
version = "0.4.4"
version = "0.4.7"
dependencies = [
"anyhow",
"base64",
@ -1437,7 +1437,7 @@ dependencies = [
[[package]]
name = "ostp-core"
version = "0.4.4"
version = "0.4.7"
dependencies = [
"anyhow",
"bytes",
@ -1471,7 +1471,7 @@ dependencies = [
[[package]]
name = "ostp-server"
version = "0.4.4"
version = "0.4.7"
dependencies = [
"anyhow",
"axum",
@ -1503,7 +1503,7 @@ dependencies = [
[[package]]
name = "ostp-tun"
version = "0.4.4"
version = "0.4.7"
dependencies = [
"anyhow",
"libc",
@ -1515,7 +1515,7 @@ dependencies = [
[[package]]
name = "ostp-tun-helper"
version = "0.4.4"
version = "0.4.7"
dependencies = [
"anyhow",
"chrono",

View File

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

View File

@ -1060,9 +1060,14 @@ impl Bridge {
let frag_sleep = self.frag_sleep;
let [junk_pc_min, junk_pc_max] = self.junk_pc;
let [junk_ps_min, junk_ps_max] = self.junk_ps;
// Per-key junk marker (derived from the access key) — NOT a global
// constant, so junk frames carry no universal DPI signature.
let junk_marker = ostp_core::crypto::derive_all_secrets(&self.access_key).junk_marker;
// Time-rotating per-key junk marker — NOT a global constant and NOT
// even a static per-user value: it changes every window, so junk
// carries no fixed DPI signature on the wire. All frames in this
// burst are sent within milliseconds, so one window applies to all.
let junk_marker = ostp_core::crypto::derive_junk_marker(
&self.access_key,
ostp_core::crypto::current_junk_window(),
);
{
use tokio::io::AsyncWriteExt;

View File

@ -3,6 +3,53 @@ use std::io::Write;
use std::path::PathBuf;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
/// The single canonical log file for the whole core. Every process (CLI daemon,
/// GUI, TUN helper) and every subsystem (tracing, the core event logger, the
/// helper IPC, panics) writes here — no more per-binary / per-subsystem sprawl
/// (`ostp-cli.log` + `ostp-core.log` + `ostp-helper.log` + `ostp-crash.log`).
pub const LOG_FILE_NAME: &str = "ostp.log";
/// Absolute path to the shared log file, next to the running executable.
pub fn log_file_path() -> PathBuf {
std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|d| d.join(LOG_FILE_NAME)))
.unwrap_or_else(|| PathBuf::from(LOG_FILE_NAME))
}
/// True if this invocation is the long-running daemon (a client/server run),
/// as opposed to a one-shot subcommand (`gk`, `check`, `init`, `-V`, ...).
///
/// Used to gate log truncation: only the daemon clears the log at startup, so a
/// one-shot command run while a daemon is live can never wipe the daemon's log.
/// A daemon invocation is simply one that carries none of the one-shot tokens
/// (`ostp`, `ostp run`, `ostp connect <url>` → daemon; everything else → one-shot).
pub fn invocation_is_daemon<I: IntoIterator<Item = String>>(args: I) -> bool {
const ONE_SHOT: &[&str] = &[
"gk", "generate-key", "check", "init", "setup", "links", "import",
"update", "migrate", "prober", "proxy-env", "proxy-env-clear",
"uninstall", "-V", "--version", "-h", "--help", "help",
];
!args
.into_iter()
.skip(1) // program name
.any(|a| ONE_SHOT.contains(&a.as_str()))
}
/// Append a single timestamped line to the shared log file. Used by the manual
/// writers (core event logger, TUN helper IPC) so their output lands in the same
/// `ostp.log` as the tracing subscriber instead of a separate file.
pub fn append_line(msg: &str) {
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(log_file_path()) {
let _ = writeln!(
file,
"[{}] {}",
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
msg
);
}
}
pub fn setup_panic_hook() {
std::panic::set_hook(Box::new(|info| {
let payload = info.payload();
@ -29,19 +76,16 @@ pub fn setup_panic_hook() {
eprintln!("{}", crash_msg);
tracing::error!("{}", crash_msg);
let path = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|d| d.join("ostp-crash.log")))
.unwrap_or_else(|| PathBuf::from("ostp-crash.log"));
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
// Crashes land in the same shared log file (append — a crash must never
// truncate, and the tracing worker may already be dead so we write direct).
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(log_file_path()) {
let _ = file.write_all(crash_msg.as_bytes());
let _ = file.write_all(b"\n===================================================\n");
}
}));
}
/// Initialises tracing and writes to `<app_name>.log` next to the executable.
/// Initialises tracing and writes to the shared `ostp.log` next to the executable.
///
/// The `level` parameter controls the minimum log level:
/// - `"error"` — only errors
@ -51,7 +95,17 @@ pub fn setup_panic_hook() {
/// - `"trace"` — all messages including very verbose internal state
///
/// The environment variable `RUST_LOG` overrides this value if set.
pub fn init_tracing(level: &str, app_name: &str, version: &str) -> Option<tracing_appender::non_blocking::WorkerGuard> {
///
/// `truncate`: clear the log at startup. Honoured **only on Windows** — Linux
/// servers keep their history (OS-rotated). Pass `true` only from the daemon's
/// own entrypoint; one-shot commands and child processes (the TUN helper) pass
/// `false` so they append instead of wiping a running daemon's log.
pub fn init_tracing(
level: &str,
app_name: &str,
version: &str,
truncate: bool,
) -> Option<tracing_appender::non_blocking::WorkerGuard> {
// RUST_LOG overrides the config-derived level
let env_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| {
@ -66,12 +120,20 @@ pub fn init_tracing(level: &str, app_name: &str, version: &str) -> Option<tracin
}
});
let path = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|d| d.join(format!("{}.log", app_name))))
.unwrap_or_else(|| PathBuf::from(format!("{}.log", app_name)));
let path = log_file_path();
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&path) {
let mut open_opts = OpenOptions::new();
open_opts.create(true);
// Truncate-on-startup is Windows-only and daemon-only. Everywhere else append:
// Linux keeps server history, and one-shot commands / the TUN helper must not
// wipe a running daemon's log.
if truncate && cfg!(windows) {
open_opts.write(true).truncate(true);
} else {
open_opts.append(true);
}
if let Ok(mut file) = open_opts.open(&path) {
// Write the startup banner directly to the log file, bypassing the
// tracing subscriber entirely. Emitting it via tracing::info!() hits
// BOTH layers below (file AND stderr), so every one-shot CLI command

View File

@ -10,10 +10,9 @@ use std::fs::OpenOptions;
use std::io::Write as _;
fn log_to_core_file(msg: &str) {
let path = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|d| d.join("ostp-core.log")))
.unwrap_or_else(|| std::path::PathBuf::from("ostp-core.log"));
// Writes into the single shared ostp.log (same file as the tracing appender),
// not a separate ostp-core.log — see logging::LOG_FILE_NAME.
let path = crate::logging::log_file_path();
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
let _ = writeln!(file, "[{}] {}", chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), msg);
}

View File

@ -8,4 +8,5 @@ pub use noise::{NoiseRole, NoiseSession};
pub use obfuscation::{
deobfuscate_header_inplace, deobfuscate_packet_inplace, obfuscate_packet_inplace,
derive_obfuscation_key, derive_psk, derive_all_secrets, DerivedSecrets,
derive_junk_marker, current_junk_window, JUNK_MARKER_WINDOW_SECS,
};

View File

@ -59,11 +59,10 @@ pub struct DerivedSecrets {
pub psk: [u8; 32],
pub handshake_pad_min: usize,
pub handshake_pad_max: usize,
/// Per-key 4-byte prefix stamped on junk frames so the server can drop them
/// without a GLOBAL constant marker (which would be a universal DPI signature
/// for all OSTP users — exactly what the version gate avoids for the handshake).
pub junk_marker: [u8; 4],
}
// NOTE: the junk marker is NOT part of DerivedSecrets — it is time-rotating and
// derived separately per window via `derive_junk_marker` (see below), so it
// carries no static per-user signature.
/// OSTP wire protocol version. Mixed into key derivation (NOT sent on the
/// wire) so peers running incompatible versions derive entirely different
@ -129,25 +128,61 @@ pub(crate) fn derive_all_secrets_versioned(access_key: &[u8], version: u8) -> De
let pad_min = 16 + (pad_bytes[0] as usize % 64); // 16-79
let pad_max = pad_min + 48 + (pad_bytes[1] as usize % 128); // +48..+175
// Derive junk marker (4 bytes) — info = key_hash[16..] || 0x04.
// Per-key: to an outsider it is indistinguishable from the random junk
// payload, so there is no cross-user signature; the server, knowing the key,
// derives the same marker and drops the junk silently.
let mut junk_info = info_base.to_vec();
junk_info.push(0x04);
let junk_bytes = hkdf_expand(&prk, &junk_info, 4);
let mut junk_marker = [0u8; 4];
junk_marker.copy_from_slice(&junk_bytes);
DerivedSecrets {
obfuscation_key,
psk,
handshake_pad_min: pad_min,
handshake_pad_max: pad_max,
junk_marker,
}
}
/// Window length (seconds) for the rotating junk marker. The marker changes
/// every window, so junk carries no static per-user fingerprint on the wire;
/// the server checks the current and previous window to absorb clock skew.
pub const JUNK_MARKER_WINDOW_SECS: u64 = 60;
/// The current junk-marker time window (unix seconds / window length).
pub fn current_junk_window() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() / JUNK_MARKER_WINDOW_SECS)
.unwrap_or(0)
}
/// Derive the 4-byte junk marker for a given time `window`.
///
/// Uses the same version-gated HKDF scheme as [`derive_all_secrets`], with the
/// window folded into the `info` (label byte `0x04`). Folding in the window
/// makes the marker rotate: to an on-path observer the junk prefix changes every
/// window (no fixed signature), and a captured marker is only valid for ~1
/// window. Only a holder of the access key can compute it, so an outsider cannot
/// forge a silently-dropped junk packet.
pub fn derive_junk_marker(access_key: &[u8], window: u64) -> [u8; 4] {
derive_junk_marker_versioned(access_key, window, PROTOCOL_VERSION)
}
pub(crate) fn derive_junk_marker_versioned(access_key: &[u8], window: u64, version: u8) -> [u8; 4] {
use sha2::Digest;
let key_hash = sha2::Sha256::digest(access_key);
let salt = &key_hash[..16];
let info_base = &key_hash[16..];
let mut ikm = Vec::with_capacity(access_key.len() + 1);
ikm.extend_from_slice(access_key);
ikm.push(version);
let prk = hkdf_extract(salt, &ikm);
// info = key_hash[16..] || 0x04 || window(LE) — same label byte as before,
// now parameterised by the time window.
let mut info = info_base.to_vec();
info.push(0x04);
info.extend_from_slice(&window.to_le_bytes());
let bytes = hkdf_expand(&prk, &info, 4);
let mut marker = [0u8; 4];
marker.copy_from_slice(&bytes);
marker
}
// ── Legacy API (delegates to derive_all_secrets) ─────────────────────────────
pub fn derive_obfuscation_key(access_key: &[u8]) -> [u8; 8] {

View File

@ -191,4 +191,29 @@ mod tests {
assert_eq!(recovered_nonce, nonce);
assert_eq!(&packet[12..], &ciphertext);
}
/// The junk marker must: be stable within a window (client and server agree),
/// rotate across windows (no static on-wire fingerprint), and differ per key
/// (one user's marker never silently-drops on another user's flow).
#[test]
fn test_junk_marker_rotation() {
let key_a = b"access-key-alpha";
let key_b = b"access-key-bravo";
// Stable within a window.
assert_eq!(derive_junk_marker(key_a, 1000), derive_junk_marker(key_a, 1000));
// Rotates across adjacent windows.
assert_ne!(derive_junk_marker(key_a, 1000), derive_junk_marker(key_a, 1001));
assert_ne!(derive_junk_marker(key_a, 1000), derive_junk_marker(key_a, 999));
// Distinct per key within the same window.
assert_ne!(derive_junk_marker(key_a, 1000), derive_junk_marker(key_b, 1000));
// A different protocol version yields a different marker (version gate).
assert_ne!(
derive_junk_marker_versioned(key_a, 1000, PROTOCOL_VERSION),
derive_junk_marker_versioned(key_a, 1000, PROTOCOL_VERSION.wrapping_add(1)),
);
}
}

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.4+16
version: 0.4.7+18
environment:
sdk: ^3.11.4

View File

@ -1,7 +1,7 @@
{
"name": "ostp-gui",
"private": true,
"version": "0.4.4",
"version": "0.4.7",
"type": "module",
"scripts": {
"tauri": "tauri",

View File

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

View File

@ -1,6 +1,6 @@
[package]
name = "ostp-gui"
version = "0.4.4"
version = "0.4.7"
description = "A Tauri App"
authors = ["you"]
edition = "2021"

View File

@ -8,7 +8,10 @@ fn main() {
// Read config BEFORE init_tracing so we can use the correct log level from config.
// If config is missing or debug=false we default to "info".
let log_level = detect_log_level_from_config();
let _log_guard = ostp_client::logging::init_tracing(&log_level, "ostp-gui", env!("CARGO_PKG_VERSION"));
// The GUI launch IS the daemon's startup, so clear the shared log here
// (Windows-only inside init_tracing). The elevated TUN helper spawned later
// passes truncate=false so it appends instead of wiping this session's log.
let _log_guard = ostp_client::logging::init_tracing(&log_level, "ostp-gui", env!("CARGO_PKG_VERSION"), true);
tracing::info!("ostp-gui starting (log_level={})", log_level);
@ -28,7 +31,7 @@ fn main() {
{
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
let msg_w: Vec<u16> = OsStr::new(&format!("OSTP GUI crashed:\n\n{}\n\nSee ostp-gui.log for details.", msg))
let msg_w: Vec<u16> = OsStr::new(&format!("OSTP GUI crashed:\n\n{}\n\nSee ostp.log for details.", msg))
.encode_wide().chain(Some(0)).collect();
let title_w: Vec<u16> = OsStr::new("OSTP GUI — Fatal Error").encode_wide().chain(Some(0)).collect();
#[link(name = "user32")] extern "system" {

View File

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

View File

@ -305,15 +305,23 @@ impl Dispatcher {
// Not an existing session — try each registered access key's derived obfuscation key
let keys_snapshot: Vec<String> = self.access_keys.read().unwrap_or_else(|e| e.into_inner()).keys().cloned().collect();
// Junk marker rotates per time window; check the current and previous
// window so a client whose clock is up to ~1 window behind/ahead is still
// recognised. Computed once per datagram, not per candidate key.
let junk_window = ostp_core::crypto::current_junk_window();
for candidate_key in keys_snapshot {
let secrets = ostp_core::crypto::derive_all_secrets(candidate_key.as_bytes());
// Junk frames carry this key's per-key derived marker (no global
// constant → no universal DPI signature). Drop silently — the secrets
// for this key are already derived here, so the check is free.
if packet.len() >= 4 && packet[0..4] == secrets.junk_marker {
// Junk frames carry this key's time-rotating marker (no global
// constant, no static per-user signature). Drop silently.
if packet.len() >= 4 {
let m_now = ostp_core::crypto::derive_junk_marker(candidate_key.as_bytes(), junk_window);
let m_prev = ostp_core::crypto::derive_junk_marker(candidate_key.as_bytes(), junk_window.wrapping_sub(1));
if packet[0..4] == m_now || packet[0..4] == m_prev {
return Ok(DispatchOutcome::Junk);
}
}
// Decode the session_id using this key's obfuscation
// The handshake mask is derived from the Noise payload at bytes [6..],

View File

@ -14,10 +14,8 @@ use portable_atomic::Ordering;
fn log_to_file(msg: &str) {
let msg = msg.to_string();
tokio::task::spawn_blocking(move || {
let path = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|d| d.join("ostp-helper.log")))
.unwrap_or_else(|| std::path::PathBuf::from("ostp-helper.log"));
// Same shared ostp.log as everything else — not a separate ostp-helper.log.
let path = ostp_client::logging::log_file_path();
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
let _ = writeln!(file, "[{}] {}", chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), msg);
}
@ -53,7 +51,10 @@ struct TunnelState {
#[tokio::main]
async fn main() -> Result<()> {
ostp_client::logging::setup_panic_hook();
let _log_guard = ostp_client::logging::init_tracing("info", "ostp-helper", env!("CARGO_PKG_VERSION"));
// The helper is a child of the GUI, which already truncated the shared log at
// its own startup — pass false so the helper APPENDS instead of wiping the
// GUI's session log.
let _log_guard = ostp_client::logging::init_tracing("info", "ostp-helper", env!("CARGO_PKG_VERSION"), false);
if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() {

View File

@ -251,7 +251,11 @@ async fn main() -> Result<()> {
// where it does not apply.
let _ = rlimit::increase_nofile_limit(1048576);
ostp_client::logging::setup_panic_hook();
let _log_guard = ostp_client::logging::init_tracing("info", "ostp-cli", env!("CARGO_PKG_VERSION"));
// Clear the shared log at startup only when THIS invocation is the daemon —
// a one-shot command (`ostp gk`, `ostp check`, ...) must not wipe a running
// daemon's log. (Truncation itself is additionally Windows-only.)
let is_daemon = ostp_client::logging::invocation_is_daemon(std::env::args());
let _log_guard = ostp_client::logging::init_tracing("info", "ostp-cli", env!("CARGO_PKG_VERSION"), is_daemon);
let res = run_app().await;
if let Err(e) = res {