mirror of https://github.com/ospab/ostp.git
0.4.1: per-key junk marker, GUI polish, pre-release pipeline
security / protocol: - Derive a PER-KEY junk marker (obfuscation.rs, info byte 0x04) instead of the global constant [0x88,0x1A,0x93,0x5D]. A fixed marker was a universal DPI signature identifying ALL OSTP users at once — exactly what the HKDF version gate avoids for the handshake. Server drops junk via a new DispatchOutcome::Junk inside the existing key-trial loop (secrets already derived → zero extra cost); client stamps its own key's marker. - §E: configurable junk/fragmentation params (junk_pc / junk_ps / frag_chunk / frag_sleep). GUI (desktop): - Light theme + toggle, GUI version footer in Settings. - Fix mouse-wheel scroll on Settings (flex child needed min-height: 0). - Drop the false "process exclusions unsupported in TUN mode" warning — they DO work (native_handler maps port->process via GetExtendedTcpTable). release / infra: - build.ps1: add -PreRelease (tag CURRENT version as v<ver>-beta.N, no bump, no master commit); guard the panel build when ostp-control ships no source; bump the real ostp-gui/package.json instead of the nonexistent ostp-control one. - release.yml: mark hyphenated tags as GitHub pre-releases; don't hard-fail the web-panel step when there is no source (use committed dist/). - Versions aligned to 0.4.1; README license badge BSL 1.1 -> AGPL v3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e1bf18e653
commit
acab38c551
|
|
@ -146,11 +146,17 @@ jobs:
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
- name: Build Web Panel
|
- name: Build Web Panel (skip if no source; use committed dist/)
|
||||||
working-directory: ostp-control
|
working-directory: ostp-control
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
npm install
|
if [ -f package.json ]; then
|
||||||
npm run build
|
npm install && npm run build
|
||||||
|
else
|
||||||
|
echo "ostp-control has no package.json — using committed dist/"
|
||||||
|
mkdir -p dist
|
||||||
|
[ -f dist/index.html ] || echo '<!doctype html><title>OSTP</title>' > dist/index.html
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Rust toolchain ─────────────────────────────────────────────────────
|
# ── Rust toolchain ─────────────────────────────────────────────────────
|
||||||
- name: Setup Rust toolchain
|
- name: Setup Rust toolchain
|
||||||
|
|
@ -236,6 +242,7 @@ jobs:
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
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, '-') }}
|
||||||
files: ${{ matrix.release_name }}
|
files: ${{ matrix.release_name }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
@ -310,6 +317,7 @@ jobs:
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
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, '-') }}
|
||||||
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 }}
|
||||||
|
|
@ -370,6 +378,7 @@ jobs:
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
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, '-') }}
|
||||||
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 }}
|
||||||
|
|
@ -427,6 +436,7 @@ jobs:
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
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, '-') }}
|
||||||
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 }}
|
||||||
|
|
@ -495,6 +505,7 @@ jobs:
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
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, '-') }}
|
||||||
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 }}
|
||||||
|
|
|
||||||
|
|
@ -1384,7 +1384,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp"
|
name = "ostp"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|
@ -1406,7 +1406,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-client"
|
name = "ostp-client"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|
@ -1437,7 +1437,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-core"
|
name = "ostp-core"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -1471,7 +1471,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-server"
|
name = "ostp-server"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
|
|
@ -1503,7 +1503,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-tun"
|
name = "ostp-tun"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
@ -1515,7 +1515,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-tun-helper"
|
name = "ostp-tun-helper"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
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.0"
|
version = "0.4.1"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
[Русский язык](README.ru.md) · [Wiki](https://github.com/ospab/ostp/wiki) · [Contributing](CONTRIBUTING.md) · [Releases](https://github.com/ospab/ostp/releases)
|
[Русский язык](README.ru.md) · [Wiki](https://github.com/ospab/ostp/wiki) · [Contributing](CONTRIBUTING.md) · [Releases](https://github.com/ospab/ostp/releases)
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
[English](README.md) · [Contributing](CONTRIBUTING.ru.md)
|
[English](README.md) · [Contributing](CONTRIBUTING.ru.md)
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,10 @@ pub struct Bridge {
|
||||||
pub transport_mode: String,
|
pub transport_mode: String,
|
||||||
pub stealth_sni: String,
|
pub stealth_sni: String,
|
||||||
pub tcp_fragmentation: bool,
|
pub tcp_fragmentation: bool,
|
||||||
|
pub frag_chunk: usize,
|
||||||
|
pub frag_sleep: u64,
|
||||||
|
pub junk_pc: [usize; 2],
|
||||||
|
pub junk_ps: [usize; 2],
|
||||||
pub mtu: usize,
|
pub mtu: usize,
|
||||||
pub kill_switch: bool,
|
pub kill_switch: bool,
|
||||||
pub reload_tx: Option<watch::Sender<crate::config::ExclusionConfig>>,
|
pub reload_tx: Option<watch::Sender<crate::config::ExclusionConfig>>,
|
||||||
|
|
@ -100,6 +104,10 @@ impl Bridge {
|
||||||
transport_mode: config.transport.mode.clone(),
|
transport_mode: config.transport.mode.clone(),
|
||||||
stealth_sni: config.transport.stealth_sni.clone(),
|
stealth_sni: config.transport.stealth_sni.clone(),
|
||||||
tcp_fragmentation: config.transport.tcp_fragmentation,
|
tcp_fragmentation: config.transport.tcp_fragmentation,
|
||||||
|
frag_chunk: config.transport.frag_chunk,
|
||||||
|
frag_sleep: config.transport.frag_sleep,
|
||||||
|
junk_pc: config.transport.junk_pc,
|
||||||
|
junk_ps: config.transport.junk_ps,
|
||||||
mtu: config.ostp.mtu,
|
mtu: config.ostp.mtu,
|
||||||
kill_switch: config.kill_switch,
|
kill_switch: config.kill_switch,
|
||||||
reload_tx: None,
|
reload_tx: None,
|
||||||
|
|
@ -1027,6 +1035,10 @@ impl Bridge {
|
||||||
self.transport_mode = cfg.transport.mode.clone();
|
self.transport_mode = cfg.transport.mode.clone();
|
||||||
self.stealth_sni = cfg.transport.stealth_sni.clone();
|
self.stealth_sni = cfg.transport.stealth_sni.clone();
|
||||||
self.tcp_fragmentation = cfg.transport.tcp_fragmentation;
|
self.tcp_fragmentation = cfg.transport.tcp_fragmentation;
|
||||||
|
self.frag_chunk = cfg.transport.frag_chunk.max(1);
|
||||||
|
self.frag_sleep = cfg.transport.frag_sleep;
|
||||||
|
self.junk_pc = cfg.transport.junk_pc;
|
||||||
|
self.junk_ps = cfg.transport.junk_ps;
|
||||||
self.mtu = cfg.ostp.mtu;
|
self.mtu = cfg.ostp.mtu;
|
||||||
self.keepalive_interval_sec = cfg.ostp.keepalive_interval_sec;
|
self.keepalive_interval_sec = cfg.ostp.keepalive_interval_sec;
|
||||||
self.kill_switch = cfg.kill_switch;
|
self.kill_switch = cfg.kill_switch;
|
||||||
|
|
@ -1044,31 +1056,37 @@ impl Bridge {
|
||||||
let (mut read_half, mut write_half) = stream.into_split();
|
let (mut read_half, mut write_half) = stream.into_split();
|
||||||
|
|
||||||
let tcp_fragmentation = self.tcp_fragmentation;
|
let tcp_fragmentation = self.tcp_fragmentation;
|
||||||
|
let frag_chunk = self.frag_chunk;
|
||||||
|
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;
|
||||||
|
|
||||||
// Amnezia-style junk to perturb DPI heuristics — ONLY over stream
|
|
||||||
// transports, where each junk frame rides inside the connection. The
|
|
||||||
// server reads it as a length-prefixed frame, fails to authenticate
|
|
||||||
// it, drops it, and keeps reading (drop-and-continue), so junk does
|
|
||||||
// not break the connection. Over plain UDP each junk would be a lone
|
|
||||||
// datagram indistinguishable from a port scan (probe-flood / wasted
|
|
||||||
// CPU), so junk is NEVER sent over UDP. Ranges are hardcoded for now;
|
|
||||||
// §E will make Jc/Jmin/Jmax configurable. (Ported from 0.3.x.)
|
|
||||||
{
|
{
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
// Build all junk frames up front so ThreadRng isn't held across an
|
// Build all junk frames up front so ThreadRng isn't held across an
|
||||||
// await point (keeps this future Send).
|
// await point (keeps this future Send).
|
||||||
let junk_frames: Vec<Vec<u8>> = {
|
let junk_frames: Vec<Vec<u8>> = {
|
||||||
use rand::Rng;
|
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
let num_junk = rng.gen_range(2..=5);
|
let min_c = junk_pc_min;
|
||||||
|
let max_c = junk_pc_max.max(min_c);
|
||||||
|
let num_junk = rng.gen_range(min_c..=max_c);
|
||||||
(0..num_junk)
|
(0..num_junk)
|
||||||
.map(|_| {
|
.map(|_| {
|
||||||
let junk_len = rng.gen_range(100..=1000usize);
|
let min_s = junk_ps_min.max(1);
|
||||||
|
let max_s = junk_ps_max.max(min_s);
|
||||||
|
let junk_len = rng.gen_range(min_s..=max_s);
|
||||||
let mut frame = Vec::with_capacity(2 + junk_len);
|
let mut frame = Vec::with_capacity(2 + junk_len);
|
||||||
frame.extend_from_slice(&(junk_len as u16).to_be_bytes());
|
frame.extend_from_slice(&(junk_len as u16).to_be_bytes());
|
||||||
let start = frame.len();
|
let start = frame.len();
|
||||||
frame.resize(start + junk_len, 0);
|
frame.resize(start + junk_len, 0);
|
||||||
rng.fill(&mut frame[start..]);
|
rng.fill(&mut frame[start..]);
|
||||||
|
// Stamp this key's derived junk marker so the server drops it silently.
|
||||||
|
if junk_len >= 4 {
|
||||||
|
frame[start..start+4].copy_from_slice(&junk_marker);
|
||||||
|
}
|
||||||
frame
|
frame
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
@ -1098,9 +1116,9 @@ impl Bridge {
|
||||||
if write_half.write_all(&len_buf[1..2]).await.is_err() { break; }
|
if write_half.write_all(&len_buf[1..2]).await.is_err() { break; }
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
|
||||||
let mut broke = false;
|
let mut broke = false;
|
||||||
for chunk in data.chunks(2) {
|
for chunk in data.chunks(frag_chunk) {
|
||||||
if write_half.write_all(chunk).await.is_err() { broke = true; break; }
|
if write_half.write_all(chunk).await.is_err() { broke = true; break; }
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(2)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(frag_sleep)).await;
|
||||||
}
|
}
|
||||||
if broke { break; }
|
if broke { break; }
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -81,11 +81,26 @@ pub struct TransportConfig {
|
||||||
pub stealth_sni: String,
|
pub stealth_sni: String,
|
||||||
/// Split the first UoT/TCP packet (handshake) into tiny TCP segments to
|
/// Split the first UoT/TCP packet (handshake) into tiny TCP segments to
|
||||||
/// break DPI that inspects the first packet. UoT/TCP only; ignored for UDP.
|
/// break DPI that inspects the first packet. UoT/TCP only; ignored for UDP.
|
||||||
#[serde(default)]
|
|
||||||
pub tcp_fragmentation: bool,
|
pub tcp_fragmentation: bool,
|
||||||
|
/// TCP chunk size (bytes)
|
||||||
|
#[serde(default = "default_frag_chunk")]
|
||||||
|
pub frag_chunk: usize,
|
||||||
|
/// TCP sleep duration between chunks (ms)
|
||||||
|
#[serde(default = "default_frag_sleep")]
|
||||||
|
pub frag_sleep: u64,
|
||||||
|
/// [min, max] junk packet count
|
||||||
|
#[serde(default = "default_junk_count")]
|
||||||
|
pub junk_pc: [usize; 2],
|
||||||
|
/// [min, max] junk packet size in bytes
|
||||||
|
#[serde(default = "default_junk_size")]
|
||||||
|
pub junk_ps: [usize; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_transport_mode() -> String { "udp".to_string() }
|
fn default_transport_mode() -> String { "udp".to_string() }
|
||||||
|
fn default_frag_chunk() -> usize { 2 }
|
||||||
|
fn default_frag_sleep() -> u64 { 2 }
|
||||||
|
fn default_junk_count() -> [usize; 2] { [2, 5] }
|
||||||
|
fn default_junk_size() -> [usize; 2] { [100, 1000] }
|
||||||
|
|
||||||
impl Default for TransportConfig {
|
impl Default for TransportConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
|
@ -93,6 +108,10 @@ impl Default for TransportConfig {
|
||||||
mode: default_transport_mode(),
|
mode: default_transport_mode(),
|
||||||
stealth_sni: String::new(),
|
stealth_sni: String::new(),
|
||||||
tcp_fragmentation: false,
|
tcp_fragmentation: false,
|
||||||
|
frag_chunk: default_frag_chunk(),
|
||||||
|
frag_sleep: default_frag_sleep(),
|
||||||
|
junk_pc: default_junk_count(),
|
||||||
|
junk_ps: default_junk_size(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -175,6 +194,10 @@ struct RawTransportSection {
|
||||||
mode: Option<String>,
|
mode: Option<String>,
|
||||||
stealth_sni: Option<String>,
|
stealth_sni: Option<String>,
|
||||||
tcp_fragmentation: Option<bool>,
|
tcp_fragmentation: Option<bool>,
|
||||||
|
frag_chunk: Option<usize>,
|
||||||
|
frag_sleep: Option<u64>,
|
||||||
|
junk_pc: Option<[usize; 2]>,
|
||||||
|
junk_ps: Option<[usize; 2]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -249,6 +272,10 @@ impl ClientConfig {
|
||||||
mode: raw.transport.as_ref().and_then(|t| t.mode.clone()).unwrap_or_else(default_transport_mode),
|
mode: raw.transport.as_ref().and_then(|t| t.mode.clone()).unwrap_or_else(default_transport_mode),
|
||||||
stealth_sni: raw.transport.as_ref().and_then(|t| t.stealth_sni.clone()).unwrap_or_default(),
|
stealth_sni: raw.transport.as_ref().and_then(|t| t.stealth_sni.clone()).unwrap_or_default(),
|
||||||
tcp_fragmentation: raw.transport.as_ref().and_then(|t| t.tcp_fragmentation).unwrap_or(false),
|
tcp_fragmentation: raw.transport.as_ref().and_then(|t| t.tcp_fragmentation).unwrap_or(false),
|
||||||
|
frag_chunk: raw.transport.as_ref().and_then(|t| t.frag_chunk).unwrap_or_else(default_frag_chunk),
|
||||||
|
frag_sleep: raw.transport.as_ref().and_then(|t| t.frag_sleep).unwrap_or_else(default_frag_sleep),
|
||||||
|
junk_pc: raw.transport.as_ref().and_then(|t| t.junk_pc).unwrap_or_else(default_junk_count),
|
||||||
|
junk_ps: raw.transport.as_ref().and_then(|t| t.junk_ps).unwrap_or_else(default_junk_size),
|
||||||
},
|
},
|
||||||
exclusions: ExclusionConfig {
|
exclusions: ExclusionConfig {
|
||||||
domains: exclusions.domains.unwrap_or_default(),
|
domains: exclusions.domains.unwrap_or_default(),
|
||||||
|
|
|
||||||
|
|
@ -250,10 +250,6 @@ pub async fn run_client_core(
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
if config.mode == "tun" && !config.exclusions.processes.is_empty() {
|
|
||||||
println!("[ostp] Process exclusions are not supported in TUN mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
let (proxy_events_tx, proxy_events_rx) = mpsc::channel(256);
|
let (proxy_events_tx, proxy_events_rx) = mpsc::channel(256);
|
||||||
let (client_msgs_tx, client_msgs_rx) = mpsc::unbounded_channel();
|
let (client_msgs_tx, client_msgs_rx) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,6 +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],
|
||||||
}
|
}
|
||||||
|
|
||||||
/// OSTP wire protocol version. Mixed into key derivation (NOT sent on the
|
/// OSTP wire protocol version. Mixed into key derivation (NOT sent on the
|
||||||
|
|
@ -125,11 +129,22 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "ostp-gui",
|
"name": "ostp-gui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.4.1",
|
||||||
"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.0"
|
version = "0.4.1"
|
||||||
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.0"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -2713,7 +2713,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-gui"
|
name = "ostp-gui"
|
||||||
version = "0.1.0"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"json_comments",
|
"json_comments",
|
||||||
|
|
@ -2721,6 +2721,7 @@ dependencies = [
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"qrcode",
|
"qrcode",
|
||||||
"rand",
|
"rand",
|
||||||
|
"rlimit",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
|
|
@ -2732,7 +2733,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-tun"
|
name = "ostp-tun"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
@ -3265,6 +3266,15 @@ dependencies = [
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rlimit"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f35ee2729c56bb610f6dba436bf78135f728b7373bdffae2ec815b2d3eb98cc3"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.2"
|
version = "2.1.2"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "ostp-gui"
|
name = "ostp-gui"
|
||||||
version = "0.1.0"
|
version = "0.4.1"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,10 @@ struct TransportConfigRaw {
|
||||||
mode: Option<String>,
|
mode: Option<String>,
|
||||||
stealth_sni: Option<String>,
|
stealth_sni: Option<String>,
|
||||||
tcp_fragmentation: Option<bool>,
|
tcp_fragmentation: Option<bool>,
|
||||||
|
frag_chunk: Option<usize>,
|
||||||
|
frag_sleep: Option<u64>,
|
||||||
|
junk_pc: Option<[usize; 2]>,
|
||||||
|
junk_ps: Option<[usize; 2]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
|
@ -165,6 +169,10 @@ fn map_to_client_config(raw: &ClientConfigRaw, mode: &str) -> ostp_client::confi
|
||||||
mode: raw.transport.as_ref().and_then(|t| t.mode.clone()).unwrap_or_else(|| "udp".to_string()),
|
mode: raw.transport.as_ref().and_then(|t| t.mode.clone()).unwrap_or_else(|| "udp".to_string()),
|
||||||
stealth_sni: raw.transport.as_ref().and_then(|t| t.stealth_sni.clone()).unwrap_or_else(|| "microsoft.com".to_string()),
|
stealth_sni: raw.transport.as_ref().and_then(|t| t.stealth_sni.clone()).unwrap_or_else(|| "microsoft.com".to_string()),
|
||||||
tcp_fragmentation: raw.transport.as_ref().and_then(|t| t.tcp_fragmentation).unwrap_or(false),
|
tcp_fragmentation: raw.transport.as_ref().and_then(|t| t.tcp_fragmentation).unwrap_or(false),
|
||||||
|
frag_chunk: raw.transport.as_ref().and_then(|t| t.frag_chunk).unwrap_or(2),
|
||||||
|
frag_sleep: raw.transport.as_ref().and_then(|t| t.frag_sleep).unwrap_or(2),
|
||||||
|
junk_pc: raw.transport.as_ref().and_then(|t| t.junk_pc).unwrap_or([2, 5]),
|
||||||
|
junk_ps: raw.transport.as_ref().and_then(|t| t.junk_ps).unwrap_or([100, 1000]),
|
||||||
},
|
},
|
||||||
exclusions: ostp_client::config::ExclusionConfig {
|
exclusions: ostp_client::config::ExclusionConfig {
|
||||||
domains: raw.exclude.as_ref().and_then(|e| e.domains.clone()).unwrap_or_default(),
|
domains: raw.exclude.as_ref().and_then(|e| e.domains.clone()).unwrap_or_default(),
|
||||||
|
|
|
||||||
|
|
@ -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.2.97",
|
"version": "0.4.1",
|
||||||
"identifier": "com.ospab.ostp",
|
"identifier": "com.ospab.ostp",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../src"
|
"frontendDist": "../src"
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,14 @@
|
||||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
|
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button id="btn-theme" class="icon-btn" aria-label="Toggle theme" title="Toggle theme">
|
||||||
|
<svg id="icon-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/>
|
||||||
|
</svg>
|
||||||
|
<svg id="icon-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button id="btn-go-settings" class="icon-btn" aria-label="Settings">
|
<button id="btn-go-settings" class="icon-btn" aria-label="Settings">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<circle cx="12" cy="12" r="3"/>
|
<circle cx="12" cy="12" r="3"/>
|
||||||
|
|
@ -273,6 +281,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="app-version" id="app-version">OSTP GUI</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -336,6 +345,55 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Advanced TCP/UoT Settings (visible only if uot is selected) -->
|
||||||
|
<div id="pm-tcp-settings" style="display:none; padding: 10px; background: rgba(0,0,0,0.2); border-radius: 8px; margin-bottom: 15px;">
|
||||||
|
<div class="toggle-row" style="padding:0; border:none; margin-bottom:10px;">
|
||||||
|
<div class="toggle-text">
|
||||||
|
<span class="toggle-name">TCP Fragmentation</span>
|
||||||
|
<span class="toggle-hint">Split handshake to bypass DPI</span>
|
||||||
|
</div>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" id="pm-tcp-frag" />
|
||||||
|
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pm-frag-details" style="display:none;">
|
||||||
|
<div style="display:flex; gap:10px; margin-bottom:10px;">
|
||||||
|
<div class="inline-field" style="padding:0; border:none; flex:1;">
|
||||||
|
<span class="field-label">Chunk Size</span>
|
||||||
|
<input id="pm-frag-chunk" class="field-input compact" type="number" placeholder="2" min="1" />
|
||||||
|
</div>
|
||||||
|
<div class="inline-field" style="padding:0; border:none; flex:1;">
|
||||||
|
<span class="field-label">Sleep (ms)</span>
|
||||||
|
<input id="pm-frag-sleep" class="field-input compact" type="number" placeholder="2" min="0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-divider-mini" style="margin-top:0;"><span>Junk Packets</span></div>
|
||||||
|
<div style="display:flex; gap:10px; margin-bottom:10px;">
|
||||||
|
<div class="inline-field" style="padding:0; border:none; flex:1;">
|
||||||
|
<span class="field-label">Count (Min)</span>
|
||||||
|
<input id="pm-junk-pc-min" class="field-input compact" type="number" placeholder="2" min="0" />
|
||||||
|
</div>
|
||||||
|
<div class="inline-field" style="padding:0; border:none; flex:1;">
|
||||||
|
<span class="field-label">Count (Max)</span>
|
||||||
|
<input id="pm-junk-pc-max" class="field-input compact" type="number" placeholder="5" min="0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; gap:10px;">
|
||||||
|
<div class="inline-field" style="padding:0; border:none; flex:1;">
|
||||||
|
<span class="field-label">Size (Min)</span>
|
||||||
|
<input id="pm-junk-ps-min" class="field-input compact" type="number" placeholder="100" min="0" />
|
||||||
|
</div>
|
||||||
|
<div class="inline-field" style="padding:0; border:none; flex:1;">
|
||||||
|
<span class="field-label">Size (Max)</span>
|
||||||
|
<input id="pm-junk-ps-max" class="field-input compact" type="number" placeholder="1000" min="0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button id="btn-profile-cancel" class="btn secondary">Cancel</button>
|
<button id="btn-profile-cancel" class="btn secondary">Cancel</button>
|
||||||
<button id="btn-profile-delete" class="btn danger" style="display:none;">Delete</button>
|
<button id="btn-profile-delete" class="btn danger" style="display:none;">Delete</button>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,11 @@ if (window.__TAURI__?.core) {
|
||||||
invoke = window.__TAURI__.core.invoke;
|
invoke = window.__TAURI__.core.invoke;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Theme: apply saved theme ASAP (before first paint) to avoid a flash ─
|
||||||
|
if (localStorage.getItem('ostp_theme') === 'light') {
|
||||||
|
document.documentElement.classList.add('light');
|
||||||
|
}
|
||||||
|
|
||||||
// ── PROFILE STORE ─────────────────────────────────────────────────────
|
// ── PROFILE STORE ─────────────────────────────────────────────────────
|
||||||
// Profiles are stored in localStorage only — the core never knows about them.
|
// Profiles are stored in localStorage only — the core never knows about them.
|
||||||
// Only the active profile is compiled into a config and passed to Tauri.
|
// Only the active profile is compiled into a config and passed to Tauri.
|
||||||
|
|
@ -108,6 +113,15 @@ const pmName = $('pm-name');
|
||||||
const pmServer = $('pm-server');
|
const pmServer = $('pm-server');
|
||||||
const pmKey = $('pm-key');
|
const pmKey = $('pm-key');
|
||||||
const pmTransport = $('pm-transport');
|
const pmTransport = $('pm-transport');
|
||||||
|
const pmTcpFrag = $('pm-tcp-frag');
|
||||||
|
const pmFragChunk = $('pm-frag-chunk');
|
||||||
|
const pmFragSleep = $('pm-frag-sleep');
|
||||||
|
const pmJunkPcMin = $('pm-junk-pc-min');
|
||||||
|
const pmJunkPcMax = $('pm-junk-pc-max');
|
||||||
|
const pmJunkPsMin = $('pm-junk-ps-min');
|
||||||
|
const pmJunkPsMax = $('pm-junk-ps-max');
|
||||||
|
const pmTcpSettings = $('pm-tcp-settings');
|
||||||
|
const pmFragDetails = $('pm-frag-details');
|
||||||
const btnProfileCancel = $('btn-profile-cancel');
|
const btnProfileCancel = $('btn-profile-cancel');
|
||||||
const btnProfileSave = $('btn-profile-save');
|
const btnProfileSave = $('btn-profile-save');
|
||||||
const btnProfileDelete = $('btn-profile-delete');
|
const btnProfileDelete = $('btn-profile-delete');
|
||||||
|
|
@ -291,6 +305,11 @@ function buildConfig() {
|
||||||
debug: !!s.debug,
|
debug: !!s.debug,
|
||||||
transport: {
|
transport: {
|
||||||
mode: active.transport || 'udp',
|
mode: active.transport || 'udp',
|
||||||
|
tcp_fragmentation: !!active.tcp_fragmentation,
|
||||||
|
frag_chunk: active.frag_chunk || 2,
|
||||||
|
frag_sleep: active.frag_sleep || 2,
|
||||||
|
junk_pc: active.junk_pc || [2, 5],
|
||||||
|
junk_ps: active.junk_ps || [100, 1000]
|
||||||
},
|
},
|
||||||
tun: {
|
tun: {
|
||||||
enable: !!s.tun,
|
enable: !!s.tun,
|
||||||
|
|
@ -485,15 +504,31 @@ function openProfileEditor(id) {
|
||||||
pmServer.value = p.server || '';
|
pmServer.value = p.server || '';
|
||||||
pmKey.value = p.key || '';
|
pmKey.value = p.key || '';
|
||||||
pmTransport.value = p.transport || 'udp';
|
pmTransport.value = p.transport || 'udp';
|
||||||
|
pmTcpFrag.checked = !!p.tcp_fragmentation;
|
||||||
|
pmFragChunk.value = p.frag_chunk || 2;
|
||||||
|
pmFragSleep.value = p.frag_sleep || 2;
|
||||||
|
pmJunkPcMin.value = p.junk_pc ? p.junk_pc[0] : 2;
|
||||||
|
pmJunkPcMax.value = p.junk_pc ? p.junk_pc[1] : 5;
|
||||||
|
pmJunkPsMin.value = p.junk_ps ? p.junk_ps[0] : 100;
|
||||||
|
pmJunkPsMax.value = p.junk_ps ? p.junk_ps[1] : 1000;
|
||||||
btnProfileDelete.style.display = '';
|
btnProfileDelete.style.display = '';
|
||||||
} else {
|
} else {
|
||||||
profileModalTitle.textContent = 'New Profile';
|
profileModalTitle.textContent = 'New Profile';
|
||||||
pmName.value = pmServer.value = pmKey.value = '';
|
pmName.value = pmServer.value = pmKey.value = '';
|
||||||
pmTransport.value = 'udp';
|
pmTransport.value = 'udp';
|
||||||
|
pmTcpFrag.checked = false;
|
||||||
|
pmFragChunk.value = 2;
|
||||||
|
pmFragSleep.value = 2;
|
||||||
|
pmJunkPcMin.value = 2;
|
||||||
|
pmJunkPcMax.value = 5;
|
||||||
|
pmJunkPsMin.value = 100;
|
||||||
|
pmJunkPsMax.value = 1000;
|
||||||
btnProfileDelete.style.display = 'none';
|
btnProfileDelete.style.display = 'none';
|
||||||
}
|
}
|
||||||
pmKey.type = 'password';
|
pmKey.type = 'password';
|
||||||
profileModal.classList.remove('hidden');
|
profileModal.classList.remove('hidden');
|
||||||
|
pmTransport.dispatchEvent(new Event('change'));
|
||||||
|
pmTcpFrag.dispatchEvent(new Event('change'));
|
||||||
setTimeout(() => pmName.focus(), 80);
|
setTimeout(() => pmName.focus(), 80);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -511,6 +546,11 @@ function saveProfileFromEditor() {
|
||||||
server,
|
server,
|
||||||
key,
|
key,
|
||||||
transport: pmTransport.value,
|
transport: pmTransport.value,
|
||||||
|
tcp_fragmentation: pmTcpFrag.checked,
|
||||||
|
frag_chunk: parseInt(pmFragChunk.value) || 2,
|
||||||
|
frag_sleep: parseInt(pmFragSleep.value) || 2,
|
||||||
|
junk_pc: [parseInt(pmJunkPcMin.value)||2, parseInt(pmJunkPcMax.value)||5],
|
||||||
|
junk_ps: [parseInt(pmJunkPsMin.value)||100, parseInt(pmJunkPsMax.value)||1000],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -520,6 +560,11 @@ function saveProfileFromEditor() {
|
||||||
server,
|
server,
|
||||||
key,
|
key,
|
||||||
transport: pmTransport.value,
|
transport: pmTransport.value,
|
||||||
|
tcp_fragmentation: pmTcpFrag.checked,
|
||||||
|
frag_chunk: parseInt(pmFragChunk.value) || 2,
|
||||||
|
frag_sleep: parseInt(pmFragSleep.value) || 2,
|
||||||
|
junk_pc: [parseInt(pmJunkPcMin.value)||2, parseInt(pmJunkPcMax.value)||5],
|
||||||
|
junk_ps: [parseInt(pmJunkPsMin.value)||100, parseInt(pmJunkPsMax.value)||1000],
|
||||||
};
|
};
|
||||||
profiles.push(p);
|
profiles.push(p);
|
||||||
if (!activeId) { activeId = p.id; saveActiveId(activeId); }
|
if (!activeId) { activeId = p.id; saveActiveId(activeId); }
|
||||||
|
|
@ -707,6 +752,24 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||||
btnGoSettings.addEventListener('click', () => showScreen('settings'));
|
btnGoSettings.addEventListener('click', () => showScreen('settings'));
|
||||||
btnBack.addEventListener('click', () => showScreen('home'));
|
btnBack.addEventListener('click', () => showScreen('home'));
|
||||||
|
|
||||||
|
// Theme toggle (dark ⇄ light), persisted in localStorage
|
||||||
|
const btnTheme = $('btn-theme');
|
||||||
|
if (btnTheme) btnTheme.addEventListener('click', () => {
|
||||||
|
const isLight = document.documentElement.classList.toggle('light');
|
||||||
|
localStorage.setItem('ostp_theme', isLight ? 'light' : 'dark');
|
||||||
|
});
|
||||||
|
|
||||||
|
// GUI version shown at the bottom of Settings
|
||||||
|
const appVersionEl = $('app-version');
|
||||||
|
if (appVersionEl) {
|
||||||
|
const setV = v => { appVersionEl.textContent = 'OSTP GUI v' + v; };
|
||||||
|
if (window.__TAURI__?.app?.getVersion) {
|
||||||
|
window.__TAURI__.app.getVersion().then(setV).catch(() => setV('0.4.1'));
|
||||||
|
} else {
|
||||||
|
setV('0.4.1');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add-profile button → dropdown
|
// Add-profile button → dropdown
|
||||||
btnAddProfile.addEventListener('click', e => {
|
btnAddProfile.addEventListener('click', e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -757,6 +820,12 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||||
btnProfileCancel.addEventListener('click', () => profileModal.classList.add('hidden'));
|
btnProfileCancel.addEventListener('click', () => profileModal.classList.add('hidden'));
|
||||||
btnProfileSave.addEventListener('click', saveProfileFromEditor);
|
btnProfileSave.addEventListener('click', saveProfileFromEditor);
|
||||||
btnProfileDelete.addEventListener('click', deleteEditingProfile);
|
btnProfileDelete.addEventListener('click', deleteEditingProfile);
|
||||||
|
pmTransport.addEventListener('change', () => {
|
||||||
|
pmTcpSettings.style.display = pmTransport.value === 'uot' ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
pmTcpFrag.addEventListener('change', () => {
|
||||||
|
pmFragDetails.style.display = pmTcpFrag.checked ? 'block' : 'none';
|
||||||
|
});
|
||||||
btnPeekPm.addEventListener('click', () => {
|
btnPeekPm.addEventListener('click', () => {
|
||||||
pmKey.type = pmKey.type === 'password' ? 'text' : 'password';
|
pmKey.type = pmKey.type === 'password' ? 'text' : 'password';
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,45 @@
|
||||||
color: var(--c-txt-1);
|
color: var(--c-txt-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Light theme (inverted monochrome) ──────────────────────────────── */
|
||||||
|
:root.light {
|
||||||
|
--c-bg: #f4f4f5;
|
||||||
|
--c-surface: #ffffff;
|
||||||
|
--c-card: rgba(0,0,0,0.03);
|
||||||
|
--c-card-border: rgba(0,0,0,0.10);
|
||||||
|
--c-card-hover: rgba(0,0,0,0.05);
|
||||||
|
--c-accent: #18181b;
|
||||||
|
--c-accent-dim: rgba(0,0,0,0.08);
|
||||||
|
--c-accent-glow: rgba(0,0,0,0.14);
|
||||||
|
--c-green: #18181b;
|
||||||
|
--c-green-glow: rgba(0,0,0,0.16);
|
||||||
|
--c-green-dim: rgba(0,0,0,0.05);
|
||||||
|
--c-red: #dc2626;
|
||||||
|
--c-amber: #d97706;
|
||||||
|
--c-txt-1: #18181b;
|
||||||
|
--c-txt-2: #71717a;
|
||||||
|
--c-txt-3: #c4c4cc;
|
||||||
|
color: var(--c-txt-1);
|
||||||
|
}
|
||||||
|
/* Eagle watermark is a light asset — invert it so it stays visible on light bg */
|
||||||
|
:root.light .watermark img { filter: invert(1); }
|
||||||
|
|
||||||
|
/* Theme toggle: show the icon of the theme you'd switch TO */
|
||||||
|
#icon-moon { display: none; }
|
||||||
|
#icon-sun { display: block; }
|
||||||
|
:root.light #icon-moon { display: block; }
|
||||||
|
:root.light #icon-sun { display: none; }
|
||||||
|
|
||||||
|
/* GUI version footer at the bottom of Settings */
|
||||||
|
.app-version {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--c-txt-2);
|
||||||
|
opacity: 0.7;
|
||||||
|
padding: 16px 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Reset ───────────────────────────────────────────────────────────── */
|
/* ── Reset ───────────────────────────────────────────────────────────── */
|
||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
html, body { width: 100%; height: 100%; background: var(--c-bg); overflow: hidden; user-select: none; }
|
html, body { width: 100%; height: 100%; background: var(--c-bg); overflow: hidden; user-select: none; }
|
||||||
|
|
@ -368,6 +407,7 @@ a { text-decoration: none; }
|
||||||
/* ── Settings body ───────────────────────────────────────────────────── */
|
/* ── Settings body ───────────────────────────────────────────────────── */
|
||||||
.settings-body {
|
.settings-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ const MAX_SESSIONS: usize = 1024;
|
||||||
|
|
||||||
pub enum DispatchOutcome {
|
pub enum DispatchOutcome {
|
||||||
Unauthorized,
|
Unauthorized,
|
||||||
|
/// Packet matched a registered key's per-key junk marker — drop silently.
|
||||||
|
Junk,
|
||||||
Accepted {
|
Accepted {
|
||||||
responses: Vec<Bytes>,
|
responses: Vec<Bytes>,
|
||||||
app_payloads: Vec<(u32, u16, Bytes)>, // session_id, stream_id, payload
|
app_payloads: Vec<(u32, u16, Bytes)>, // session_id, stream_id, payload
|
||||||
|
|
@ -306,6 +308,13 @@ impl Dispatcher {
|
||||||
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
|
||||||
|
// 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 {
|
||||||
|
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..],
|
||||||
// so we must deobfuscate the full packet, not just the header.
|
// so we must deobfuscate the full packet, not just the header.
|
||||||
|
|
|
||||||
|
|
@ -551,7 +551,8 @@ async fn handle_udp_packet(
|
||||||
last_empty_app_log: &mut Instant,
|
last_empty_app_log: &mut Instant,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let size = packet.len();
|
let size = packet.len();
|
||||||
match dispatcher.on_datagram(peer, packet) {
|
match dispatcher.on_datagram(peer, packet.clone()) {
|
||||||
|
Ok(DispatchOutcome::Junk) => return Ok(()),
|
||||||
Ok(DispatchOutcome::Unauthorized) => {
|
Ok(DispatchOutcome::Unauthorized) => {
|
||||||
let _ = ui_event_tx.send(UiEvent::UnauthorizedProbe { peer: peer.ip(), bytes: size });
|
let _ = ui_event_tx.send(UiEvent::UnauthorizedProbe { peer: peer.ip(), bytes: size });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1630,6 +1630,10 @@ async fn run_client_directly(client_cfg: ClientConfig) -> Result<()> {
|
||||||
mode: client_cfg.transport.as_ref().and_then(|t| t.mode.clone()).unwrap_or_else(|| "udp".to_string()),
|
mode: client_cfg.transport.as_ref().and_then(|t| t.mode.clone()).unwrap_or_else(|| "udp".to_string()),
|
||||||
stealth_sni: client_cfg.transport.as_ref().and_then(|t| t.stealth_sni.clone()).unwrap_or_else(|| "microsoft.com".to_string()),
|
stealth_sni: client_cfg.transport.as_ref().and_then(|t| t.stealth_sni.clone()).unwrap_or_else(|| "microsoft.com".to_string()),
|
||||||
tcp_fragmentation: client_cfg.transport.as_ref().and_then(|t| t.tcp_fragmentation).unwrap_or(false),
|
tcp_fragmentation: client_cfg.transport.as_ref().and_then(|t| t.tcp_fragmentation).unwrap_or(false),
|
||||||
|
frag_chunk: 2,
|
||||||
|
frag_sleep: 2,
|
||||||
|
junk_pc: [2, 5],
|
||||||
|
junk_ps: [100, 1000],
|
||||||
},
|
},
|
||||||
dns_server: client_cfg.tun.as_ref().and_then(|t| t.dns.clone()),
|
dns_server: client_cfg.tun.as_ref().and_then(|t| t.dns.clone()),
|
||||||
kill_switch: client_cfg.tun.as_ref().and_then(|t| t.kill_switch).unwrap_or(false),
|
kill_switch: client_cfg.tun.as_ref().and_then(|t| t.kill_switch).unwrap_or(false),
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
# OSTP Build & Release Pipeline
|
# OSTP Build & Release Pipeline
|
||||||
# Usage:
|
# Usage:
|
||||||
# .\scripts\build.ps1 Build locally + trigger CI/CD
|
# .\scripts\build.ps1 Build locally + trigger CI/CD (stable release)
|
||||||
# .\scripts\build.ps1 -TriggerOnly Skip local builds, trigger CI/CD only
|
# .\scripts\build.ps1 -TriggerOnly Skip local builds, trigger CI/CD only
|
||||||
|
# .\scripts\build.ps1 -TriggerOnly -PreRelease Beta: tag CURRENT version as pre-release (no bump, no master commit)
|
||||||
# .\scripts\build.ps1 -Check Run cargo check only (no build, no release)
|
# .\scripts\build.ps1 -Check Run cargo check only (no build, no release)
|
||||||
|
|
||||||
param(
|
param(
|
||||||
[switch]$Flatten,
|
[switch]$Flatten,
|
||||||
[switch]$TriggerOnly,
|
[switch]$TriggerOnly,
|
||||||
[switch]$Check
|
[switch]$Check,
|
||||||
|
[switch]$PreRelease
|
||||||
)
|
)
|
||||||
|
|
||||||
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
|
@ -17,22 +19,27 @@ Push-Location $ProjectRoot
|
||||||
Write-Output "Synchronizing with origin master..."
|
Write-Output "Synchronizing with origin master..."
|
||||||
& git pull origin master --rebase --autostash | Out-Null
|
& git pull origin master --rebase --autostash | Out-Null
|
||||||
|
|
||||||
# --- Version bump ---
|
# --- Version resolution / bump ---
|
||||||
$CargoToml = Join-Path $ProjectRoot "Cargo.toml"
|
$CargoToml = Join-Path $ProjectRoot "Cargo.toml"
|
||||||
$Version = "0.2.0"
|
$Version = "0.2.0"
|
||||||
if (Test-Path $CargoToml) {
|
$Content = if (Test-Path $CargoToml) { [System.IO.File]::ReadAllText($CargoToml) } else { "" }
|
||||||
$Content = [System.IO.File]::ReadAllText($CargoToml)
|
|
||||||
# Match version only in [workspace.package] section (first occurrence)
|
if ($Content -match '\[workspace\.package\][\s\S]*?version\s*=\s*"(\d+)\.(\d+)\.(\d+)"') {
|
||||||
if ($Content -match '\[workspace\.package\][\s\S]*?version\s*=\s*"(\d+)\.(\d+)\.(\d+)"') {
|
|
||||||
$Major = [int]$Matches[1]
|
$Major = [int]$Matches[1]
|
||||||
$Minor = [int]$Matches[2]
|
$Minor = [int]$Matches[2]
|
||||||
$Patch = [int]$Matches[3]
|
$Patch = [int]$Matches[3]
|
||||||
|
|
||||||
|
if ($PreRelease) {
|
||||||
|
# Beta: build the CURRENT version as a pre-release. No bump, no manifest rewrites.
|
||||||
|
$Version = "{0}.{1}.{2}" -f $Major, $Minor, $Patch
|
||||||
|
Write-Output "[ok] Pre-release build of current v$Version (no version bump)"
|
||||||
|
} else {
|
||||||
$NewPatch = $Patch + 1
|
$NewPatch = $Patch + 1
|
||||||
$Version = "{0}.{1}.{2}" -f $Major, $Minor, $NewPatch
|
$Version = "{0}.{1}.{2}" -f $Major, $Minor, $NewPatch
|
||||||
# Replace only the workspace version line, not dependency versions
|
|
||||||
|
# Replace only the workspace version line (first occurrence), not dependency versions
|
||||||
$OldVersionStr = 'version = "{0}.{1}.{2}"' -f $Major, $Minor, $Patch
|
$OldVersionStr = 'version = "{0}.{1}.{2}"' -f $Major, $Minor, $Patch
|
||||||
$NewVersionStr = 'version = "' + $Version + '"'
|
$NewVersionStr = 'version = "' + $Version + '"'
|
||||||
# Use .NET Replace to swap only the first occurrence
|
|
||||||
$idx = $Content.IndexOf($OldVersionStr)
|
$idx = $Content.IndexOf($OldVersionStr)
|
||||||
if ($idx -ge 0) {
|
if ($idx -ge 0) {
|
||||||
$NewContent = $Content.Remove($idx, $OldVersionStr.Length).Insert($idx, $NewVersionStr)
|
$NewContent = $Content.Remove($idx, $OldVersionStr.Length).Insert($idx, $NewVersionStr)
|
||||||
|
|
@ -40,24 +47,22 @@ if (Test-Path $CargoToml) {
|
||||||
}
|
}
|
||||||
Write-Output "[ok] Version: v$Version"
|
Write-Output "[ok] Version: v$Version"
|
||||||
|
|
||||||
# Bump Tauri GUI
|
# Bump Tauri GUI config
|
||||||
$TauriConf = Join-Path $ProjectRoot "ostp-gui\src-tauri\tauri.conf.json"
|
$TauriConf = Join-Path $ProjectRoot "ostp-gui\src-tauri\tauri.conf.json"
|
||||||
if (Test-Path $TauriConf) {
|
if (Test-Path $TauriConf) {
|
||||||
$TauriContent = [System.IO.File]::ReadAllText($TauriConf)
|
$TauriContent = [System.IO.File]::ReadAllText($TauriConf)
|
||||||
$TauriRegex = [regex] '"version":\s*"[^"]+"'
|
$TauriContent = ([regex]'"version":\s*"[^"]+"').Replace($TauriContent, ('"version": "' + $Version + '"'), 1)
|
||||||
$TauriContent = $TauriRegex.Replace($TauriContent, ('"version": "' + $Version + '"'), 1)
|
|
||||||
[System.IO.File]::WriteAllText($TauriConf, $TauriContent)
|
[System.IO.File]::WriteAllText($TauriConf, $TauriContent)
|
||||||
Write-Output " [ok] Updated tauri.conf.json"
|
Write-Output " [ok] Updated tauri.conf.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Bump React Control Panel
|
# Bump GUI package.json
|
||||||
$PackageJson = Join-Path $ProjectRoot "ostp-control\package.json"
|
$GuiPkg = Join-Path $ProjectRoot "ostp-gui\package.json"
|
||||||
if (Test-Path $PackageJson) {
|
if (Test-Path $GuiPkg) {
|
||||||
$PkgContent = [System.IO.File]::ReadAllText($PackageJson)
|
$GuiContent = [System.IO.File]::ReadAllText($GuiPkg)
|
||||||
$PkgRegex = [regex] '"version":\s*"[^"]+"'
|
$GuiContent = ([regex]'"version":\s*"[^"]+"').Replace($GuiContent, ('"version": "' + $Version + '"'), 1)
|
||||||
$PkgContent = $PkgRegex.Replace($PkgContent, ('"version": "' + $Version + '"'), 1)
|
[System.IO.File]::WriteAllText($GuiPkg, $GuiContent)
|
||||||
[System.IO.File]::WriteAllText($PackageJson, $PkgContent)
|
Write-Output " [ok] Updated ostp-gui/package.json"
|
||||||
Write-Output " [ok] Updated package.json"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Bump Flutter App
|
# Bump Flutter App
|
||||||
|
|
@ -66,8 +71,7 @@ if (Test-Path $CargoToml) {
|
||||||
$PubContent = [System.IO.File]::ReadAllText($Pubspec)
|
$PubContent = [System.IO.File]::ReadAllText($Pubspec)
|
||||||
if ($PubContent -match 'version:\s*(\d+\.\d+\.\d+)\+(\d+)') {
|
if ($PubContent -match 'version:\s*(\d+\.\d+\.\d+)\+(\d+)') {
|
||||||
$BuildNumber = [int]$Matches[2] + 1
|
$BuildNumber = [int]$Matches[2] + 1
|
||||||
$PubRegex = [regex] 'version:\s*\d+\.\d+\.\d+\+\d+'
|
$PubContent = ([regex]'version:\s*\d+\.\d+\.\d+\+\d+').Replace($PubContent, ("version: $Version+$BuildNumber"), 1)
|
||||||
$PubContent = $PubRegex.Replace($PubContent, ("version: $Version+$BuildNumber"), 1)
|
|
||||||
[System.IO.File]::WriteAllText($Pubspec, $PubContent)
|
[System.IO.File]::WriteAllText($Pubspec, $PubContent)
|
||||||
Write-Output " [ok] Updated pubspec.yaml"
|
Write-Output " [ok] Updated pubspec.yaml"
|
||||||
}
|
}
|
||||||
|
|
@ -75,13 +79,18 @@ if (Test-Path $CargoToml) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Pre-flight: frontend build ---
|
# --- Pre-flight: frontend build (only if the panel ships source) ---
|
||||||
Write-Output ""
|
$ControlDir = Join-Path $ProjectRoot "ostp-control"
|
||||||
Write-Output "Building frontend control panel..."
|
if (Test-Path (Join-Path $ControlDir "package.json")) {
|
||||||
Push-Location (Join-Path $ProjectRoot "ostp-control")
|
Write-Output ""
|
||||||
& npm install | Out-Null
|
Write-Output "Building frontend control panel..."
|
||||||
& npm run build | Out-Null
|
Push-Location $ControlDir
|
||||||
Pop-Location
|
& npm install | Out-Null
|
||||||
|
& npm run build | Out-Null
|
||||||
|
Pop-Location
|
||||||
|
} else {
|
||||||
|
Write-Output "[skip] ostp-control has no package.json — using prebuilt dist/."
|
||||||
|
}
|
||||||
|
|
||||||
# --- Pre-flight: cargo check ---
|
# --- Pre-flight: cargo check ---
|
||||||
Write-Output ""
|
Write-Output ""
|
||||||
|
|
@ -259,25 +268,46 @@ if (-not $TriggerOnly) {
|
||||||
Write-Output ""
|
Write-Output ""
|
||||||
Write-Output "--- Phase 3: CI/CD release ---"
|
Write-Output "--- Phase 3: CI/CD release ---"
|
||||||
|
|
||||||
Write-Output "Pushing version metadata..."
|
if ($PreRelease) {
|
||||||
& git add Cargo.toml Cargo.lock
|
# Beta: tag the CURRENT commit as a pre-release. Do NOT bump/commit master.
|
||||||
& git commit -m "CI/CD: release version v$Version" --allow-empty | Out-Null
|
# The workflow marks any tag containing '-' as a GitHub pre-release.
|
||||||
& git push origin master | Out-Null
|
$existingBetas = @(& git tag -l "v$Version-beta.*")
|
||||||
|
$BetaNum = $existingBetas.Count + 1
|
||||||
|
$Tag = "v$Version-beta.$BetaNum"
|
||||||
|
Write-Output "Creating pre-release tag: $Tag"
|
||||||
|
& git tag $Tag
|
||||||
|
Write-Output "Pushing tag to GitHub..."
|
||||||
|
& git push origin $Tag
|
||||||
|
|
||||||
Write-Output "Creating release tag: v$Version"
|
if ($LASTEXITCODE -eq 0) {
|
||||||
& git tag -d "v$Version" 2>&1 | Out-Null
|
Write-Output ""
|
||||||
& git tag "v$Version"
|
Write-Output "[ok] Pre-release $Tag triggered on GitHub Actions (marked as pre-release)."
|
||||||
|
Write-Output " Monitor: https://github.com/ospab/ostp/actions"
|
||||||
|
} else {
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output "[error] Failed to push pre-release tag."
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Output "Pushing version metadata..."
|
||||||
|
& git add Cargo.toml Cargo.lock
|
||||||
|
& git commit -m "CI/CD: release version v$Version" --allow-empty | Out-Null
|
||||||
|
& git push origin master | Out-Null
|
||||||
|
|
||||||
Write-Output "Pushing tag to GitHub..."
|
Write-Output "Creating release tag: v$Version"
|
||||||
& git push origin "v$Version" --force
|
& git tag -d "v$Version" 2>&1 | Out-Null
|
||||||
|
& git tag "v$Version"
|
||||||
|
|
||||||
if ($LASTEXITCODE -eq 0) {
|
Write-Output "Pushing tag to GitHub..."
|
||||||
|
& git push origin "v$Version" --force
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
Write-Output ""
|
Write-Output ""
|
||||||
Write-Output "[ok] Release v$Version triggered on GitHub Actions."
|
Write-Output "[ok] Release v$Version triggered on GitHub Actions."
|
||||||
Write-Output " Monitor: https://github.com/ospab/ostp/actions"
|
Write-Output " Monitor: https://github.com/ospab/ostp/actions"
|
||||||
} else {
|
} else {
|
||||||
Write-Output ""
|
Write-Output ""
|
||||||
Write-Output "[error] Failed to push release tag."
|
Write-Output "[error] Failed to push release tag."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Pop-Location
|
Pop-Location
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue