mirror of https://github.com/ospab/ostp.git
Compare commits
6 Commits
1568db3323
...
61091b6d56
| Author | SHA1 | Date |
|---|---|---|
|
|
61091b6d56 | |
|
|
aa1c4ccd52 | |
|
|
5ab6833eab | |
|
|
5dc3a60017 | |
|
|
a33e5d3874 | |
|
|
e21acc2ee1 |
|
|
@ -73,19 +73,27 @@ jobs:
|
||||||
BASE_VERSION=$(grep -m1 '^version' Cargo.toml | sed -E 's/version *= *"([^"]+)"/\1/')
|
BASE_VERSION=$(grep -m1 '^version' Cargo.toml | sed -E 's/version *= *"([^"]+)"/\1/')
|
||||||
|
|
||||||
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
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 }}"
|
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 }}"
|
CHANNEL="${{ github.event.inputs.channel }}"
|
||||||
elif [ "${{ github.ref_name }}" = "alpha" ]; then
|
|
||||||
CHANNEL="alpha"
|
|
||||||
elif [ "${{ github.ref_name }}" = "pre-release" ]; then
|
elif [ "${{ github.ref_name }}" = "pre-release" ]; then
|
||||||
CHANNEL="beta"
|
CHANNEL="beta"
|
||||||
else
|
else
|
||||||
CHANNEL="alpha"
|
CHANNEL="alpha"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$CHANNEL" != "stable" ]; then
|
|
||||||
TAG="v${BASE_VERSION}-${CHANNEL}"
|
TAG="v${BASE_VERSION}-${CHANNEL}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -203,7 +211,7 @@ jobs:
|
||||||
artifact_name: ostp
|
artifact_name: ostp
|
||||||
release_name: ostp-linux-mipsle.tar.gz
|
release_name: ostp-linux-mipsle.tar.gz
|
||||||
use_cross: true
|
use_cross: true
|
||||||
toolchain: alpha
|
toolchain: nightly
|
||||||
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
target: riscv64gc-unknown-linux-gnu
|
target: riscv64gc-unknown-linux-gnu
|
||||||
|
|
|
||||||
|
|
@ -43,3 +43,5 @@ ostp-brain/
|
||||||
|
|
||||||
# Management panel built assets (built separately; dummy dist created for rust-embed build)
|
# Management panel built assets (built separately; dummy dist created for rust-embed build)
|
||||||
ostp-control/
|
ostp-control/
|
||||||
|
|
||||||
|
.agents/
|
||||||
|
|
|
||||||
|
|
@ -1384,7 +1384,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp"
|
name = "ostp"
|
||||||
version = "0.4.4"
|
version = "0.4.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|
@ -1406,7 +1406,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-client"
|
name = "ostp-client"
|
||||||
version = "0.4.4"
|
version = "0.4.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|
@ -1437,7 +1437,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-core"
|
name = "ostp-core"
|
||||||
version = "0.4.4"
|
version = "0.4.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -1471,7 +1471,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-server"
|
name = "ostp-server"
|
||||||
version = "0.4.4"
|
version = "0.4.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
|
|
@ -1503,7 +1503,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-tun"
|
name = "ostp-tun"
|
||||||
version = "0.4.4"
|
version = "0.4.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
@ -1515,7 +1515,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-tun-helper"
|
name = "ostp-tun-helper"
|
||||||
version = "0.4.4"
|
version = "0.4.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ resolver = "2"
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
version = "0.4.4"
|
version = "0.4.7"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
|
|
||||||
|
|
@ -1060,9 +1060,14 @@ impl Bridge {
|
||||||
let frag_sleep = self.frag_sleep;
|
let frag_sleep = self.frag_sleep;
|
||||||
let [junk_pc_min, junk_pc_max] = self.junk_pc;
|
let [junk_pc_min, junk_pc_max] = self.junk_pc;
|
||||||
let [junk_ps_min, junk_ps_max] = self.junk_ps;
|
let [junk_ps_min, junk_ps_max] = self.junk_ps;
|
||||||
// Per-key junk marker (derived from the access key) — NOT a global
|
// Time-rotating per-key junk marker — NOT a global constant and NOT
|
||||||
// constant, so junk frames carry no universal DPI signature.
|
// even a static per-user value: it changes every window, so junk
|
||||||
let junk_marker = ostp_core::crypto::derive_all_secrets(&self.access_key).junk_marker;
|
// 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;
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,53 @@ use std::io::Write;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
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() {
|
pub fn setup_panic_hook() {
|
||||||
std::panic::set_hook(Box::new(|info| {
|
std::panic::set_hook(Box::new(|info| {
|
||||||
let payload = info.payload();
|
let payload = info.payload();
|
||||||
|
|
@ -29,19 +76,16 @@ pub fn setup_panic_hook() {
|
||||||
eprintln!("{}", crash_msg);
|
eprintln!("{}", crash_msg);
|
||||||
tracing::error!("{}", crash_msg);
|
tracing::error!("{}", crash_msg);
|
||||||
|
|
||||||
let path = std::env::current_exe()
|
// Crashes land in the same shared log file (append — a crash must never
|
||||||
.ok()
|
// truncate, and the tracing worker may already be dead so we write direct).
|
||||||
.and_then(|p| p.parent().map(|d| d.join("ostp-crash.log")))
|
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(log_file_path()) {
|
||||||
.unwrap_or_else(|| PathBuf::from("ostp-crash.log"));
|
|
||||||
|
|
||||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
|
|
||||||
let _ = file.write_all(crash_msg.as_bytes());
|
let _ = file.write_all(crash_msg.as_bytes());
|
||||||
let _ = file.write_all(b"\n===================================================\n");
|
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:
|
/// The `level` parameter controls the minimum log level:
|
||||||
/// - `"error"` — only errors
|
/// - `"error"` — only errors
|
||||||
|
|
@ -51,7 +95,17 @@ pub fn setup_panic_hook() {
|
||||||
/// - `"trace"` — all messages including very verbose internal state
|
/// - `"trace"` — all messages including very verbose internal state
|
||||||
///
|
///
|
||||||
/// The environment variable `RUST_LOG` overrides this value if set.
|
/// 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
|
// RUST_LOG overrides the config-derived level
|
||||||
let env_filter = EnvFilter::try_from_default_env()
|
let env_filter = EnvFilter::try_from_default_env()
|
||||||
.unwrap_or_else(|_| {
|
.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()
|
let path = log_file_path();
|
||||||
.ok()
|
|
||||||
.and_then(|p| p.parent().map(|d| d.join(format!("{}.log", app_name))))
|
|
||||||
.unwrap_or_else(|| PathBuf::from(format!("{}.log", app_name)));
|
|
||||||
|
|
||||||
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
|
// Write the startup banner directly to the log file, bypassing the
|
||||||
// tracing subscriber entirely. Emitting it via tracing::info!() hits
|
// tracing subscriber entirely. Emitting it via tracing::info!() hits
|
||||||
// BOTH layers below (file AND stderr), so every one-shot CLI command
|
// BOTH layers below (file AND stderr), so every one-shot CLI command
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,9 @@ use std::fs::OpenOptions;
|
||||||
use std::io::Write as _;
|
use std::io::Write as _;
|
||||||
|
|
||||||
fn log_to_core_file(msg: &str) {
|
fn log_to_core_file(msg: &str) {
|
||||||
let path = std::env::current_exe()
|
// Writes into the single shared ostp.log (same file as the tracing appender),
|
||||||
.ok()
|
// not a separate ostp-core.log — see logging::LOG_FILE_NAME.
|
||||||
.and_then(|p| p.parent().map(|d| d.join("ostp-core.log")))
|
let path = crate::logging::log_file_path();
|
||||||
.unwrap_or_else(|| std::path::PathBuf::from("ostp-core.log"));
|
|
||||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(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);
|
let _ = writeln!(file, "[{}] {}", chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), msg);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,5 @@ pub use noise::{NoiseRole, NoiseSession};
|
||||||
pub use obfuscation::{
|
pub use obfuscation::{
|
||||||
deobfuscate_header_inplace, deobfuscate_packet_inplace, obfuscate_packet_inplace,
|
deobfuscate_header_inplace, deobfuscate_packet_inplace, obfuscate_packet_inplace,
|
||||||
derive_obfuscation_key, derive_psk, derive_all_secrets, DerivedSecrets,
|
derive_obfuscation_key, derive_psk, derive_all_secrets, DerivedSecrets,
|
||||||
|
derive_junk_marker, current_junk_window, JUNK_MARKER_WINDOW_SECS,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -59,11 +59,10 @@ pub struct DerivedSecrets {
|
||||||
pub psk: [u8; 32],
|
pub psk: [u8; 32],
|
||||||
pub handshake_pad_min: usize,
|
pub handshake_pad_min: usize,
|
||||||
pub handshake_pad_max: 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
|
/// OSTP wire protocol version. Mixed into key derivation (NOT sent on the
|
||||||
/// wire) so peers running incompatible versions derive entirely different
|
/// 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_min = 16 + (pad_bytes[0] as usize % 64); // 16-79
|
||||||
let pad_max = pad_min + 48 + (pad_bytes[1] as usize % 128); // +48..+175
|
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 {
|
DerivedSecrets {
|
||||||
obfuscation_key,
|
obfuscation_key,
|
||||||
psk,
|
psk,
|
||||||
handshake_pad_min: pad_min,
|
handshake_pad_min: pad_min,
|
||||||
handshake_pad_max: pad_max,
|
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) ─────────────────────────────
|
// ── Legacy API (delegates to derive_all_secrets) ─────────────────────────────
|
||||||
|
|
||||||
pub fn derive_obfuscation_key(access_key: &[u8]) -> [u8; 8] {
|
pub fn derive_obfuscation_key(access_key: &[u8]) -> [u8; 8] {
|
||||||
|
|
|
||||||
|
|
@ -191,4 +191,29 @@ mod tests {
|
||||||
assert_eq!(recovered_nonce, nonce);
|
assert_eq!(recovered_nonce, nonce);
|
||||||
assert_eq!(&packet[12..], &ciphertext);
|
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)),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
# 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
|
# 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.
|
# 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:
|
environment:
|
||||||
sdk: ^3.11.4
|
sdk: ^3.11.4
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "ostp-gui",
|
"name": "ostp-gui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.4.4",
|
"version": "0.4.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
|
|
|
||||||
|
|
@ -2665,7 +2665,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-client"
|
name = "ostp-client"
|
||||||
version = "0.4.4"
|
version = "0.4.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
|
@ -2696,7 +2696,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-core"
|
name = "ostp-core"
|
||||||
version = "0.4.4"
|
version = "0.4.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -2713,7 +2713,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-gui"
|
name = "ostp-gui"
|
||||||
version = "0.4.4"
|
version = "0.4.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"json_comments",
|
"json_comments",
|
||||||
|
|
@ -2733,7 +2733,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-tun"
|
name = "ostp-tun"
|
||||||
version = "0.4.4"
|
version = "0.4.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "ostp-gui"
|
name = "ostp-gui"
|
||||||
version = "0.4.4"
|
version = "0.4.7"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,10 @@ fn main() {
|
||||||
// Read config BEFORE init_tracing so we can use the correct log level from config.
|
// 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".
|
// If config is missing or debug=false we default to "info".
|
||||||
let log_level = detect_log_level_from_config();
|
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);
|
tracing::info!("ostp-gui starting (log_level={})", log_level);
|
||||||
|
|
||||||
|
|
@ -28,7 +31,7 @@ fn main() {
|
||||||
{
|
{
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::os::windows::ffi::OsStrExt;
|
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();
|
.encode_wide().chain(Some(0)).collect();
|
||||||
let title_w: Vec<u16> = OsStr::new("OSTP GUI — Fatal Error").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" {
|
#[link(name = "user32")] extern "system" {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "ostp-gui",
|
"productName": "ostp-gui",
|
||||||
"version": "0.4.4",
|
"version": "0.4.7",
|
||||||
"identifier": "com.ospab.ostp",
|
"identifier": "com.ospab.ostp",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../src"
|
"frontendDist": "../src"
|
||||||
|
|
|
||||||
|
|
@ -305,15 +305,23 @@ impl Dispatcher {
|
||||||
// Not an existing session — try each registered access key's derived obfuscation key
|
// 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();
|
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 {
|
for candidate_key in keys_snapshot {
|
||||||
let secrets = ostp_core::crypto::derive_all_secrets(candidate_key.as_bytes());
|
let secrets = ostp_core::crypto::derive_all_secrets(candidate_key.as_bytes());
|
||||||
|
|
||||||
// Junk frames carry this key's per-key derived marker (no global
|
// Junk frames carry this key's time-rotating marker (no global
|
||||||
// constant → no universal DPI signature). Drop silently — the secrets
|
// constant, no static per-user signature). Drop silently.
|
||||||
// for this key are already derived here, so the check is free.
|
if packet.len() >= 4 {
|
||||||
if packet.len() >= 4 && packet[0..4] == secrets.junk_marker {
|
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);
|
return Ok(DispatchOutcome::Junk);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Decode the session_id using this key's obfuscation
|
// Decode the session_id using this key's obfuscation
|
||||||
// The handshake mask is derived from the Noise payload at bytes [6..],
|
// The handshake mask is derived from the Noise payload at bytes [6..],
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,8 @@ use portable_atomic::Ordering;
|
||||||
fn log_to_file(msg: &str) {
|
fn log_to_file(msg: &str) {
|
||||||
let msg = msg.to_string();
|
let msg = msg.to_string();
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
let path = std::env::current_exe()
|
// Same shared ostp.log as everything else — not a separate ostp-helper.log.
|
||||||
.ok()
|
let path = ostp_client::logging::log_file_path();
|
||||||
.and_then(|p| p.parent().map(|d| d.join("ostp-helper.log")))
|
|
||||||
.unwrap_or_else(|| std::path::PathBuf::from("ostp-helper.log"));
|
|
||||||
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(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);
|
let _ = writeln!(file, "[{}] {}", chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), msg);
|
||||||
}
|
}
|
||||||
|
|
@ -53,7 +51,10 @@ struct TunnelState {
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
ostp_client::logging::setup_panic_hook();
|
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 Ok(exe) = std::env::current_exe() {
|
||||||
if let Some(dir) = exe.parent() {
|
if let Some(dir) = exe.parent() {
|
||||||
|
|
|
||||||
|
|
@ -251,7 +251,11 @@ async fn main() -> Result<()> {
|
||||||
// where it does not apply.
|
// where it does not apply.
|
||||||
let _ = rlimit::increase_nofile_limit(1048576);
|
let _ = rlimit::increase_nofile_limit(1048576);
|
||||||
ostp_client::logging::setup_panic_hook();
|
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;
|
let res = run_app().await;
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue