mirror of https://github.com/ospab/ostp.git
Compare commits
2 Commits
39127d30f3
...
acab38c551
| Author | SHA1 | Date |
|---|---|---|
|
|
acab38c551 | |
|
|
e1bf18e653 |
|
|
@ -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,
|
||||||
|
|
@ -342,7 +350,7 @@ impl Bridge {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if is_uot {
|
if is_uot {
|
||||||
// TCP is dead — drop sender to signal bridge via channel close
|
// TCP is dead — drop sender to signal bridge via channel close
|
||||||
tracing::warn!("UoT session {} disconnected: {}", session_index, e);
|
tracing::debug!("UoT session {} disconnected: {}", session_index, e);
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!("UDP socket recv error (session {}): {}", session_index, e);
|
tracing::warn!("UDP socket recv error (session {}): {}", session_index, e);
|
||||||
|
|
@ -436,7 +444,7 @@ impl Bridge {
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if is_uot {
|
if is_uot {
|
||||||
tracing::warn!("UoT network-change session {} disconnected: {}", session_index, e);
|
tracing::debug!("UoT network-change session {} disconnected: {}", session_index, e);
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!("UDP recv error (network-change session {}): {}", session_index, e);
|
tracing::warn!("UDP recv error (network-change session {}): {}", session_index, e);
|
||||||
|
|
@ -574,7 +582,7 @@ impl Bridge {
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if is_uot {
|
if is_uot {
|
||||||
tracing::warn!("UoT reconnect session {} disconnected: {}", session_index, e);
|
tracing::debug!("UoT reconnect session {} disconnected: {}", session_index, e);
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!("UDP socket recv error (reconnect session {}): {}", session_index, e);
|
tracing::warn!("UDP socket recv error (reconnect session {}): {}", session_index, e);
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,7 @@ fn refresh_wininet() {
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
pub fn enable_system_proxy(proxy_addr: &str) {
|
pub fn enable_system_proxy(proxy_addr: &str) {
|
||||||
let parts: Vec<&str> = proxy_addr.split(':').collect();
|
let parts: Vec<&str> = proxy_addr.split(':').collect();
|
||||||
let host = parts.get(0).unwrap_or(&"127.0.0.1");
|
let host = parts.first().unwrap_or(&"127.0.0.1");
|
||||||
let port = parts.get(1).unwrap_or(&"1088");
|
let port = parts.get(1).unwrap_or(&"1088");
|
||||||
|
|
||||||
let is_gui = std::env::var("DISPLAY").is_ok() || std::env::var("WAYLAND_DISPLAY").is_ok();
|
let is_gui = std::env::var("DISPLAY").is_ok() || std::env::var("WAYLAND_DISPLAY").is_ok();
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ pub async fn run_udp_nat(
|
||||||
async fn start_udp_bypass_session(
|
async fn start_udp_bypass_session(
|
||||||
client_src: SocketAddr,
|
client_src: SocketAddr,
|
||||||
phys_if_index: Option<u32>,
|
phys_if_index: Option<u32>,
|
||||||
phys_if_name: Option<String>,
|
_phys_if_name: Option<String>,
|
||||||
session_rx: &mut mpsc::Receiver<(Vec<u8>, SocketAddr)>,
|
session_rx: &mut mpsc::Receiver<(Vec<u8>, SocketAddr)>,
|
||||||
smoltcp_tx: Arc<Mutex<netstack_smoltcp::udp::WriteHalf>>,
|
smoltcp_tx: Arc<Mutex<netstack_smoltcp::udp::WriteHalf>>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,12 @@
|
||||||
//! bandwidth and minimum RTT to determine the optimal sending rate.
|
//! bandwidth and minimum RTT to determine the optimal sending rate.
|
||||||
//! This replaces the fixed `retransmit_budget = 8` with an adaptive
|
//! This replaces the fixed `retransmit_budget = 8` with an adaptive
|
||||||
//! congestion window that responds to network conditions.
|
//! congestion window that responds to network conditions.
|
||||||
|
//!
|
||||||
|
//! RTO calculation follows RFC 6298:
|
||||||
|
//! SRTT = (1 - α) * SRTT + α * RTT (α = 1/8)
|
||||||
|
//! RTTVAR = (1 - β) * RTTVAR + β * |SRTT - RTT| (β = 1/4)
|
||||||
|
//! RTO = SRTT + 4 * RTTVAR
|
||||||
|
//! clamped to [RTO_MIN, RTO_MAX]
|
||||||
|
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
|
@ -15,8 +21,14 @@ pub struct CongestionController {
|
||||||
ssthresh: u64,
|
ssthresh: u64,
|
||||||
/// Current phase
|
/// Current phase
|
||||||
phase: Phase,
|
phase: Phase,
|
||||||
/// Minimum RTT observed
|
/// Minimum RTT observed (for BBR-style bandwidth estimation)
|
||||||
min_rtt: Duration,
|
min_rtt: Duration,
|
||||||
|
/// Smoothed RTT (RFC 6298 SRTT)
|
||||||
|
srtt: Duration,
|
||||||
|
/// RTT variance (RFC 6298 RTTVAR)
|
||||||
|
rttvar: Duration,
|
||||||
|
/// Whether we have received a first RTT sample
|
||||||
|
rtt_initialized: bool,
|
||||||
/// Bytes currently in flight (unacknowledged)
|
/// Bytes currently in flight (unacknowledged)
|
||||||
bytes_in_flight: u64,
|
bytes_in_flight: u64,
|
||||||
/// Total bytes acknowledged (for bandwidth estimation)
|
/// Total bytes acknowledged (for bandwidth estimation)
|
||||||
|
|
@ -37,31 +49,43 @@ pub struct CongestionController {
|
||||||
enum Phase {
|
enum Phase {
|
||||||
/// Exponential growth until loss or ssthresh
|
/// Exponential growth until loss or ssthresh
|
||||||
SlowStart,
|
SlowStart,
|
||||||
/// Probe bandwidth: cycle through pacing gains
|
/// Probe bandwidth: additive increase
|
||||||
ProbeBandwidth,
|
ProbeBandwidth,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initial congestion window: 10 packets × MTU
|
/// Initial congestion window: 32 packets × MTU (IW10 is too conservative for modern links)
|
||||||
const INITIAL_CWND_PACKETS: u64 = 10;
|
const INITIAL_CWND_PACKETS: u64 = 32;
|
||||||
/// Minimum cwnd: 2 packets
|
/// Minimum cwnd: 2 packets
|
||||||
const MIN_CWND_PACKETS: u64 = 2;
|
const MIN_CWND_PACKETS: u64 = 2;
|
||||||
/// Min RTT expiry window (after which we re-probe)
|
/// Min RTT expiry window (after which we re-probe)
|
||||||
const MIN_RTT_EXPIRY: Duration = Duration::from_secs(10);
|
const MIN_RTT_EXPIRY: Duration = Duration::from_secs(10);
|
||||||
|
/// Minimum RTO (RFC 6298: 1s in TCP; we use 50ms since we own the protocol)
|
||||||
|
const RTO_MIN: Duration = Duration::from_millis(50);
|
||||||
|
/// Maximum RTO
|
||||||
|
const RTO_MAX: Duration = Duration::from_secs(16);
|
||||||
|
/// Initial RTT estimate — 30 ms is reasonable for a well-connected VPN server.
|
||||||
|
/// Will be replaced by first real measurement within milliseconds.
|
||||||
|
const INITIAL_RTT: Duration = Duration::from_millis(30);
|
||||||
|
|
||||||
impl CongestionController {
|
impl CongestionController {
|
||||||
pub fn new(mtu: u64) -> Self {
|
pub fn new(mtu: u64) -> Self {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let initial_cwnd = INITIAL_CWND_PACKETS * mtu;
|
let initial_cwnd = INITIAL_CWND_PACKETS * mtu;
|
||||||
|
// Initial pacing: deliver cwnd in ~2 RTTs to fill the pipe quickly
|
||||||
|
let initial_pacing = initial_cwnd * 1_000_000 / INITIAL_RTT.as_micros().max(1) as u64;
|
||||||
Self {
|
Self {
|
||||||
cwnd: initial_cwnd,
|
cwnd: initial_cwnd,
|
||||||
ssthresh: u64::MAX,
|
ssthresh: u64::MAX,
|
||||||
phase: Phase::SlowStart,
|
phase: Phase::SlowStart,
|
||||||
min_rtt: Duration::from_millis(100), // Conservative initial estimate
|
min_rtt: INITIAL_RTT,
|
||||||
|
srtt: INITIAL_RTT,
|
||||||
|
rttvar: INITIAL_RTT / 2,
|
||||||
|
rtt_initialized: false,
|
||||||
bytes_in_flight: 0,
|
bytes_in_flight: 0,
|
||||||
total_acked: 0,
|
total_acked: 0,
|
||||||
last_ack_time: now,
|
last_ack_time: now,
|
||||||
loss_count: 0,
|
loss_count: 0,
|
||||||
pacing_rate: initial_cwnd * 10, // initial: ~10 windows/sec
|
pacing_rate: initial_pacing,
|
||||||
mtu,
|
mtu,
|
||||||
min_rtt_stamp: now,
|
min_rtt_stamp: now,
|
||||||
}
|
}
|
||||||
|
|
@ -82,9 +106,20 @@ impl CongestionController {
|
||||||
self.pacing_rate
|
self.pacing_rate
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the smoothed RTT estimate.
|
/// Returns the smoothed RTT estimate (SRTT).
|
||||||
pub fn smoothed_rtt(&self) -> Duration {
|
pub fn smoothed_rtt(&self) -> Duration {
|
||||||
self.min_rtt
|
self.srtt
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the adaptive RTO computed per RFC 6298:
|
||||||
|
/// RTO = SRTT + 4 * RTTVAR, clamped to [RTO_MIN, RTO_MAX].
|
||||||
|
///
|
||||||
|
/// This replaces the static `rto_ms` field in ProtocolMachine so that
|
||||||
|
/// retransmit timers automatically track changing network conditions.
|
||||||
|
pub fn rto(&self) -> Duration {
|
||||||
|
let rttvar4 = self.rttvar.saturating_mul(4);
|
||||||
|
let rto = self.srtt.saturating_add(rttvar4);
|
||||||
|
rto.clamp(RTO_MIN, RTO_MAX)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns how many bytes can still be sent.
|
/// Returns how many bytes can still be sent.
|
||||||
|
|
@ -115,16 +150,13 @@ impl CongestionController {
|
||||||
self.bytes_in_flight = self.bytes_in_flight.saturating_sub(bytes);
|
self.bytes_in_flight = self.bytes_in_flight.saturating_sub(bytes);
|
||||||
self.total_acked = self.total_acked.saturating_add(bytes);
|
self.total_acked = self.total_acked.saturating_add(bytes);
|
||||||
|
|
||||||
// Update RTT
|
// Update RTT measurements
|
||||||
self.update_rtt(rtt, now);
|
self.update_rtt(rtt, now);
|
||||||
|
|
||||||
// Update bandwidth estimate
|
|
||||||
self.update_bandwidth(bytes, now);
|
|
||||||
|
|
||||||
// State machine
|
// State machine
|
||||||
match self.phase {
|
match self.phase {
|
||||||
Phase::SlowStart => {
|
Phase::SlowStart => {
|
||||||
// Exponential growth: increase cwnd by acked bytes
|
// Exponential growth: increase cwnd by acked bytes (doubles per RTT)
|
||||||
self.cwnd = self.cwnd.saturating_add(bytes);
|
self.cwnd = self.cwnd.saturating_add(bytes);
|
||||||
if self.cwnd >= self.ssthresh {
|
if self.cwnd >= self.ssthresh {
|
||||||
self.phase = Phase::ProbeBandwidth;
|
self.phase = Phase::ProbeBandwidth;
|
||||||
|
|
@ -164,32 +196,49 @@ impl CongestionController {
|
||||||
self.update_pacing_rate();
|
self.update_pacing_rate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called periodically to update state.
|
|
||||||
pub fn on_tick(&mut self) {
|
|
||||||
// Nothing special needed per-tick -- state updates happen on ACK/loss
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Private ──────────────────────────────────────────────────────────────
|
// ── Private ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn update_rtt(&mut self, rtt: Duration, now: Instant) {
|
fn update_rtt(&mut self, rtt: Duration, now: Instant) {
|
||||||
// Track windowed minimum RTT
|
// Update windowed minimum RTT (for pacing)
|
||||||
if rtt < self.min_rtt || now.duration_since(self.min_rtt_stamp) >= MIN_RTT_EXPIRY {
|
if rtt < self.min_rtt || now.duration_since(self.min_rtt_stamp) >= MIN_RTT_EXPIRY {
|
||||||
self.min_rtt = rtt;
|
self.min_rtt = rtt;
|
||||||
self.min_rtt_stamp = now;
|
self.min_rtt_stamp = now;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn update_bandwidth(&mut self, _acked_bytes: u64, now: Instant) {
|
// Update SRTT and RTTVAR per RFC 6298
|
||||||
let elapsed = now.duration_since(self.last_ack_time);
|
if !self.rtt_initialized {
|
||||||
if elapsed.as_micros() > 0 {
|
// First measurement: initialize directly
|
||||||
// Removed bw_samples tracking
|
self.srtt = rtt;
|
||||||
|
self.rttvar = rtt / 2;
|
||||||
|
self.rtt_initialized = true;
|
||||||
|
} else {
|
||||||
|
// RTTVAR = (3/4) * RTTVAR + (1/4) * |SRTT - R|
|
||||||
|
let diff = if rtt > self.srtt {
|
||||||
|
rtt - self.srtt
|
||||||
|
} else {
|
||||||
|
self.srtt - rtt
|
||||||
|
};
|
||||||
|
// Integer-safe: RTTVAR = RTTVAR - RTTVAR/4 + diff/4
|
||||||
|
self.rttvar = self.rttvar
|
||||||
|
.saturating_sub(self.rttvar / 4)
|
||||||
|
.saturating_add(diff / 4);
|
||||||
|
|
||||||
|
// SRTT = (7/8) * SRTT + (1/8) * R
|
||||||
|
self.srtt = self.srtt
|
||||||
|
.saturating_sub(self.srtt / 8)
|
||||||
|
.saturating_add(rtt / 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tracing::trace!(
|
||||||
|
srtt_ms = self.srtt.as_millis(),
|
||||||
|
rttvar_ms = self.rttvar.as_millis(),
|
||||||
|
rto_ms = self.rto().as_millis(),
|
||||||
|
"congestion: RTT updated"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fn update_pacing_rate(&mut self) {
|
fn update_pacing_rate(&mut self) {
|
||||||
// Pacing rate = cwnd / min_rtt (with gain)
|
// Pacing rate = cwnd / min_rtt (delivery rate target)
|
||||||
let rtt_us = self.min_rtt.as_micros().max(1) as u64;
|
let rtt_us = self.min_rtt.as_micros().max(1) as u64;
|
||||||
self.pacing_rate = self.cwnd * 1_000_000 / rtt_us;
|
self.pacing_rate = self.cwnd * 1_000_000 / rtt_us;
|
||||||
}
|
}
|
||||||
|
|
@ -202,19 +251,18 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_initial_state() {
|
fn test_initial_state() {
|
||||||
let cc = CongestionController::new(1200);
|
let cc = CongestionController::new(1200);
|
||||||
assert_eq!(cc.cwnd(), 12000); // 10 * 1200
|
assert_eq!(cc.cwnd(), 32 * 1200); // 32 * 1200
|
||||||
assert!(cc.can_send());
|
assert!(cc.can_send());
|
||||||
assert_eq!(cc.cwnd_packets(), 10);
|
assert_eq!(cc.cwnd_packets(), 32);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_slow_start_growth() {
|
fn test_slow_start_growth() {
|
||||||
let mut cc = CongestionController::new(1200);
|
let mut cc = CongestionController::new(1200);
|
||||||
// Simulate sending and ACKing
|
let initial = cc.cwnd();
|
||||||
cc.on_send(1200);
|
cc.on_send(1200);
|
||||||
cc.on_ack(1200, Duration::from_millis(50));
|
cc.on_ack(1200, Duration::from_millis(50));
|
||||||
// cwnd should grow
|
assert!(cc.cwnd() > initial);
|
||||||
assert!(cc.cwnd() > 12000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -229,7 +277,7 @@ mod tests {
|
||||||
fn test_can_send_limits() {
|
fn test_can_send_limits() {
|
||||||
let mut cc = CongestionController::new(1200);
|
let mut cc = CongestionController::new(1200);
|
||||||
// Send until cwnd is exhausted
|
// Send until cwnd is exhausted
|
||||||
for _ in 0..10 {
|
for _ in 0..32 {
|
||||||
cc.on_send(1200);
|
cc.on_send(1200);
|
||||||
}
|
}
|
||||||
assert!(!cc.can_send()); // cwnd exhausted
|
assert!(!cc.can_send()); // cwnd exhausted
|
||||||
|
|
@ -244,10 +292,46 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_rtt_tracking() {
|
fn test_rtt_tracking_first_sample() {
|
||||||
let mut cc = CongestionController::new(1200);
|
let mut cc = CongestionController::new(1200);
|
||||||
cc.on_send(1200);
|
cc.on_send(1200);
|
||||||
cc.on_ack(1200, Duration::from_millis(25));
|
cc.on_ack(1200, Duration::from_millis(25));
|
||||||
|
// After first sample: SRTT = 25ms, RTTVAR = 12ms
|
||||||
assert_eq!(cc.smoothed_rtt(), Duration::from_millis(25));
|
assert_eq!(cc.smoothed_rtt(), Duration::from_millis(25));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rto_rfc6298() {
|
||||||
|
let mut cc = CongestionController::new(1200);
|
||||||
|
// After first sample with RTT=50ms: SRTT=50ms, RTTVAR=25ms, RTO=150ms
|
||||||
|
cc.on_send(1200);
|
||||||
|
cc.on_ack(1200, Duration::from_millis(50));
|
||||||
|
let rto = cc.rto();
|
||||||
|
// RTO = 50 + 4*25 = 150ms; clamped to [50ms, 16s]
|
||||||
|
assert!(rto >= RTO_MIN);
|
||||||
|
assert!(rto <= RTO_MAX);
|
||||||
|
assert_eq!(rto, Duration::from_millis(150));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rto_clamp_min() {
|
||||||
|
let cc = CongestionController::new(1200);
|
||||||
|
// Even with no RTT samples, RTO should not go below RTO_MIN
|
||||||
|
assert!(cc.rto() >= RTO_MIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rto_adapts_after_multiple_samples() {
|
||||||
|
let mut cc = CongestionController::new(1200);
|
||||||
|
// Feed several consistent RTT samples
|
||||||
|
for _ in 0..8 {
|
||||||
|
cc.on_send(1200);
|
||||||
|
cc.on_ack(1200, Duration::from_millis(20));
|
||||||
|
}
|
||||||
|
// After convergence, RTTVAR should be small → RTO close to SRTT + small margin
|
||||||
|
let rto = cc.rto();
|
||||||
|
// Should be well below 100ms (the old hardcoded default)
|
||||||
|
assert!(rto < Duration::from_millis(200));
|
||||||
|
assert!(rto >= RTO_MIN);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -395,18 +395,20 @@ impl ProtocolMachine {
|
||||||
self.last_recv_advance = Instant::now();
|
self.last_recv_advance = Instant::now();
|
||||||
} else {
|
} else {
|
||||||
// Gap detected
|
// Gap detected
|
||||||
if self.reorder_buffer.len() < self.max_reorder_buffer {
|
if nonce >= self.expected_recv_nonce {
|
||||||
self.reorder_buffer.insert(nonce, action);
|
if self.reorder_buffer.len() < self.max_reorder_buffer {
|
||||||
|
self.reorder_buffer.insert(nonce, action);
|
||||||
|
} else {
|
||||||
|
tracing::warn!("Reorder buffer still full after gap recovery, dropping frame nonce={}", nonce);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!("Reorder buffer full ({}/{}), dropping frame nonce={}",
|
tracing::debug!("Frame nonce={} arrived too late after gap recovery, dropping", nonce);
|
||||||
self.reorder_buffer.len(), self.max_reorder_buffer, nonce
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rate-limited NACK: send at most once per 30ms to prevent retransmit storms.
|
// Rate-limited NACK: send at most once per (rto/2) to prevent retransmit storms.
|
||||||
// Under high load with natural UDP reordering, sending a NACK per packet
|
// Using rto/2 means we send a NACK before the sender's timer fires, prompting
|
||||||
// causes exponential retransmit explosion that saturates the channel.
|
// fast retransmit without flooding. Floor at 10ms to handle very low-RTT links.
|
||||||
let nack_cooldown = Duration::from_millis(30);
|
let nack_cooldown = (self.cc.rto() / 2).max(Duration::from_millis(10));
|
||||||
if self.last_nack_sent.elapsed() >= nack_cooldown {
|
if self.last_nack_sent.elapsed() >= nack_cooldown {
|
||||||
self.last_nack_sent = Instant::now();
|
self.last_nack_sent = Instant::now();
|
||||||
let nack_payload = self.expected_recv_nonce.to_be_bytes();
|
let nack_payload = self.expected_recv_nonce.to_be_bytes();
|
||||||
|
|
@ -514,44 +516,18 @@ impl ProtocolMachine {
|
||||||
fn handle_tick(&mut self) -> Result<ProtocolAction, ProtocolError> {
|
fn handle_tick(&mut self) -> Result<ProtocolAction, ProtocolError> {
|
||||||
let mut actions = Vec::new();
|
let mut actions = Vec::new();
|
||||||
|
|
||||||
// ── Gap Recovery ──────────────────────────────────────────────
|
|
||||||
// If expected_recv_nonce hasn't advanced for 500ms+ and there
|
|
||||||
// are buffered frames waiting, the sender likely evicted the lost
|
|
||||||
// frame from sent_history. Skip the gap to restore data flow.
|
|
||||||
// This trades a small amount of data loss for connection liveness.
|
|
||||||
if !self.reorder_buffer.is_empty()
|
|
||||||
&& self.last_recv_advance.elapsed() > Duration::from_millis(500)
|
|
||||||
{
|
|
||||||
if let Some(&first_buffered) = self.reorder_buffer.keys().next() {
|
|
||||||
let skipped = first_buffered.saturating_sub(self.expected_recv_nonce);
|
|
||||||
self.expected_recv_nonce = first_buffered;
|
|
||||||
self.last_recv_advance = Instant::now();
|
|
||||||
|
|
||||||
let mut delivered = 0u64;
|
|
||||||
while let Some(buffered_action) = self.reorder_buffer.remove(&self.expected_recv_nonce) {
|
|
||||||
actions.push(buffered_action);
|
|
||||||
self.expected_recv_nonce = self.expected_recv_nonce.saturating_add(1);
|
|
||||||
delivered += 1;
|
|
||||||
}
|
|
||||||
self.ack_pending = true;
|
|
||||||
tracing::debug!("Gap recovery: skipped {} lost frames, delivered {} buffered frames (reorder_buf={})",
|
|
||||||
skipped, delivered, self.reorder_buffer.len()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Pending ACK flush ─────────────────────────────────────────
|
// ── Pending ACK flush ─────────────────────────────────────────
|
||||||
if let Some(ack_frame) = self.build_ack_if_due()? {
|
if let Some(ack_frame) = self.build_ack_if_due()? {
|
||||||
actions.push(ProtocolAction::SendDatagram(ack_frame));
|
actions.push(ProtocolAction::SendDatagram(ack_frame));
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let base_rto_ms = self.rto.as_millis().max(1) as u64;
|
// Use the adaptive RTO from the congestion controller (RFC 6298 SRTT + 4*RTTVAR).
|
||||||
|
// Falls back to rto_initial before the first ACK is received.
|
||||||
|
let base_rto_ms = self.cc.rto().max(self.rto).as_millis().max(1) as u64;
|
||||||
|
|
||||||
// ── Zombie frame eviction ────────────────────────────────────
|
// ── Zombie frame eviction ────────────────────────────────────
|
||||||
// Evict frames that exceeded max_retries + 2 grace retries.
|
// Evict frames that exceeded max_retries + 2 grace retries.
|
||||||
// Shorter grace period than before (was +4) to free memory faster
|
|
||||||
// after high-throughput bursts.
|
|
||||||
let grace = self.max_retries.saturating_add(2);
|
let grace = self.max_retries.saturating_add(2);
|
||||||
let before = self.sent_history.len();
|
let before = self.sent_history.len();
|
||||||
self.sent_history.retain(|f| !f.is_retransmittable || f.retries <= grace);
|
self.sent_history.retain(|f| !f.is_retransmittable || f.retries <= grace);
|
||||||
|
|
@ -562,14 +538,15 @@ impl ProtocolMachine {
|
||||||
|
|
||||||
// ── Retransmit expired frames ────────────────────────────────
|
// ── Retransmit expired frames ────────────────────────────────
|
||||||
// Limit retransmits per tick to prevent bandwidth saturation
|
// Limit retransmits per tick to prevent bandwidth saturation
|
||||||
|
// Backoff starts from retry #0 (immediately effective):
|
||||||
|
// effective_rto = base_rto * 2^retries, capped at 2^6 = 64×
|
||||||
let mut retransmit_budget: usize = self.cc.retransmit_budget();
|
let mut retransmit_budget: usize = self.cc.retransmit_budget();
|
||||||
for frame in self.sent_history.iter_mut() {
|
for frame in self.sent_history.iter_mut() {
|
||||||
if !frame.is_retransmittable {
|
if !frame.is_retransmittable {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let retry_over = frame.retries.saturating_sub(self.max_retries);
|
let backoff_factor = 1u64 << (frame.retries as u64).min(6);
|
||||||
let backoff_factor = 1u64 << retry_over.min(6);
|
|
||||||
let effective_rto = Duration::from_millis(base_rto_ms.saturating_mul(backoff_factor));
|
let effective_rto = Duration::from_millis(base_rto_ms.saturating_mul(backoff_factor));
|
||||||
|
|
||||||
if now.duration_since(frame.last_sent) >= effective_rto {
|
if now.duration_since(frame.last_sent) >= effective_rto {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -31,3 +31,4 @@ json_comments = "0.2"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
qrcode = { version = "0.14", default-features = false, features = ["svg"] }
|
qrcode = { version = "0.14", default-features = false, features = ["svg"] }
|
||||||
|
|
||||||
|
rlimit = "0.11.0"
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
@ -789,8 +797,13 @@ pub fn run() {
|
||||||
if let Ok(listener) = std::net::TcpListener::bind("127.0.0.1:49153") {
|
if let Ok(listener) = std::net::TcpListener::bind("127.0.0.1:49153") {
|
||||||
let _ = SINGLE_INSTANCE_LOCK.set(listener);
|
let _ = SINGLE_INSTANCE_LOCK.set(listener);
|
||||||
} else {
|
} else {
|
||||||
show_error_dialog("Приложение OSTP GUI уже запущено!");
|
#[cfg(not(debug_assertions))]
|
||||||
return;
|
{
|
||||||
|
show_error_dialog("Приложение OSTP GUI уже запущено!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
println!("WARNING: OSTP GUI is already running, ignoring in debug mode.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let state = AppState(Mutex::new(AppStateInner { tunnel: None }));
|
let state = AppState(Mutex::new(AppStateInner { tunnel: None }));
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
let _ = rlimit::increase_nofile_limit(1048576);
|
||||||
ostp_client::logging::setup_panic_hook();
|
ostp_client::logging::setup_panic_hook();
|
||||||
|
|
||||||
// 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.
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -6,52 +6,41 @@
|
||||||
<title>OSTP</title>
|
<title>OSTP</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" />
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" />
|
||||||
<link rel="stylesheet" href="styles.css" />
|
<link rel="stylesheet" href="styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-root">
|
<div class="app-root">
|
||||||
|
|
||||||
<!-- Ambient light blobs -->
|
<!-- Eagle watermark — behind everything, every screen -->
|
||||||
<div class="ambient" aria-hidden="true">
|
|
||||||
<div class="blob blob-1"></div>
|
|
||||||
<div class="blob blob-2"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Eagle watermark (brand) -->
|
|
||||||
<div class="watermark" aria-hidden="true">
|
<div class="watermark" aria-hidden="true">
|
||||||
<img src="assets/logo.svg" alt="" />
|
<img src="assets/logo.svg" alt="" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── HOME SCREEN ──────────────────────────────────────────── -->
|
<!-- ── HOME SCREEN ──────────────────────────────────────── -->
|
||||||
<div id="home-screen" class="screen active">
|
<div id="home-screen" class="screen active">
|
||||||
|
|
||||||
<!-- Top bar -->
|
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<div class="brand-dot" id="brand-dot"></div>
|
<div class="brand-dot" id="brand-dot"></div>
|
||||||
<span class="brand-name">OSTP</span>
|
<span class="brand-name">OSTP</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="topbar-right">
|
<div class="topbar-right">
|
||||||
<button id="btn-auto-connect" class="icon-btn" aria-label="Auto">
|
<button id="btn-auto-connect" class="icon-btn" aria-label="Auto" title="Auto-connect">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="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">
|
||||||
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
|
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button id="btn-theme-toggle" class="theme-toggle-btn" aria-label="Toggle theme">
|
<button id="btn-theme" class="icon-btn" aria-label="Toggle theme" title="Toggle theme">
|
||||||
<!-- Sun icon (shown in dark mode) -->
|
<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">
|
||||||
<svg class="icon-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="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"/>
|
||||||
<circle cx="12" cy="12" r="5"/>
|
|
||||||
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
<!-- Moon icon (shown in light mode) -->
|
<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">
|
||||||
<svg class="icon-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="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"/>
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button id="btn-go-settings" class="icon-btn" aria-label="Settings">
|
<button id="btn-go-settings" class="icon-btn" aria-label="Settings">
|
||||||
<!-- Gear icon -->
|
<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="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<circle cx="12" cy="12" r="3"/>
|
<circle cx="12" cy="12" r="3"/>
|
||||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -62,15 +51,16 @@
|
||||||
<!-- Center stage -->
|
<!-- Center stage -->
|
||||||
<main class="stage">
|
<main class="stage">
|
||||||
|
|
||||||
<!-- Orbit rings -->
|
<!-- Orbit rings (animated when connecting/connected) -->
|
||||||
<div class="orbit-wrap" id="orbit-wrap">
|
<div class="orbit-wrap" id="orbit-wrap">
|
||||||
<div class="orbit orbit-1"></div>
|
<div class="orbit orbit-1"></div>
|
||||||
<div class="orbit orbit-2"></div>
|
<div class="orbit orbit-2"></div>
|
||||||
|
<div class="orbit orbit-3"></div>
|
||||||
|
|
||||||
<!-- Power button -->
|
<!-- Power button -->
|
||||||
<button id="btn-connect" class="power-btn" aria-label="Connect">
|
<button id="btn-connect" class="power-btn" aria-label="Connect / Disconnect">
|
||||||
<div class="power-icon">
|
<div class="power-icon">
|
||||||
<svg width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="46" height="46" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"/>
|
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"/>
|
||||||
<line x1="12" y1="2" x2="12" y2="12"/>
|
<line x1="12" y1="2" x2="12" y2="12"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -78,16 +68,19 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status block -->
|
<!-- Status text -->
|
||||||
<div class="status-block">
|
<div class="status-block">
|
||||||
<div id="status-text" class="status-label" data-i18n="status_disconnected">Disconnected</div>
|
<div id="status-text" class="status-label">Disconnected</div>
|
||||||
<div id="uptime-text" class="status-sub" data-i18n="hint_tap">Tap to protect your traffic</div>
|
<div id="uptime-text" class="status-sub">Tap to protect your traffic</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Connection info (shown when connected) -->
|
<!-- Error banner -->
|
||||||
|
<div id="error-banner" class="error-banner hidden"></div>
|
||||||
|
|
||||||
|
<!-- Connection info (visible when connected) -->
|
||||||
<div id="connection-info" class="connection-info hidden">
|
<div id="connection-info" class="connection-info hidden">
|
||||||
<div class="server-badge">
|
<div class="server-badge">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<rect x="2" y="2" width="20" height="8" rx="2"/>
|
<rect x="2" y="2" width="20" height="8" rx="2"/>
|
||||||
<rect x="2" y="14" width="20" height="8" rx="2"/>
|
<rect x="2" y="14" width="20" height="8" rx="2"/>
|
||||||
<line x1="6" y1="6" x2="6.01" y2="6"/>
|
<line x1="6" y1="6" x2="6.01" y2="6"/>
|
||||||
|
|
@ -96,53 +89,56 @@
|
||||||
<span id="server-badge-text">—</span>
|
<span id="server-badge-text">—</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ping-test-box">
|
<!-- Live RTT + speeds -->
|
||||||
<div class="ping-test-left">
|
<div class="live-stats">
|
||||||
<span class="ping-test-title">CONNECTION TEST</span>
|
<div class="live-stat">
|
||||||
<span id="ping-text-value" class="ping-test-value">Target Ping: -- ms</span>
|
<span class="live-stat-label">RTT</span>
|
||||||
|
<span id="live-rtt" class="live-stat-value">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="live-stat-sep"></div>
|
||||||
|
<div class="live-stat">
|
||||||
|
<span class="live-stat-label">↓</span>
|
||||||
|
<span id="live-down-speed" class="live-stat-value">0 B/s</span>
|
||||||
|
</div>
|
||||||
|
<div class="live-stat-sep"></div>
|
||||||
|
<div class="live-stat">
|
||||||
|
<span class="live-stat-label">↑</span>
|
||||||
|
<span id="live-up-speed" class="live-stat-value">0 B/s</span>
|
||||||
</div>
|
</div>
|
||||||
<button id="btn-test-ping" class="ping-btn">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
|
|
||||||
</svg>
|
|
||||||
<span>Test Ping</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Traffic metrics bar -->
|
<!-- Total traffic bar -->
|
||||||
<footer class="metrics-bar">
|
<footer class="metrics-bar">
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<div class="metric-icon down-icon">
|
<div class="metric-icon down-icon">
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M12 5v14M19 12l-7 7-7-7"/>
|
<path d="M12 5v14M19 12l-7 7-7-7"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-body">
|
<div class="metric-body">
|
||||||
<span class="metric-label" data-i18n="download">Download</span>
|
<span class="metric-label">Download</span>
|
||||||
<span id="metric-down" class="metric-value">0 B</span>
|
<span id="metric-down" class="metric-value">0 B</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="metric-sep"></div>
|
<div class="metric-sep"></div>
|
||||||
|
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<div class="metric-icon up-icon">
|
<div class="metric-icon up-icon">
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M12 19V5M5 12l7-7 7 7"/>
|
<path d="M12 19V5M5 12l7-7 7 7"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-body">
|
<div class="metric-body">
|
||||||
<span class="metric-label" data-i18n="upload">Upload</span>
|
<span class="metric-label">Upload</span>
|
||||||
<span id="metric-up" class="metric-value">0 B</span>
|
<span id="metric-up" class="metric-value">0 B</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── SETTINGS SCREEN ──────────────────────────────────────── -->
|
<!-- ── SETTINGS SCREEN ──────────────────────────────────── -->
|
||||||
<div id="settings-screen" class="screen">
|
<div id="settings-screen" class="screen">
|
||||||
|
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
|
|
@ -151,236 +147,296 @@
|
||||||
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<span class="topbar-title" data-i18n="settings_title">Configuration</span>
|
<span class="topbar-title">Profiles</span>
|
||||||
<div style="width:36px"></div>
|
<button id="btn-add-profile" class="icon-btn add-btn" aria-label="Add profile">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19"/>
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="settings-body">
|
<div class="settings-body">
|
||||||
|
|
||||||
<!-- Quick import -->
|
<!-- Profile list -->
|
||||||
<div class="import-row">
|
<div id="profile-list" class="profile-list">
|
||||||
<input id="in-import-url"
|
<div id="profile-empty" class="profile-empty">
|
||||||
class="import-input"
|
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">
|
||||||
type="text"
|
<circle cx="12" cy="12" r="10"/>
|
||||||
data-i18n-placeholder="import_placeholder"
|
<line x1="12" y1="8" x2="12" y2="16"/>
|
||||||
placeholder="Paste ostp:// share link..." />
|
<line x1="8" y1="12" x2="16" y2="12"/>
|
||||||
<button id="btn-import-url" class="accent-btn" data-i18n="import_btn">Import</button>
|
</svg>
|
||||||
<button id="btn-share-url" class="btn secondary" data-i18n="share_btn" title="Share this config as a QR code / ostp:// link">Share</button>
|
<p>No profiles yet.<br/>Tap <strong>+</strong> to add one.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Form card -->
|
<!-- Client settings -->
|
||||||
<div class="card scrollable">
|
<div class="section-divider"><span>Client Settings</span></div>
|
||||||
|
|
||||||
<div class="field-group">
|
<div class="client-settings-card">
|
||||||
<label class="field-label" for="in-server" data-i18n="label_server">Server Address</label>
|
|
||||||
<input id="in-server" class="field-input" type="text" placeholder="host:port" spellcheck="false" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-group">
|
|
||||||
<label class="field-label" for="in-key" data-i18n="label_key">Access Key</label>
|
|
||||||
<div class="input-wrap">
|
|
||||||
<input id="in-key" class="field-input has-icon" type="password" data-i18n-placeholder="ph_key" placeholder="Secure access key" spellcheck="false" />
|
|
||||||
<button class="peek-btn" id="btn-peek-key" tabindex="-1" aria-label="Show key">
|
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
|
||||||
<circle cx="12" cy="12" r="3"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-group">
|
|
||||||
<label class="field-label" for="in-socks" data-i18n="label_socks">Local Proxy</label>
|
|
||||||
<input id="in-socks" class="field-input" type="text" placeholder="127.0.0.1:1088" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-group">
|
|
||||||
<label class="field-label" for="in-dns" data-i18n="label_dns">Custom DNS Server</label>
|
|
||||||
<input id="in-dns" class="field-input" type="text" placeholder="1.1.1.1" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-group">
|
|
||||||
<label class="field-label" for="in-transport" data-i18n="label_transport">Transport Protocol</label>
|
|
||||||
<select id="in-transport" class="field-input">
|
|
||||||
<option value="udp">UDP (Default)</option>
|
|
||||||
<option value="uot">TCP (UoT)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-group">
|
|
||||||
<label class="field-label" for="in-stealth-sni" data-i18n="label_sni">Stealth SNI</label>
|
|
||||||
<input id="in-stealth-sni" class="field-input" type="text" placeholder="www.microsoft.com" spellcheck="false" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-group">
|
|
||||||
<label class="field-label" for="in-mtu" data-i18n="label_mtu">MTU Size</label>
|
|
||||||
<input id="in-mtu" class="field-input" type="number" placeholder="1350" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-group">
|
|
||||||
<label class="field-label" for="in-mux-sessions" data-i18n="label_mux_sessions">Mux Sessions</label>
|
|
||||||
<input id="in-mux-sessions" class="field-input" type="number" placeholder="1" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Toggles -->
|
|
||||||
<div class="toggle-row">
|
<div class="toggle-row">
|
||||||
<div class="toggle-text">
|
<div class="toggle-text">
|
||||||
<span class="toggle-name" data-i18n="label_tun">TUN Mode</span>
|
<span class="toggle-name">TUN Mode</span>
|
||||||
<span class="toggle-hint" data-i18n="tun_hint">Route all system traffic</span>
|
<span class="toggle-hint">Route all system traffic</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle">
|
<label class="toggle">
|
||||||
<input type="checkbox" id="in-tun-mode" />
|
<input type="checkbox" id="in-tun-mode" />
|
||||||
<span class="toggle-track">
|
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||||||
<span class="toggle-thumb"></span>
|
|
||||||
</span>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toggle-row" id="group-kill-switch" style="display: none;">
|
<div class="toggle-row sub-row" id="group-kill-switch" style="display:none;">
|
||||||
<div class="toggle-text">
|
<div class="toggle-text">
|
||||||
<span class="toggle-name" data-i18n="label_kill_switch">Kill Switch</span>
|
<span class="toggle-name">Kill Switch</span>
|
||||||
<span class="toggle-hint" data-i18n="kill_switch_hint">Block traffic if connection drops</span>
|
<span class="toggle-hint">Block traffic if VPN drops</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle">
|
<label class="toggle">
|
||||||
<input type="checkbox" id="in-kill-switch" />
|
<input type="checkbox" id="in-kill-switch" />
|
||||||
<span class="toggle-track">
|
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||||||
<span class="toggle-thumb"></span>
|
|
||||||
</span>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="toggle-row">
|
<div class="toggle-row">
|
||||||
<div class="toggle-text">
|
<div class="toggle-text">
|
||||||
<span class="toggle-name" data-i18n="label_mux">Multiplexing (Mux)</span>
|
<span class="toggle-name">Multiplexing</span>
|
||||||
<span class="toggle-hint" data-i18n="mux_hint">Run multiple streams over one connection</span>
|
<span class="toggle-hint">Multiple streams over one connection</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle">
|
<label class="toggle">
|
||||||
<input type="checkbox" id="in-mux-mode" />
|
<input type="checkbox" id="in-mux-mode" />
|
||||||
<span class="toggle-track">
|
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||||||
<span class="toggle-thumb"></span>
|
|
||||||
</span>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toggle-row">
|
<div class="inline-field sub-row" id="group-mux-sessions" style="display:none;">
|
||||||
<div class="toggle-text">
|
<span class="field-label">Sessions</span>
|
||||||
<span class="toggle-name" data-i18n="label_launch_startup">Launch at Startup</span>
|
<input id="in-mux-sessions" class="field-input compact" type="number" placeholder="2" min="1" max="8" />
|
||||||
<span class="toggle-hint" data-i18n="launch_startup_hint">Start with Windows</span>
|
|
||||||
</div>
|
|
||||||
<label class="toggle">
|
|
||||||
<input type="checkbox" id="in-launch-startup" />
|
|
||||||
<span class="toggle-track">
|
|
||||||
<span class="toggle-thumb"></span>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toggle-row">
|
<div class="inline-field">
|
||||||
|
<span class="field-label">MTU</span>
|
||||||
|
<input id="in-mtu" class="field-input compact" type="number" placeholder="1350" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inline-field">
|
||||||
|
<span class="field-label">DNS</span>
|
||||||
|
<input id="in-dns" class="field-input compact" type="text" placeholder="1.1.1.1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inline-field">
|
||||||
|
<span class="field-label">Local Proxy</span>
|
||||||
|
<input id="in-socks" class="field-input compact" type="text" placeholder="127.0.0.1:1088" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-divider-mini"><span>Exceptions / Routing</span></div>
|
||||||
|
|
||||||
|
<div class="field-group" style="padding: 10px 14px; margin-bottom: 0;">
|
||||||
|
<label class="field-label" for="in-ex-domains">Excluded Domains</label>
|
||||||
|
<textarea id="in-ex-domains" class="field-input mono" placeholder="google.com, mycompany.internal" rows="2" spellcheck="false"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-group" style="padding: 0 14px 10px; margin-bottom: 0;">
|
||||||
|
<label class="field-label" for="in-ex-ips">Excluded IPs / Subnets</label>
|
||||||
|
<textarea id="in-ex-ips" class="field-input mono" placeholder="192.168.1.0/24, 10.0.0.1" rows="2" spellcheck="false"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-group" style="padding: 0 14px 10px; margin-bottom: 0; border-bottom: 1px solid rgba(255,255,255,0.04);">
|
||||||
|
<label class="field-label" for="in-ex-procs">Excluded Processes</label>
|
||||||
|
<textarea id="in-ex-procs" class="field-input mono" placeholder="chrome.exe, spotify.exe" rows="2" spellcheck="false"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-divider-mini"><span>Application</span></div>
|
||||||
|
|
||||||
|
<div class="toggle-row" style="border-top:none;">
|
||||||
<div class="toggle-text">
|
<div class="toggle-text">
|
||||||
<span class="toggle-name" data-i18n="label_autoconnect">Auto-connect</span>
|
<span class="toggle-name">Auto-connect</span>
|
||||||
<span class="toggle-hint" data-i18n="autoconnect_hint">Connect automatically on startup</span>
|
<span class="toggle-hint">Connect on startup</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle">
|
<label class="toggle">
|
||||||
<input type="checkbox" id="in-autoconnect" />
|
<input type="checkbox" id="in-autoconnect" />
|
||||||
<span class="toggle-track">
|
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||||||
<span class="toggle-thumb"></span>
|
|
||||||
</span>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="toggle-row" style="border-top:none;">
|
||||||
<div class="toggle-row">
|
|
||||||
<div class="toggle-text">
|
<div class="toggle-text">
|
||||||
<span class="toggle-name" data-i18n="label_debug">Debug Logs</span>
|
<span class="toggle-name">Launch at Startup</span>
|
||||||
<span class="toggle-hint" data-i18n="debug_hint">Verbose output</span>
|
<span class="toggle-hint">Start with Windows</span>
|
||||||
|
</div>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" id="in-launch-startup" />
|
||||||
|
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toggle-row" style="border-top:none;">
|
||||||
|
<div class="toggle-text">
|
||||||
|
<span class="toggle-name">Debug Logs</span>
|
||||||
|
<span class="toggle-hint">Verbose output to .log file</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle">
|
<label class="toggle">
|
||||||
<input type="checkbox" id="in-debug" />
|
<input type="checkbox" id="in-debug" />
|
||||||
<span class="toggle-track">
|
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||||||
<span class="toggle-thumb"></span>
|
|
||||||
</span>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Split Tunneling / Exclusions -->
|
|
||||||
<div class="section-head">
|
|
||||||
<span data-i18n="excl_title">Exclusions</span>
|
|
||||||
<span class="section-hint" data-i18n="excl_hint">traffic that bypasses the tunnel</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-group">
|
|
||||||
<label class="field-label" for="tag-input-domains" data-i18n="excl_domains">Bypass Domains</label>
|
|
||||||
<div class="tag-input-wrap" id="tag-wrap-domains">
|
|
||||||
<div class="tag-list" id="tag-list-domains"></div>
|
|
||||||
<input id="tag-input-domains" class="tag-input-field" type="text"
|
|
||||||
placeholder="example.com" spellcheck="false" autocomplete="off" />
|
|
||||||
</div>
|
|
||||||
<span class="field-hint">Enter domain suffix and press Enter. Example: google.com, *.local</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-group">
|
|
||||||
<label class="field-label" for="tag-input-ips" data-i18n="excl_ips">Bypass IPs / CIDR</label>
|
|
||||||
<div class="tag-input-wrap" id="tag-wrap-ips">
|
|
||||||
<div class="tag-list" id="tag-list-ips"></div>
|
|
||||||
<input id="tag-input-ips" class="tag-input-field" type="text"
|
|
||||||
placeholder="192.168.1.0/24" spellcheck="false" autocomplete="off" />
|
|
||||||
</div>
|
|
||||||
<span class="field-hint">Local network ranges bypass the tunnel automatically</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-group">
|
|
||||||
<label class="field-label" for="tag-input-processes" data-i18n="excl_processes">Bypass Processes</label>
|
|
||||||
<div class="tag-input-wrap" id="tag-wrap-processes">
|
|
||||||
<div class="tag-list" id="tag-list-processes"></div>
|
|
||||||
<input id="tag-input-processes" class="tag-input-field" type="text"
|
|
||||||
placeholder="chrome.exe" spellcheck="false" autocomplete="off" />
|
|
||||||
</div>
|
|
||||||
<span class="field-hint" id="proc-hint">Type process name and press Enter.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="app-version" id="app-version">OSTP GUI</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Toast -->
|
<!-- ── ADD PROFILE DROPDOWN ─────────────────────────────── -->
|
||||||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
<div id="add-menu" class="add-menu hidden">
|
||||||
|
<button id="add-from-link" class="add-menu-item">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
||||||
|
From link
|
||||||
|
</button>
|
||||||
|
<button id="add-from-clipboard" class="add-menu-item">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||||||
|
From clipboard
|
||||||
|
</button>
|
||||||
|
<button id="add-manually" class="add-menu-item">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
||||||
|
Manually
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Wintun Modal -->
|
<!-- ── LINK INPUT MODAL ─────────────────────────────────── -->
|
||||||
<div id="wintun-modal" class="modal-overlay hidden">
|
<div id="link-modal" class="modal-overlay hidden">
|
||||||
<div class="modal-content">
|
<div class="modal-content compact">
|
||||||
<h3 class="modal-title" data-i18n="wintun_missing_title">Wintun Driver Missing</h3>
|
<h3 class="modal-title">Paste link</h3>
|
||||||
<p class="modal-text" data-i18n="wintun_missing_desc">TUN mode requires the Wintun network driver.</p>
|
<div class="field-group">
|
||||||
<ol class="modal-steps">
|
<input id="link-input" class="field-input mono" type="text" placeholder="ostp://key@host:port" spellcheck="false" />
|
||||||
<li data-i18n="wintun_step1">Download <strong>wintun.zip</strong> from the official site</li>
|
</div>
|
||||||
<li data-i18n="wintun_step2">Extract <code>amd64\wintun.dll</code> from the archive</li>
|
|
||||||
<li><span data-i18n="wintun_step3">Place it here:</span> <code id="wintun-install-path">...</code></li>
|
|
||||||
<li data-i18n="wintun_step4">Restart the connection</li>
|
|
||||||
</ol>
|
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button id="btn-wintun-cancel" class="btn secondary" data-i18n="cancel_btn">Cancel</button>
|
<button id="btn-link-cancel" class="btn secondary">Cancel</button>
|
||||||
<a id="btn-wintun-open" href="https://www.wintun.net" target="_blank" class="btn primary" data-i18n="wintun_open_btn">Open wintun.net ↗</a>
|
<button id="btn-link-import" class="btn primary">Import</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Share Modal -->
|
<!-- ── PROFILE EDITOR MODAL ─────────────────────────────── -->
|
||||||
|
<div id="profile-modal" class="modal-overlay hidden">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3 class="modal-title" id="profile-modal-title">New Profile</h3>
|
||||||
|
|
||||||
|
<div class="field-group">
|
||||||
|
<label class="field-label" for="pm-name">Name</label>
|
||||||
|
<input id="pm-name" class="field-input" type="text" placeholder="My Server" />
|
||||||
|
</div>
|
||||||
|
<div class="field-group">
|
||||||
|
<label class="field-label" for="pm-server">Server</label>
|
||||||
|
<input id="pm-server" class="field-input mono" type="text" placeholder="host:port" spellcheck="false" />
|
||||||
|
</div>
|
||||||
|
<div class="field-group">
|
||||||
|
<label class="field-label" for="pm-key">Access Key</label>
|
||||||
|
<div class="input-wrap">
|
||||||
|
<input id="pm-key" class="field-input mono has-icon" type="password" placeholder="Secure access key" spellcheck="false" />
|
||||||
|
<button class="peek-btn" id="btn-peek-pm" tabindex="-1" aria-label="Show key">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field-group">
|
||||||
|
<label class="field-label" for="pm-transport">Transport</label>
|
||||||
|
<select id="pm-transport" class="field-input">
|
||||||
|
<option value="udp">UDP (Default)</option>
|
||||||
|
<option value="uot">TCP (UoT)</option>
|
||||||
|
</select>
|
||||||
|
</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">
|
||||||
|
<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-save" class="btn primary">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── SHARE MODAL ──────────────────────────────────────── -->
|
||||||
<div id="share-modal" class="modal-overlay hidden">
|
<div id="share-modal" class="modal-overlay hidden">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<h3 class="modal-title" data-i18n="share_title">Share configuration</h3>
|
<h3 class="modal-title">Share Profile</h3>
|
||||||
<p class="modal-text" data-i18n="share_desc">Scan the QR or copy the link. The QR is generated locally — the access key never leaves this device.</p>
|
<p class="modal-text">QR generated locally — the key never leaves this device.</p>
|
||||||
<div id="share-qr" class="share-qr"></div>
|
<div id="share-qr" class="share-qr"></div>
|
||||||
<input id="share-link" class="field-input" type="text" readonly />
|
<input id="share-link" class="field-input mono" type="text" readonly />
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button id="btn-share-close" class="btn secondary" data-i18n="close_btn">Close</button>
|
<button id="btn-share-close" class="btn secondary">Close</button>
|
||||||
<button id="btn-share-copy" class="btn primary" data-i18n="copy_btn">Copy link</button>
|
<button id="btn-share-copy" class="btn primary">Copy link</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ── WINTUN MODAL ─────────────────────────────────────── -->
|
||||||
|
<div id="wintun-modal" class="modal-overlay hidden">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3 class="modal-title">Wintun Driver Missing</h3>
|
||||||
|
<p class="modal-text">TUN mode requires the Wintun network driver.</p>
|
||||||
|
<ol class="modal-steps">
|
||||||
|
<li>Download <strong>wintun.zip</strong> from wintun.net</li>
|
||||||
|
<li>Extract <code>amd64\wintun.dll</code></li>
|
||||||
|
<li>Place it here: <code id="wintun-install-path">...</code></li>
|
||||||
|
<li>Restart the connection</li>
|
||||||
|
</ol>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button id="btn-wintun-cancel" class="btn secondary">Cancel</button>
|
||||||
|
<a id="btn-wintun-open" href="https://www.wintun.net" target="_blank" class="btn primary">Open wintun.net ↗</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast notification -->
|
||||||
|
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="main.js"></script>
|
<script type="module" src="main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
1284
ostp-gui/src/main.js
1284
ostp-gui/src/main.js
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -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 -Check Run cargo check only (no build, no release)
|
# .\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)
|
||||||
|
|
||||||
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"
|
||||||
Write-Output "Pushing tag to GitHub..."
|
} else {
|
||||||
& git push origin "v$Version" --force
|
Write-Output ""
|
||||||
|
Write-Output "[error] Failed to push pre-release tag."
|
||||||
if ($LASTEXITCODE -eq 0) {
|
}
|
||||||
Write-Output ""
|
|
||||||
Write-Output "[ok] Release v$Version triggered on GitHub Actions."
|
|
||||||
Write-Output " Monitor: https://github.com/ospab/ostp/actions"
|
|
||||||
} else {
|
} else {
|
||||||
Write-Output ""
|
Write-Output "Pushing version metadata..."
|
||||||
Write-Output "[error] Failed to push release tag."
|
& 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 "Creating release tag: v$Version"
|
||||||
|
& git tag -d "v$Version" 2>&1 | Out-Null
|
||||||
|
& git tag "v$Version"
|
||||||
|
|
||||||
|
Write-Output "Pushing tag to GitHub..."
|
||||||
|
& git push origin "v$Version" --force
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output "[ok] Release v$Version triggered on GitHub Actions."
|
||||||
|
Write-Output " Monitor: https://github.com/ospab/ostp/actions"
|
||||||
|
} else {
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output "[error] Failed to push release tag."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Pop-Location
|
Pop-Location
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue