Compare commits

..

28 Commits

Author SHA1 Message Date
ospab 5f9682663e Suppress dead_code warnings in ostp-gui lib
Log::message is deserialized from the IPC stream but not acted on
(informational variant, GUI shows it via the tray). HelperState::port
is stored for potential reconnection but not read back after initial
connection. Both are correctly annotated with #[allow(dead_code)].

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 22:41:22 +03:00
ospab ee38b15402 Fix tun-helper IPC encryption mismatch and unify log format
tun-helper: the GUI encrypts all IPC commands with ChaCha20Poly1305 and
sends them as hex, but the helper was reading plain JSON — every command
was silently dropped and the tunnel core was never started. Fix by:
- Moving IpcCrypto + derive_key into ostp-client/src/ipc_crypto.rs as a
  shared module so GUI and helper always use identical crypto logic.
- Rewriting tun-helper/src/main.rs to hex-decode and decrypt every
  incoming line before JSON-parsing, and to encrypt + hex-encode every
  outgoing HelperMsg before sending.
- Replacing the custom log_to_file() helper with tracing::info/warn/error
  so all helper output goes through the standard tracing pipeline.
- Adding tracing and hex to ostp-tun-helper Cargo.toml; dropping chrono
  (no longer needed after removing log_to_file).

logging: unify output format across all OSTP binaries to match the
standard tracing-subscriber style:
  2026-06-21T19:11:18.643226Z  INFO ostp_server: message
- Enable the `time` feature in tracing-subscriber and set UTC RFC-3339
  timer on both file and stderr layers in init_tracing.
- Remove with_line_number(true) — line numbers are not part of the
  desired format and bloat the target field.
- Replace println! in runner.rs with tracing::info!.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 22:38:29 +03:00
ospab 47d44fa072 Fix Closing state, replace sent_history VecDeque with BTreeMap, clean up dead code
- protocol: Closing+Inbound no longer force-transitions to Closed after
  one packet; handle_inbound now owns the transition when it receives a
  Close frame, preventing data loss on in-flight packets during teardown.
  Add Tick handling for Closing state so the Close frame is retransmitted.
- protocol: replace sent_history VecDeque<SentFrame> with BTreeMap<u64,
  SentFrame>; NACK lookup is now O(log n) instead of O(n) linear scan.
- protocol: remove unused _mtu field; drop VecDeque import.
- congestion: remove no-op on_tick method (was never called).
- dispatcher: remove broad #[allow(dead_code)] on impl block; annotate
  three genuinely unused methods individually. Fix comment "100000
  entries" → "50000" and log "inactive >5min" → ">10min" (real timeout
  is 600 s). Remove unused mut on stream variable in ostp client.
- docs: correct timestamp window ±30 s → ±300 s in EN and RU specs to
  match the actual drift > 300 check in dispatcher.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 22:09:56 +03:00
ospab d031b15679 Integrate dnstt into ostp-core and update build dependencies
Rewrite DNS transport on both client and server sides with embedded
dnstt binaries compiled from Go source via build.rs. Add Go 1.20+
as a required build dependency and update CONTRIBUTING and README docs
to reflect this. Extend relay and lib with dnstt-aware session handling.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 22:08:59 +03:00
ospab b31da29b2d Fix DNS roaming by using a stable fake peer IP derived from ClientID 2026-06-20 19:25:38 +03:00
ospab 10c1772271 Fix DNS server responses 2026-06-20 19:15:01 +03:00
ospab 3ced4a19b6 Rewrite DNS transport with dnstt-style fragmentation, ClientID, polling and reassembly 2026-06-20 18:45:23 +03:00
ospab 6987ac5344 Fallback to server parameter for DNS resolver if not specified 2026-06-20 00:07:52 +03:00
ospab d65af355f1 Fix handshake timeouts in OSTP outbounds and remove test_parse 2026-06-19 23:57:35 +03:00
ospab 23c4d38ee4 Make --import and --url patch existing configuration instead of overwriting 2026-06-19 23:45:33 +03:00
ospab b7a31af911 Add DNS Tunneling example to client init config 2026-06-19 23:21:39 +03:00
ospab 76bf1c9a98 fix(cli): evaluate CARGO_PKG_VERSION in parse_ostp_link to prevent false migrations 2026-06-19 19:24:37 +03:00
ospab fc339b3643 feat(server): log reasons for dropped packets 2026-06-19 19:14:46 +03:00
ospab 6eb7b369a0 fix(client): wait for handshake response in dial_tcp before sending data 2026-06-19 19:06:51 +03:00
ospab 01d7d19b11 Restore Session import for Windows compatibility and fix Flutter build 2026-06-19 18:24:51 +03:00
ospab 0953b83e3c CI/CD: release version v0.3.12 2026-06-19 17:53:16 +03:00
ospab 8a0b633bb1 Fix compiler warnings and errors 2026-06-19 17:51:58 +03:00
ospab 72077bbd0c CI/CD: release version v0.3.11 2026-06-19 17:36:16 +03:00
ospab 0cd189fb84 Prober now auto-reads DNS domain from config 2026-06-19 17:34:37 +03:00
ospab 87694c6218 Add update version targeting and fix dns prober 2026-06-19 17:31:43 +03:00
ospab 916a21eeec Fix type mismatch error in make_transport 2026-06-19 16:19:51 +03:00
ospab f8f27d366d Fix empty handshake payload and dummy keys in ostp outbound client 2026-06-19 16:11:37 +03:00
ospab ce9f11a35e Fix ReloadUser missing rename for 'key' resulting in all keys being dropped 2026-06-19 15:54:55 +03:00
ospab 7fadc8d28d Fix hot-reloader clearing access keys due to modular config migration 2026-06-19 15:44:55 +03:00
ospab 3efbfd75cc CI/CD: release version v0.3.10 2026-06-19 15:21:17 +03:00
ospab 8820a42359 Fix DNS Prober real RTT logic, fix Flutter DNS proxy UI, fix ServerInbound struct tags and migrator 2026-06-19 15:18:41 +03:00
ospab 0394971791 chore: remove embedded wiki submodule 2026-06-19 14:43:04 +03:00
ospab 430e304936 docs: remove useless ostp-wiki folder from root 2026-06-19 14:42:45 +03:00
128 changed files with 2420 additions and 2190 deletions

BIN
.gitignore vendored

Binary file not shown.

View File

@ -22,6 +22,7 @@ By contributing to this project, you agree to abide by our code of conduct and l
To build and test OSTP locally, you will need: To build and test OSTP locally, you will need:
* **Rust Toolchain**: Install via [rustup](https://rustup.rs/) (stable channel). * **Rust Toolchain**: Install via [rustup](https://rustup.rs/) (stable channel).
* **Go 1.20+**: Required to compile the embedded `dnstt` tunnel binaries.
* **Node.js (18+) & npm**: Required to compile Tauri GUI resources. * **Node.js (18+) & npm**: Required to compile Tauri GUI resources.
* **Git**: For version control. * **Git**: For version control.

View File

@ -22,6 +22,7 @@
Для локальной сборки и тестирования OSTP вам понадобятся: Для локальной сборки и тестирования OSTP вам понадобятся:
* **Rust Toolchain**: Установите через [rustup](https://rustup.rs/) (stable канал). * **Rust Toolchain**: Установите через [rustup](https://rustup.rs/) (stable канал).
* **Go 1.20+**: Необходимо для сборки встроенного DNS-туннеля dnstt.
* **Node.js (18+) и npm**: Необходимы для сборки интерфейса Tauri. * **Node.js (18+) и npm**: Необходимы для сборки интерфейса Tauri.
* **Git**: Для контроля версий. * **Git**: Для контроля версий.

38
Cargo.lock generated
View File

@ -388,6 +388,16 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "clipboard-win"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342"
dependencies = [
"lazy-bytes-cast",
"winapi",
]
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.5" version = "1.0.5"
@ -1252,6 +1262,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dbbfed4e59ba9750e15ba154fdfd9329cee16ff3df539c2666b70f58cc32105" checksum = "9dbbfed4e59ba9750e15ba154fdfd9329cee16ff3df539c2666b70f58cc32105"
[[package]]
name = "lazy-bytes-cast"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b"
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@ -1431,16 +1447,18 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]] [[package]]
name = "ostp" name = "ostp"
version = "0.3.8" version = "0.3.12"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
"clap", "clap",
"clipboard-win",
"colored", "colored",
"json_comments", "json_comments",
"ostp-client", "ostp-client",
"ostp-core", "ostp-core",
"ostp-server", "ostp-server",
"pico-args",
"rand 0.8.5", "rand 0.8.5",
"reqwest", "reqwest",
"serde", "serde",
@ -1453,7 +1471,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-client" name = "ostp-client"
version = "0.3.8" version = "0.3.12"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
@ -1488,7 +1506,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-core" name = "ostp-core"
version = "0.3.8" version = "0.3.12"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"byteorder", "byteorder",
@ -1497,9 +1515,11 @@ dependencies = [
"hkdf", "hkdf",
"hmac", "hmac",
"rand 0.8.5", "rand 0.8.5",
"serde",
"sha2", "sha2",
"snow", "snow",
"thiserror 1.0.69", "thiserror 1.0.69",
"tokio",
"tracing", "tracing",
"x25519-dalek", "x25519-dalek",
] ]
@ -1523,7 +1543,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-server" name = "ostp-server"
version = "0.3.8" version = "0.3.12"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
@ -1556,7 +1576,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-tun" name = "ostp-tun"
version = "0.3.8" version = "0.3.12"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"libc", "libc",
@ -1568,7 +1588,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-tun-helper" name = "ostp-tun-helper"
version = "0.3.8" version = "0.3.12"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -1592,6 +1612,12 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pico-args"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.17" version = "0.2.17"

View File

@ -12,7 +12,7 @@ resolver = "2"
[workspace.package] [workspace.package]
edition = "2021" edition = "2021"
license = "BSL 1.1" license = "BSL 1.1"
version = "0.3.8" version = "0.3.12"
[workspace.dependencies] [workspace.dependencies]
anyhow = "1.0" anyhow = "1.0"

View File

@ -142,8 +142,13 @@ irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | ie
## Сборка из исходников ## Сборка из исходников
### Зависимости для сборки
- Rust 1.70+
- Go 1.20+ (необходимо для сборки встроенного DNS-туннеля dnstt)
> **Благодарности:** Этот проект использует [dnstt](https://www.bamsoftware.com/software/dnstt/) от Bamsoftware для обеспечения устойчивого туннелирования поверх DNS. Бинарники dnstt автоматически компилируются и встраиваются в ядро OSTP.
```bash ```bash
# Требования: Rust 1.75+
cargo build --release cargo build --release
# Кросс-компиляция для Linux # Кросс-компиляция для Linux

View File

@ -94,7 +94,7 @@ OSTP executes a Noise Protocol Framework exchange utilizing the `Noise_NNpsk0_25
2. The PSK is integrated into the state at pattern position zero, authorizing and encrypting the very first handshaking datagram. 2. The PSK is integrated into the state at pattern position zero, authorizing and encrypting the very first handshaking datagram.
3. Ephemeral Curve25519 key exchange is evaluated to synthesize autonomous symmetric keys for subsequent read/write channels. 3. Ephemeral Curve25519 key exchange is evaluated to synthesize autonomous symmetric keys for subsequent read/write channels.
The initial handshake payload includes a Unix timestamp to mitigate replay attacks. The server enforces a strict ±30-second synchronization window. The initial handshake payload includes a Unix timestamp to mitigate replay attacks. The server enforces a ±300-second synchronization window to accommodate clock drift and mobile roaming scenarios.
--- ---

View File

@ -94,7 +94,7 @@ OSTP использует Noise Protocol Framework с паттерном `Noise_
2. PSK применяется на нулевой позиции паттерна, обеспечивая авторизацию и шифрование самой первой датаграммы рукопожатия (Zero-RTT авторизация). 2. PSK применяется на нулевой позиции паттерна, обеспечивая авторизацию и шифрование самой первой датаграммы рукопожатия (Zero-RTT авторизация).
3. Выполняется эфемерный обмен ключами Curve25519 для создания симметричных ключей передачи данных. 3. Выполняется эфемерный обмен ключами Curve25519 для создания симметричных ключей передачи данных.
Первичная полезная нагрузка рукопожатия содержит Unix-отметку времени для защиты от атак повторного воспроизведения (Replay Attacks). Сервер строго контролирует окно синхронизации (±30 секунд). Первичная полезная нагрузка рукопожатия содержит Unix-отметку времени для защиты от атак повторного воспроизведения (Replay Attacks). Сервер контролирует окно синхронизации (±300 секунд) с учётом дрейфа часов и смены сети при роуминге.
--- ---

View File

@ -1,36 +0,0 @@
import os
with open('ostp/src/main.rs', 'r', encoding='utf-8') as f:
content = f.read()
old_block = ''' if let Some(key) = first_key {
let host = get_or_ask_public_ip(&args.config);
let mut query_params = Vec::<String>::new();
query_params.push("type=udp".to_string());
let mut link = format!("ostp://{}@{}:{}", key, host, port);
if !query_params.is_empty() {
link.push('?');
link.push_str(&query_params.join("&"));
}
println!(" [1] {}", link);
}'''
new_block = ''' if let Some(key) = first_key {
let host = get_or_ask_public_ip(&args.config);
let mut query_params = Vec::<String>::new();
query_params.push("type=udp".to_string());
let mut link = format!("ostp://{}@{}:{}", key, host, port);
if !query_params.is_empty() {
link.push('?');
link.push_str(&query_params.join("&"));
}
println!(" [1] {}", link);
}
}'''
content = content.replace(old_block, new_block)
with open('ostp/src/main.rs', 'w', encoding='utf-8') as f:
f.write(content)

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

7
icons/logo_icon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -9,7 +9,7 @@ anyhow.workspace = true
bytes.workspace = true bytes.workspace = true
tokio.workspace = true tokio.workspace = true
tracing.workspace = true tracing.workspace = true
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter", "time"] }
tracing-appender = "0.2" tracing-appender = "0.2"
ostp-core = { path = "../ostp-core" } ostp-core = { path = "../ostp-core" }
ostp-tun = { path = "../ostp-tun" } ostp-tun = { path = "../ostp-tun" }

View File

@ -18,7 +18,7 @@ impl Default for BridgeMetrics {
} }
} }
pub fn set_socket_protector<F>(f: F) pub fn set_socket_protector<F>(_f: F)
where where
F: Fn(i32) -> bool + Send + Sync + 'static, F: Fn(i32) -> bool + Send + Sync + 'static,
{ {

View File

@ -50,6 +50,8 @@ pub enum InboundConfig {
protocol: String, // "socks" or "http" protocol: String, // "socks" or "http"
listen: String, listen: String,
port: u16, port: u16,
#[serde(default)]
set_system_proxy: bool,
}, },
} }
@ -172,33 +174,42 @@ impl ClientConfig {
.with_context(|| format!("failed to parse JSON from {}", path.display()))?; .with_context(|| format!("failed to parse JSON from {}", path.display()))?;
let (migrated_json, was_migrated) = Self::migrate_json(raw_json); let (migrated_json, was_migrated) = Self::migrate_json(raw_json);
if was_migrated { if was_migrated {
tracing::info!("Config was migrated to v0.3.1. Saving to {}", path.display()); tracing::warn!(
let serialized = serde_json::to_string_pretty(&migrated_json)?; "Config at {} is in an outdated format. Run 'ostp --migrate' to upgrade it.",
let header = "// OSTP Configuration v0.3.1\n// DO NOT EDIT THIS COMMENT - Migrator relies on it\n"; path.display()
let final_content = format!("{}{}", header, serialized); );
std::fs::write(&path, final_content)
.with_context(|| format!("failed to save migrated config to {}", path.display()))?;
} }
let config: ClientConfig = serde_json::from_value(migrated_json) let config: ClientConfig = serde_json::from_value(migrated_json)
.with_context(|| format!("failed to deserialize migrated config from {}", path.display()))?; .with_context(|| format!("failed to deserialize config from {}", path.display()))?;
Ok(config) Ok(config)
} }
/// Migrates old monolithic JSON to the new modular format. /// Migrates old monolithic JSON to the new modular format.
/// Returns the migrated JSON value and a boolean indicating if a migration occurred. /// Returns the migrated JSON value and a boolean indicating if a migration occurred.
pub fn migrate_json(mut json: serde_json::Value) -> (serde_json::Value, bool) { pub fn migrate_json(json: serde_json::Value) -> (serde_json::Value, bool) {
let is_migrated = json.get("version").and_then(|v| v.as_str()) == Some("0.3.1"); // Consider the config already migrated if:
if is_migrated { // 1. Version matches exactly, OR
// 2. The JSON already has the new modular format (inbounds + outbounds arrays)
let has_version = json.get("version").and_then(|v| v.as_str()) == Some(env!("CARGO_PKG_VERSION"));
let has_new_format = json.get("inbounds").and_then(|v| v.as_array()).is_some()
&& json.get("outbounds").and_then(|v| v.as_array()).is_some();
if has_version || has_new_format {
// If format is already new but version is old, just bump the version
if has_new_format && !has_version {
let mut updated = json.clone();
updated["version"] = serde_json::json!(env!("CARGO_PKG_VERSION"));
return (updated, false);
}
return (json, false); return (json, false);
} }
// Needs migration // Needs migration
let mut new_json = serde_json::json!({ let mut new_json = serde_json::json!({
"version": "0.3.1", "version": env!("CARGO_PKG_VERSION"),
}); });
// 1. Log level // 1. Log level

View File

@ -0,0 +1,41 @@
use anyhow::{anyhow, Result};
use chacha20poly1305::{ChaCha20Poly1305, Nonce};
use chacha20poly1305::aead::{Aead, KeyInit};
use sha2::{Sha256, Digest};
/// Symmetric IPC channel encryption for the tun-helper ↔ GUI pipe.
///
/// Both sides derive the same key from the per-launch random token, so no
/// secret is ever passed on the command line. The zero nonce is safe here
/// because each session uses a fresh random token, making key reuse impossible.
#[derive(Clone)]
pub struct IpcCrypto {
cipher: ChaCha20Poly1305,
}
impl IpcCrypto {
pub fn new(key: &[u8; 32]) -> Self {
let cipher = ChaCha20Poly1305::new_from_slice(key)
.expect("32-byte key is always valid for ChaCha20Poly1305");
Self { cipher }
}
pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
let nonce = Nonce::from_slice(&[0u8; 12]);
self.cipher.encrypt(nonce, plaintext)
.map_err(|e| anyhow!("IPC encrypt: {}", e))
}
pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
let nonce = Nonce::from_slice(&[0u8; 12]);
self.cipher.decrypt(nonce, ciphertext)
.map_err(|e| anyhow!("IPC decrypt: {}", e))
}
}
/// Derive a 32-byte key from the per-session random token.
pub fn derive_key(token: &str) -> [u8; 32] {
let mut key = [0u8; 32];
key.copy_from_slice(&Sha256::digest(token.as_bytes()));
key
}

View File

@ -9,3 +9,4 @@ pub mod tunnel;
pub mod runner; pub mod runner;
pub mod logging; pub mod logging;
pub mod ipc_crypto;

View File

@ -74,16 +74,20 @@ pub fn init_tracing(level: &str, app_name: &str, version: &str) -> Option<tracin
if let Ok(file) = OpenOptions::new().create(true).append(true).open(&path) { if let Ok(file) = OpenOptions::new().create(true).append(true).open(&path) {
let (file_writer, guard) = tracing_appender::non_blocking(file); let (file_writer, guard) = tracing_appender::non_blocking(file);
let timer = tracing_subscriber::fmt::time::UtcTime::rfc_3339();
let fmt_layer = tracing_subscriber::fmt::layer() let fmt_layer = tracing_subscriber::fmt::layer()
.with_target(true) .with_target(true)
.with_line_number(true) .with_line_number(false)
.with_thread_ids(false) .with_thread_ids(false)
.with_thread_names(false) .with_thread_names(false)
.with_ansi(false) .with_ansi(false)
.with_timer(timer.clone())
.with_writer(file_writer); .with_writer(file_writer);
let stderr_layer = tracing_subscriber::fmt::layer() let stderr_layer = tracing_subscriber::fmt::layer()
.with_target(true) .with_target(true)
.with_timer(timer)
.with_writer(std::io::stderr); .with_writer(std::io::stderr);
let _ = tracing_subscriber::registry() let _ = tracing_subscriber::registry()
@ -107,6 +111,7 @@ pub fn init_tracing(level: &str, app_name: &str, version: &str) -> Option<tracin
// Fallback: stderr only // Fallback: stderr only
let stderr_layer = tracing_subscriber::fmt::layer() let stderr_layer = tracing_subscriber::fmt::layer()
.with_target(true) .with_target(true)
.with_timer(tracing_subscriber::fmt::time::UtcTime::rfc_3339())
.with_writer(std::io::stderr); .with_writer(std::io::stderr);
let _ = tracing_subscriber::registry() let _ = tracing_subscriber::registry()
.with(EnvFilter::new(level)) .with(EnvFilter::new(level))

View File

@ -1,8 +1,7 @@
use anyhow::Result; use anyhow::Result;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::{mpsc, watch}; use tokio::sync::watch;
use crate::app::{BridgeCommand, ConnectionStatus, UiEvent};
use crate::config::{ClientConfig, InboundConfig}; use crate::config::{ClientConfig, InboundConfig};
use crate::tunnel::balancer::Balancer; use crate::tunnel::balancer::Balancer;
use crate::tunnel::outbounds::OutboundManager; use crate::tunnel::outbounds::OutboundManager;
@ -10,11 +9,11 @@ use crate::tunnel::router::Router;
pub async fn run_client_core( pub async fn run_client_core(
config: ClientConfig, config: ClientConfig,
metrics: Arc<crate::bridge::BridgeMetrics>, _metrics: Arc<crate::bridge::BridgeMetrics>,
mut shutdown_rx_ext: watch::Receiver<bool>, mut shutdown_rx_ext: watch::Receiver<bool>,
config_rx: Option<watch::Receiver<ClientConfig>>, _config_rx: Option<watch::Receiver<ClientConfig>>,
) -> Result<()> { ) -> Result<()> {
println!("[ostp] Starting run_client_core with multi-server architecture"); tracing::info!("starting client core");
let router = Arc::new(Router::new(config.routing.clone())); let router = Arc::new(Router::new(config.routing.clone()));
let balancer = Arc::new(Balancer::new(&config)); let balancer = Arc::new(Balancer::new(&config));

View File

@ -1,103 +1 @@
use std::sync::Arc; // Left empty by request
use std::time::Duration;
use bytes::Bytes;
use tokio::net::UdpSocket;
use tokio::sync::{mpsc, Mutex};
use rand::Rng;
use ostp_core::dns::{
DnsPacket, DnsRecordType, encode_payload_to_domain, decode_domain_to_payload,
};
use crate::transport::Transport;
pub async fn start_dns_transport(domain: String, resolver: String, _pubkey: Option<String>) -> std::io::Result<Transport> {
let (app_tx, transport_rx) = mpsc::channel::<Bytes>(100);
let (transport_tx, app_rx) = mpsc::channel::<Bytes>(100);
let resolver_addr = if resolver.contains(':') {
resolver.clone()
} else {
format!("{}:53", resolver)
};
let socket = UdpSocket::bind("0.0.0.0:0").await?;
socket.connect(&resolver_addr).await?;
let socket = Arc::new(socket);
let sock_rx = socket.clone();
let sock_tx = socket;
let base_domain = domain.clone();
// Send task (reads from app, encodes into DNS TXT, sends to UDP socket)
tokio::spawn(async move {
let mut rx = transport_rx;
loop {
let data_opt = tokio::select! {
res = rx.recv() => res,
_ = tokio::time::sleep(Duration::from_secs(2)) => Some(Bytes::new()),
};
let data = match data_opt {
Some(d) => d,
None => break, // App closed
};
// Encode data to base32 domain
let fqdn = encode_payload_to_domain(&data, &base_domain);
let id: u16 = rand::thread_rng().gen();
// Randomly choose TXT or NULL for diversity (as requested)
let qtype = if rand::thread_rng().gen_bool(0.5) {
DnsRecordType::TXT
} else {
DnsRecordType::NULL
};
let packet = DnsPacket::new_query(id, &fqdn, qtype);
let encoded = packet.encode();
if let Err(e) = sock_tx.send(&encoded).await {
tracing::warn!("DNS transport send error: {}", e);
break;
}
}
});
// Receive task (reads from UDP socket, decodes DNS answer, sends to app)
let base_domain_rx = domain.clone();
tokio::spawn(async move {
let mut buf = vec![0u8; 65535];
loop {
match sock_rx.recv(&mut buf).await {
Ok(n) => {
if let Some(packet) = DnsPacket::decode(&buf[..n]) {
for answer in packet.answers {
if answer.rtype == DnsRecordType::TXT || answer.rtype == DnsRecordType::NULL {
// If it's a TXT record, the response might be base32 encoded payload?
// Actually, dnstt puts the payload in the TXT/NULL record data.
// We'll just assume the rdata is the raw payload, or base32 encoded if it was sent as such.
// Let's just pass the raw data (TXT strings are decoded in DnsPacket::decode)
// Wait, dnstt server responds with raw bytes in NULL, and base32/chunked strings in TXT.
// Our `DnsPacket::decode` already handles extracting TXT string bytes or NULL raw bytes into `rdata`.
// Let's just send `rdata` to the app.
if transport_tx.send(Bytes::from(answer.rdata)).await.is_err() {
return; // App closed
}
}
}
}
}
Err(e) => {
tracing::warn!("DNS transport recv error: {}", e);
break;
}
}
}
});
Ok(Transport::Dns {
tx: app_tx,
rx: Arc::new(Mutex::new(app_rx)),
})
}

View File

@ -1,4 +1,3 @@
pub mod dns;
use std::sync::Arc; use std::sync::Arc;
use tokio::net::UdpSocket; use tokio::net::UdpSocket;
use bytes::Bytes; use bytes::Bytes;
@ -10,9 +9,10 @@ pub enum Transport {
tx: tokio::sync::mpsc::Sender<Bytes>, tx: tokio::sync::mpsc::Sender<Bytes>,
rx: Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<Bytes>>>, rx: Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<Bytes>>>,
}, },
Dns { Dnstt {
tx: tokio::sync::mpsc::Sender<Bytes>, tx: tokio::sync::mpsc::Sender<Bytes>,
rx: Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<Bytes>>>, rx: Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<Bytes>>>,
_guard: Arc<tokio::sync::Mutex<ostp_core::dnstt::DnsttProcess>>,
} }
} }
@ -20,7 +20,7 @@ impl Transport {
pub async fn send(&self, frame: &Bytes) -> std::io::Result<usize> { pub async fn send(&self, frame: &Bytes) -> std::io::Result<usize> {
match self { match self {
Self::Udp(sock) => sock.send(frame).await, Self::Udp(sock) => sock.send(frame).await,
Self::Uot { tx, .. } | Self::Dns { tx, .. } => { Self::Uot { tx, .. } | Self::Dnstt { tx, .. } => {
tx.send(frame.clone()).await.map_err(|_| std::io::Error::new(std::io::ErrorKind::BrokenPipe, "channel closed"))?; tx.send(frame.clone()).await.map_err(|_| std::io::Error::new(std::io::ErrorKind::BrokenPipe, "channel closed"))?;
Ok(frame.len()) Ok(frame.len())
} }
@ -30,31 +30,40 @@ impl Transport {
pub async fn send_to(&self, frame: &Bytes, target: std::net::SocketAddr) -> std::io::Result<usize> { pub async fn send_to(&self, frame: &Bytes, target: std::net::SocketAddr) -> std::io::Result<usize> {
match self { match self {
Self::Udp(sock) => sock.send_to(frame, target).await, Self::Udp(sock) => sock.send_to(frame, target).await,
Self::Uot { .. } | Self::Dns { .. } => self.send(frame).await, Self::Uot { .. } | Self::Dnstt { .. } => self.send(frame).await,
} }
} }
pub async fn recv(&self, buf: &mut [u8]) -> std::io::Result<usize> { pub async fn recv(&self, buf: &mut [u8]) -> std::io::Result<usize> {
match self { match self {
Self::Udp(sock) => sock.recv(buf).await, Self::Udp(sock) => sock.recv(buf).await,
Self::Uot { rx, .. } | Self::Dns { rx, .. } => { Self::Uot { rx, .. } | Self::Dnstt { rx, .. } => {
let mut rx = rx.lock().await; let mut rx = rx.lock().await;
match rx.recv().await { if let Some(frame) = rx.recv().await {
Some(bytes) => { let len = frame.len().min(buf.len());
let len = bytes.len().min(buf.len()); buf[..len].copy_from_slice(&frame[..len]);
buf[..len].copy_from_slice(&bytes[..len]); Ok(len)
Ok(len) } else {
} Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "channel closed"))
None => Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "channel closed")),
} }
} }
} }
} }
pub async fn recv_from(&self, buf: &mut [u8]) -> std::io::Result<(usize, std::net::SocketAddr)> {
match self {
Self::Udp(sock) => sock.recv_from(buf).await,
Self::Uot { .. } | Self::Dnstt { .. } => {
let n = self.recv(buf).await?;
Ok((n, "127.0.0.1:0".parse().unwrap()))
}
}
}
pub fn local_addr(&self) -> std::io::Result<std::net::SocketAddr> { pub fn local_addr(&self) -> std::io::Result<std::net::SocketAddr> {
match self { match self {
Self::Udp(sock) => sock.local_addr(), Self::Udp(sock) => sock.local_addr(),
Self::Uot { .. } | Self::Dns { .. } => Ok("0.0.0.0:0".parse().unwrap()), Self::Uot { .. } | Self::Dnstt { .. } => Ok("0.0.0.0:0".parse().unwrap()),
} }
} }
} }

View File

@ -1,6 +1,5 @@
use crate::config::{ClientConfig, OutboundConfig}; use crate::config::{ClientConfig, OutboundConfig};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc;
pub struct Balancer { pub struct Balancer {
outbounds: HashMap<String, OutboundConfig>, outbounds: HashMap<String, OutboundConfig>,
@ -60,6 +59,7 @@ impl Balancer {
/// Fetches the config for a concrete outbound /// Fetches the config for a concrete outbound
pub fn get_concrete_outbound(&self, tag: &str) -> Option<&OutboundConfig> { pub fn get_concrete_outbound(&self, tag: &str) -> Option<&OutboundConfig> {
let resolved_tag = self.resolve_outbound(tag); let resolved_tag = self.resolve_outbound(tag);
tracing::debug!("Balancer: tag '{}' resolved to '{}'", tag, resolved_tag);
self.outbounds.get(&resolved_tag) self.outbounds.get(&resolved_tag)
} }
} }

View File

@ -14,13 +14,20 @@ pub async fn run_socks_inbound(
outbound_manager: Arc<OutboundManager>, outbound_manager: Arc<OutboundManager>,
mut shutdown: watch::Receiver<bool>, mut shutdown: watch::Receiver<bool>,
) -> Result<()> { ) -> Result<()> {
let InboundConfig::LocalProxy { tag, protocol, listen, port } = inbound_config else { let InboundConfig::LocalProxy { tag, protocol, listen, port, set_system_proxy } = inbound_config else {
return Err(anyhow!("Invalid config for LocalProxy inbound")); return Err(anyhow!("Invalid config for LocalProxy inbound"));
}; };
let bind_addr = format!("{}:{}", listen, port); let bind_addr = format!("{}:{}", listen, port);
tracing::info!("Starting {} proxy inbound on {} (tag: {})", protocol, bind_addr, tag); tracing::info!("Starting {} proxy inbound on {} (tag: {})", protocol, bind_addr, tag);
let _proxy_guard = if set_system_proxy {
let proxy_host = if listen == "0.0.0.0" { "127.0.0.1" } else { &listen };
Some(crate::sysproxy::SystemProxyGuard::enable(&format!("{}:{}", proxy_host, port)))
} else {
None
};
let listener = TcpListener::bind(&bind_addr).await?; let listener = TcpListener::bind(&bind_addr).await?;
loop { loop {
@ -85,7 +92,7 @@ async fn handle_socks5_connection(
} }
let atyp = buf[3]; let atyp = buf[3];
let (target_host, mut ip_addr) = match atyp { let (target_host, ip_addr) = match atyp {
0x01 => { // IPv4 0x01 => { // IPv4
stream.read_exact(&mut buf[0..4]).await?; stream.read_exact(&mut buf[0..4]).await?;
let ip = std::net::Ipv4Addr::new(buf[0], buf[1], buf[2], buf[3]); let ip = std::net::Ipv4Addr::new(buf[0], buf[1], buf[2], buf[3]);

View File

@ -1,6 +1,7 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use std::sync::Arc; use std::sync::Arc;
use crate::config::{ClientConfig, InboundConfig}; use crate::config::{ClientConfig, InboundConfig};
#[allow(unused_imports)]
use crate::tunnel::router::{Router, Session}; use crate::tunnel::router::{Router, Session};
use crate::tunnel::outbounds::OutboundManager; use crate::tunnel::outbounds::OutboundManager;
use tokio::sync::watch; use tokio::sync::watch;
@ -13,7 +14,7 @@ pub async fn run_tun_inbound(
outbound_manager: Arc<OutboundManager>, outbound_manager: Arc<OutboundManager>,
mut shutdown: watch::Receiver<bool>, mut shutdown: watch::Receiver<bool>,
) -> Result<()> { ) -> Result<()> {
use std::net::ToSocketAddrs;
use netstack_smoltcp::StackBuilder; use netstack_smoltcp::StackBuilder;
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use futures::{StreamExt, SinkExt}; use futures::{StreamExt, SinkExt};
@ -72,7 +73,7 @@ pub async fn run_tun_inbound(
#[allow(unused_variables)] #[allow(unused_variables)]
let mut _route_guard = None; let mut _route_guard = None;
let (mut tun_to_stack, mut stack_to_tun) = { let (tun_to_stack, stack_to_tun) = {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
{ {
if let Some(fd) = fd { if let Some(fd) = fd {
@ -183,7 +184,7 @@ pub async fn run_tun_inbound(
let router_tcp = router.clone(); let router_tcp = router.clone();
let tag_tcp = tag.clone(); let tag_tcp = tag.clone();
let mut tcp_accept_task = tokio::spawn(async move { let tcp_accept_task = tokio::spawn(async move {
let Some(mut listener) = tcp_listener else { return; }; let Some(mut listener) = tcp_listener else { return; };
while let Some((mut stream, local, remote)) = listener.next().await { while let Some((mut stream, local, remote)) = listener.next().await {
let om = outbound_manager_tcp.clone(); let om = outbound_manager_tcp.clone();
@ -249,7 +250,7 @@ pub async fn run_tun_inbound(
let router_udp = router.clone(); let router_udp = router.clone();
let tag_udp = tag.clone(); let tag_udp = tag.clone();
let mut udp_proxy_task = tokio::spawn(async move { let udp_proxy_task = tokio::spawn(async move {
if let Some(udp_sock) = udp_socket { if let Some(udp_sock) = udp_socket {
let (mut udp_rx, _udp_tx) = udp_sock.split(); let (mut udp_rx, _udp_tx) = udp_sock.split();
while let Some((payload, local, remote)) = udp_rx.next().await { while let Some((payload, local, remote)) = udp_rx.next().await {

View File

@ -66,7 +66,7 @@ pub fn bind_socket_to_interface(socket: &tokio::net::TcpSocket, _is_ipv6: bool,
Ok(()) Ok(())
} }
pub async fn dial_tcp(target_host: &str, target_port: u16, phys_if_idx: Option<u32>) -> Result<TcpStream> { pub async fn dial_tcp(target_host: &str, target_port: u16, _phys_if_idx: Option<u32>) -> Result<TcpStream> {
let addrs = tokio::net::lookup_host((target_host, target_port)).await?.collect::<Vec<_>>(); let addrs = tokio::net::lookup_host((target_host, target_port)).await?.collect::<Vec<_>>();
if addrs.is_empty() { if addrs.is_empty() {
return Err(anyhow!("Could not resolve target host: {}", target_host)); return Err(anyhow!("Could not resolve target host: {}", target_host));
@ -79,7 +79,7 @@ pub async fn dial_tcp(target_host: &str, target_port: u16, phys_if_idx: Option<u
}; };
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
if let Some(idx) = phys_if_idx { if let Some(idx) = _phys_if_idx {
if let Err(e) = bind_socket_to_interface(&socket, target_addr.is_ipv6(), idx) { if let Err(e) = bind_socket_to_interface(&socket, target_addr.is_ipv6(), idx) {
tracing::warn!("DIRECT: Failed to bind to physical interface {}: {}", idx, e); tracing::warn!("DIRECT: Failed to bind to physical interface {}: {}", idx, e);
} }

View File

@ -1,6 +1,5 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use std::sync::Arc; use std::sync::Arc;
use tokio::net::TcpStream;
use crate::tunnel::balancer::Balancer; use crate::tunnel::balancer::Balancer;
use crate::config::OutboundConfig; use crate::config::OutboundConfig;
@ -12,7 +11,7 @@ pub mod socks;
pub struct OutboundManager { pub struct OutboundManager {
balancer: Arc<Balancer>, balancer: Arc<Balancer>,
phys_if_index: Option<u32>, phys_if_index: Option<u32>,
phys_if_name: Option<String>, _phys_if_name: Option<String>,
} }
impl OutboundManager { impl OutboundManager {
@ -24,7 +23,7 @@ impl OutboundManager {
Self { Self {
balancer, balancer,
phys_if_index, phys_if_index,
phys_if_name, _phys_if_name: phys_if_name,
} }
} }
@ -40,7 +39,7 @@ impl OutboundManager {
block::dial_tcp(target_host, target_port).await block::dial_tcp(target_host, target_port).await
} }
OutboundConfig::Ostp { server, port, access_key, transport, multiplex, .. } => { OutboundConfig::Ostp { server, port, access_key, transport, multiplex, .. } => {
ostp::dial_tcp(server, *port, access_key, transport, multiplex).await ostp::dial_tcp(target_host, target_port, server, *port, access_key, transport, multiplex).await
} }
OutboundConfig::Socks { server, port, .. } => { OutboundConfig::Socks { server, port, .. } => {
socks::dial_tcp(target_host, target_port, server, *port).await socks::dial_tcp(target_host, target_port, server, *port).await

View File

@ -1,76 +1,215 @@
use anyhow::{anyhow, Result}; use anyhow::Result;
use tokio::net::TcpStream; use tokio::net::TcpStream;
use crate::config::{TransportConfig, MultiplexConfig}; use crate::config::{TransportConfig, MultiplexConfig};
use ostp_core::{NoiseRole, OstpEvent, ProtocolAction, ProtocolConfig, ProtocolMachine}; use ostp_core::{OstpEvent, ProtocolAction, ProtocolConfig, ProtocolMachine};
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::UdpSocket;
/// Build the handshake payload the server expects:
/// [timestamp_u64_be (8 bytes)] [session_id_u32_be (4 bytes)] [access_key bytes]
fn build_handshake_payload(session_id: u32, access_key: &str) -> Vec<u8> {
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let mut payload = Vec::with_capacity(12 + access_key.len());
payload.extend_from_slice(&ts.to_be_bytes());
payload.extend_from_slice(&session_id.to_be_bytes());
payload.extend_from_slice(access_key.as_bytes());
payload
}
/// Build a correctly configured ProtocolConfig for an outgoing OSTP connection.
fn make_initiator_config(
session_id: u32,
access_key: &str,
transport_cfg: &TransportConfig,
) -> ProtocolConfig {
let secrets = ostp_core::crypto::derive_all_secrets(access_key.as_bytes());
let payload = build_handshake_payload(session_id, access_key);
let mtu = match transport_cfg.r#type.as_str() {
"dns" => 1100,
_ => 1350,
};
// For DNS transport: use larger ack_delay and rto to match DNS round-trip latency
// (each DNS query + reply takes 300-800ms end-to-end through Cloudflare).
// For UDP: minimize ack_delay to 1ms (ACK asap) and let CC drive the RTO.
let (ack_delay_ms, rto_ms) = match transport_cfg.r#type.as_str() {
"dns" => (50, 1500),
_ => (1, 200),
};
ProtocolConfig {
role: ostp_core::NoiseRole::Initiator,
psk: secrets.psk,
session_id,
handshake_payload: payload,
max_padding: 256,
padding_strategy: ostp_core::framing::PaddingStrategy::Adaptive,
obfuscation_key: secrets.obfuscation_key,
max_reorder: 16384,
max_reorder_buffer: 8192,
ack_delay_ms,
rto_ms,
max_retries: 8,
max_sent_history: 32768,
handshake_pad_min: secrets.handshake_pad_min,
handshake_pad_max: secrets.handshake_pad_max,
mtu,
}
}
fn random_session_id() -> u32 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut h = DefaultHasher::new();
std::time::Instant::now().hash(&mut h);
std::thread::current().id().hash(&mut h);
h.finish() as u32
}
pub async fn dial_tcp( pub async fn dial_tcp(
target_host: &str,
target_port: u16,
server: &str, server: &str,
port: u16, port: u16,
access_key: &str, access_key: &str,
transport_cfg: &TransportConfig, transport_cfg: &TransportConfig,
_multiplex: &MultiplexConfig, _multiplex: &MultiplexConfig,
) -> Result<TcpStream> { ) -> Result<TcpStream> {
tracing::info!("Dialing OSTP server {}:{} for target {}:{}", server, port, target_host, target_port);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
let local_addr = listener.local_addr()?; let local_addr = listener.local_addr()?;
let client_stream = tokio::net::TcpStream::connect(local_addr).await?; let client_stream = tokio::net::TcpStream::connect(local_addr).await?;
let (mut server_stream, _) = listener.accept().await?; let (mut server_stream, _) = listener.accept().await?;
let transport = match transport_cfg.r#type.as_str() { let transport = make_transport(transport_cfg, server, port).await?;
"dns" => {
let domain = transport_cfg.domain.clone().unwrap_or_else(|| "tunnel.example.com".to_string());
let resolver = transport_cfg.resolver.clone().unwrap_or_else(|| "8.8.8.8".to_string());
crate::transport::dns::start_dns_transport(domain, resolver, transport_cfg.pubkey.clone()).await?
}
// Fallback to UDP for now if unknown
_ => {
let udp = tokio::net::UdpSocket::bind("0.0.0.0:0").await?;
udp.connect((server, port)).await?;
crate::transport::Transport::Udp(std::sync::Arc::new(udp))
}
};
let mut psk = [0u8; 32];
let key_bytes = access_key.as_bytes();
let len = key_bytes.len().min(32);
psk[..len].copy_from_slice(&key_bytes[..len]);
let config = ProtocolConfig {
role: ostp_core::NoiseRole::Initiator,
psk,
session_id: 1,
handshake_payload: vec![],
max_padding: 0,
padding_strategy: ostp_core::framing::PaddingStrategy::Fixed(0),
obfuscation_key: [0; 8],
max_reorder: 16384,
max_reorder_buffer: 8192,
ack_delay_ms: 10,
rto_ms: 100,
max_retries: 5,
max_sent_history: 32768,
handshake_pad_min: 8,
handshake_pad_max: 24,
mtu: 1400,
};
let session_id = random_session_id();
let config = make_initiator_config(session_id, access_key, transport_cfg);
let mut machine = ProtocolMachine::new(config).unwrap(); let mut machine = ProtocolMachine::new(config).unwrap();
// Spawn bridge task let target_host_str = target_host.to_string();
tokio::spawn(async move {
if let Ok(action) = machine.on_event(OstpEvent::Start) { let server_str = server.to_string();
// Spawn bridge task
tokio::spawn(async move {
// Send initial handshake
if let Ok(action) = machine.on_event(OstpEvent::Start) {
handle_action(action, &transport, &mut server_stream).await;
}
// Wait for handshake response (server sends HandshakePayload back)
let mut buf = [0u8; 8192];
let mut handshake_success = false;
match tokio::time::timeout(
std::time::Duration::from_millis(15000),
transport.recv(&mut buf),
).await {
Ok(Ok(n)) => {
if let Ok(action) = machine.on_event(OstpEvent::Inbound(bytes::Bytes::copy_from_slice(&buf[..n]))) {
handle_action(action, &transport, &mut server_stream).await;
handshake_success = true;
}
}
_ => {
tracing::warn!("OSTP handshake timeout for {}:{}", server_str, port);
return;
}
}
if !handshake_success {
tracing::warn!("TCP handshake failed or protocol machine error");
return;
}
// Send connection request
let connect_msg = ostp_core::relay::RelayMessage::Connect(format!("{}:{}", target_host_str, target_port));
let connect_encoded = connect_msg.encode();
if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::from(connect_encoded))) {
handle_action(action, &transport, &mut server_stream).await; handle_action(action, &transport, &mut server_stream).await;
} }
// ── Wait for ConnectOk before forwarding any data ─────────────────
// This is critical: if we enter the data loop immediately, the TLS
// ClientHello arrives at the server before it has established the
// outbound TCP connection, causing it to drop the packet as
// "Relay DATA for unknown stream".
// The kernel will buffer incoming data from server_stream while we wait.
let mut connect_ok = false;
match tokio::time::timeout(
std::time::Duration::from_secs(30),
async {
let mut wait_buf = [0u8; 8192];
loop {
tokio::select! {
Ok(n) = transport.recv(&mut wait_buf) => {
if let Ok(action) = machine.on_event(OstpEvent::Inbound(
bytes::Bytes::copy_from_slice(&wait_buf[..n]),
)) {
// Check for ConnectOk or Error before dispatching
let result = check_connect_result(&action);
handle_action(action, &transport, &mut server_stream).await;
match result {
Some(true) => return true,
Some(false) => return false,
None => {}
}
}
}
_ = tokio::time::sleep(std::time::Duration::from_millis(10)) => {
if let Ok(action) = machine.on_event(OstpEvent::Tick) {
handle_action(action, &transport, &mut server_stream).await;
}
}
}
}
},
)
.await
{
Ok(true) => {
tracing::debug!("ConnectOk received for {}:{}, starting data forwarding", target_host_str, target_port);
connect_ok = true;
}
Ok(false) => {
tracing::warn!("Server refused connection to {}:{}", target_host_str, target_port);
}
Err(_) => {
tracing::warn!("ConnectOk timeout for {}:{}", target_host_str, target_port);
}
}
if !connect_ok {
return;
}
// ── Main bidirectional data forwarding loop ───────────────────────
// Backpressure: we track how many frames are in-flight vs the congestion
// window. When the window is full we stop reading from the TCP stream
// (the kernel buffers it) until the remote ACKs enough frames.
// This prevents overrunning the sender's sent_history and collapsing cwnd.
let mut buf = [0u8; 65535]; let mut buf = [0u8; 65535];
let mut udp_buf = [0u8; 65535]; let mut udp_buf = [0u8; 65535];
loop { loop {
// Compute adaptive tick interval:
// - If there is a pending ACK: tick = ack_delay (flush it quickly)
// - Otherwise: tick = rto/4 (check retransmits without busy-spinning)
// Floor at 1ms, ceiling at 50ms.
let tick_ms = (machine.rto().as_millis() / 4).clamp(1, 50) as u64;
let can_send = machine.in_flight_count() < machine.cwnd_packets().max(4);
tokio::select! { tokio::select! {
Ok(n) = server_stream.read(&mut buf) => { // Only read from the application TCP stream when cwnd allows
Ok(n) = server_stream.read(&mut buf), if can_send => {
if n == 0 { break; } if n == 0 { break; }
if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::copy_from_slice(&buf[..n]))) { let data_msg = ostp_core::relay::RelayMessage::Data(buf[..n].to_vec());
let encoded = data_msg.encode();
if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::from(encoded))) {
handle_action(action, &transport, &mut server_stream).await; handle_action(action, &transport, &mut server_stream).await;
} }
} }
@ -79,7 +218,7 @@ pub async fn dial_tcp(
handle_action(action, &transport, &mut server_stream).await; handle_action(action, &transport, &mut server_stream).await;
} }
} }
_ = tokio::time::sleep(std::time::Duration::from_millis(10)) => { _ = tokio::time::sleep(std::time::Duration::from_millis(tick_ms)) => {
if let Ok(action) = machine.on_event(OstpEvent::Tick) { if let Ok(action) = machine.on_event(OstpEvent::Tick) {
handle_action(action, &transport, &mut server_stream).await; handle_action(action, &transport, &mut server_stream).await;
} }
@ -88,6 +227,7 @@ pub async fn dial_tcp(
} }
}); });
Ok(client_stream) Ok(client_stream)
} }
@ -101,64 +241,55 @@ pub async fn handle_udp(
transport_cfg: &TransportConfig, transport_cfg: &TransportConfig,
_multiplex: &MultiplexConfig, _multiplex: &MultiplexConfig,
) -> Result<()> { ) -> Result<()> {
let transport = match transport_cfg.r#type.as_str() { let transport = make_transport(transport_cfg, server, port).await?;
"dns" => {
let domain = transport_cfg.domain.clone().unwrap_or_else(|| "tunnel.example.com".to_string()); // Derive session_id from client source addr for stable per-flow sessions
let resolver = transport_cfg.resolver.clone().unwrap_or_else(|| "8.8.8.8".to_string()); let ip_bytes = match client_src.ip() {
crate::transport::dns::start_dns_transport(domain, resolver, transport_cfg.pubkey.clone()).await? std::net::IpAddr::V4(v4) => {
let o = v4.octets();
u32::from_be_bytes(o)
} }
_ => { std::net::IpAddr::V6(v6) => {
let udp = tokio::net::UdpSocket::bind("0.0.0.0:0").await?; let o = v6.octets();
udp.connect((server, port)).await?; u32::from_be_bytes([o[12], o[13], o[14], o[15]])
crate::transport::Transport::Udp(std::sync::Arc::new(udp))
} }
}; };
let session_id = ip_bytes ^ (client_src.port() as u32);
let mut psk = [0u8; 32]; let config = make_initiator_config(session_id, access_key, transport_cfg);
let key_bytes = access_key.as_bytes();
let len = key_bytes.len().min(32);
psk[..len].copy_from_slice(&key_bytes[..len]);
let config = ProtocolConfig {
role: ostp_core::NoiseRole::Initiator,
psk,
session_id: u32::from_ne_bytes([
client_src.ip().to_string().as_bytes().get(0).copied().unwrap_or(0),
client_src.ip().to_string().as_bytes().get(1).copied().unwrap_or(0),
client_src.ip().to_string().as_bytes().get(2).copied().unwrap_or(0),
client_src.ip().to_string().as_bytes().get(3).copied().unwrap_or(0),
]),
handshake_payload: vec![],
max_padding: 0,
padding_strategy: ostp_core::framing::PaddingStrategy::Fixed(0),
obfuscation_key: [0; 8],
max_reorder: 4096,
max_reorder_buffer: 2048,
ack_delay_ms: 50,
rto_ms: 200,
max_retries: 3,
max_sent_history: 8192,
handshake_pad_min: 8,
handshake_pad_max: 24,
mtu: 1400,
};
let mut machine = ProtocolMachine::new(config)?; let mut machine = ProtocolMachine::new(config)?;
// Send initial packet with UDP payload // Send handshake first
if let Ok(action) = machine.on_event(OstpEvent::Start) { if let Ok(action) = machine.on_event(OstpEvent::Start) {
handle_udp_action(action, &transport).await; handle_udp_action(action, &transport).await;
} }
// Send the actual UDP payload // Wait for handshake response (server sends HandshakePayload back)
let relay_msg = ostp_core::relay::RelayMessage::Connect(format!("{}:{}", target_dst.ip(), target_dst.port())); let mut buf = [0u8; 8192];
let encoded = relay_msg.encode(); match tokio::time::timeout(
std::time::Duration::from_millis(15000),
transport.recv(&mut buf),
).await {
Ok(Ok(n)) => {
let _ = machine.on_event(OstpEvent::Inbound(bytes::Bytes::copy_from_slice(&buf[..n])));
}
_ => {
tracing::warn!("OSTP handshake timeout for {}:{}", server, port);
return Ok(());
}
}
// Send relay UdpAssociate + data
let assoc_msg = ostp_core::relay::RelayMessage::UdpAssociate;
let encoded = assoc_msg.encode();
if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::from(encoded))) { if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::from(encoded))) {
handle_udp_action(action, &transport).await; handle_udp_action(action, &transport).await;
} }
// Send data packet let data_msg = ostp_core::relay::RelayMessage::UdpData(
let data_msg = ostp_core::relay::RelayMessage::Data(payload.to_vec()); format!("{}:{}", target_dst.ip(), target_dst.port()),
payload.to_vec()
);
let encoded = data_msg.encode(); let encoded = data_msg.encode();
if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::from(encoded))) { if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::from(encoded))) {
handle_udp_action(action, &transport).await; handle_udp_action(action, &transport).await;
@ -166,13 +297,15 @@ pub async fn handle_udp(
// Keep-alive for a short time to receive response // Keep-alive for a short time to receive response
for _ in 0..5 { for _ in 0..5 {
let mut buf = [0u8; 8192];
match tokio::time::timeout( match tokio::time::timeout(
std::time::Duration::from_millis(100), std::time::Duration::from_millis(100),
transport.recv(&mut buf) transport.recv(&mut buf),
).await { ).await {
Ok(Ok(n)) => { Ok(Ok(n)) => {
let _ = machine.on_event(OstpEvent::Inbound(bytes::Bytes::copy_from_slice(&buf[..n]))); if let Ok(action) = machine.on_event(OstpEvent::Inbound(bytes::Bytes::copy_from_slice(&buf[..n]))) {
// Just process incoming UDP response internally
let _ = action;
}
} }
_ => break, _ => break,
} }
@ -181,6 +314,76 @@ pub async fn handle_udp(
Ok(()) Ok(())
} }
async fn make_transport(
transport_cfg: &TransportConfig,
server: &str,
port: u16,
) -> Result<crate::transport::Transport> {
let debug = tracing::enabled!(tracing::Level::DEBUG);
match transport_cfg.r#type.as_str() {
"dns" => {
let domain = transport_cfg.domain.clone()
.unwrap_or_else(|| "tunnel.example.com".to_string());
let pubkey = transport_cfg.pubkey.clone()
.unwrap_or_else(|| "".to_string());
let resolver = transport_cfg.resolver.clone()
.unwrap_or_else(|| server.to_string());
let resolver_with_port = if resolver.contains(':') {
resolver.clone()
} else {
format!("{}:53", resolver)
};
let (local_port, process) = ostp_core::dnstt::spawn_client(&pubkey, &domain, &resolver_with_port, debug)?;
// Wait for dnstt-client to start its local TCP listener
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
// Connect TCP to the local dnstt-client port
let stream = tokio::net::TcpStream::connect(("127.0.0.1", local_port)).await?;
let (mut rh, mut wh) = stream.into_split();
let (tx_send, mut tx_recv) = tokio::sync::mpsc::channel::<bytes::Bytes>(1024);
let (rx_send, rx_recv) = tokio::sync::mpsc::channel::<bytes::Bytes>(1024);
// Writer task
tokio::spawn(async move {
use tokio::io::AsyncWriteExt;
while let Some(data) = tx_recv.recv().await {
let len = data.len() as u16;
if wh.write_u16(len).await.is_err() { break; }
if wh.write_all(&data).await.is_err() { break; }
}
});
// Reader task
tokio::spawn(async move {
use tokio::io::AsyncReadExt;
loop {
let len = match rh.read_u16().await {
Ok(l) => l,
Err(_) => break,
};
let mut buf = vec![0u8; len as usize];
if rh.read_exact(&mut buf).await.is_err() { break; }
if rx_send.send(bytes::Bytes::from(buf)).await.is_err() { break; }
}
});
Ok(crate::transport::Transport::Dnstt {
tx: tx_send,
rx: std::sync::Arc::new(tokio::sync::Mutex::new(rx_recv)),
_guard: std::sync::Arc::new(tokio::sync::Mutex::new(process)),
})
}
_ => {
let udp = tokio::net::UdpSocket::bind("0.0.0.0:0").await?;
udp.connect((server, port)).await?;
Ok(crate::transport::Transport::Udp(std::sync::Arc::new(udp)))
}
}
}
async fn handle_udp_action(action: ProtocolAction, transport: &crate::transport::Transport) { async fn handle_udp_action(action: ProtocolAction, transport: &crate::transport::Transport) {
match action { match action {
ProtocolAction::SendDatagram(data) => { ProtocolAction::SendDatagram(data) => {
@ -203,17 +406,53 @@ async fn handle_action(action: ProtocolAction, transport: &crate::transport::Tra
let _ = transport.send(&data).await; let _ = transport.send(&data).await;
} }
ProtocolAction::DeliverApp(_stream_id, payload) => { ProtocolAction::DeliverApp(_stream_id, payload) => {
let _ = server_stream.write_all(&payload).await; if let Ok(msg) = ostp_core::relay::RelayMessage::decode(&payload) {
match msg {
ostp_core::relay::RelayMessage::Data(data) => {
let _ = server_stream.write_all(&data).await;
}
ostp_core::relay::RelayMessage::ConnectOk => {
tracing::debug!("TCP Connection established successfully");
}
ostp_core::relay::RelayMessage::Error(err) => {
tracing::warn!("Server returned TCP connection error: {}", err);
}
_ => {}
}
}
} }
ProtocolAction::Multiple(actions) => { ProtocolAction::Multiple(actions) => {
for a in actions { for a in actions {
match a { Box::pin(handle_action(a, transport, server_stream)).await;
ProtocolAction::SendDatagram(data) => { let _ = transport.send(&data).await; }
ProtocolAction::DeliverApp(_stream_id, payload) => { let _ = server_stream.write_all(&payload).await; }
_ => {}
}
} }
} }
_ => {} _ => {}
} }
} }
/// Inspect a ProtocolAction for ConnectOk / Error relay messages.
/// Returns Some(true) on ConnectOk, Some(false) on Error, None if neither.
/// Works recursively through Multiple actions.
fn check_connect_result(action: &ProtocolAction) -> Option<bool> {
match action {
ProtocolAction::DeliverApp(_stream_id, payload) => {
if let Ok(msg) = ostp_core::relay::RelayMessage::decode(payload) {
match msg {
ostp_core::relay::RelayMessage::ConnectOk => return Some(true),
ostp_core::relay::RelayMessage::Error(_) => return Some(false),
_ => {}
}
}
None
}
ProtocolAction::Multiple(actions) => {
for a in actions {
if let Some(result) = check_connect_result(a) {
return Some(result);
}
}
None
}
_ => None,
}
}

View File

@ -126,7 +126,6 @@ pub fn get_process_name_from_port(port: u16) -> Option<String> {
use std::fs; use std::fs;
use std::io::{BufRead, BufReader}; use std::io::{BufRead, BufReader};
let mut target_inode = None;
let hex_port = format!("{:04X}", port); let hex_port = format!("{:04X}", port);
let check_net_file = |path: &str| -> Option<u64> { let check_net_file = |path: &str| -> Option<u64> {
@ -146,12 +145,11 @@ pub fn get_process_name_from_port(port: u16) -> Option<String> {
None None
}; };
target_inode = check_net_file("/proc/net/tcp") let target_inode = check_net_file("/proc/net/tcp")
.or_else(|| check_net_file("/proc/net/tcp6")) .or_else(|| check_net_file("/proc/net/tcp6"))
.or_else(|| check_net_file("/proc/net/udp")) .or_else(|| check_net_file("/proc/net/udp"))
.or_else(|| check_net_file("/proc/net/udp6")); .or_else(|| check_net_file("/proc/net/udp6"))?;
let target_inode = target_inode?;
let socket_str = format!("socket:[{}]", target_inode); let socket_str = format!("socket:[{}]", target_inode);
for entry in fs::read_dir("/proc").ok()?.filter_map(Result::ok) { for entry in fs::read_dir("/proc").ok()?.filter_map(Result::ok) {

View File

@ -17,3 +17,5 @@ sha2.workspace = true
hmac.workspace = true hmac.workspace = true
x25519-dalek = { version = "2.0.1", features = ["static_secrets"] } x25519-dalek = { version = "2.0.1", features = ["static_secrets"] }
hkdf = "0.12.0" hkdf = "0.12.0"
tokio.workspace = true
serde = { version = "1.0", features = ["derive"] }

View File

@ -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);
}
} }

View File

@ -1,5 +1,5 @@
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use std::io::{Cursor, Read, Write}; use std::io::{Cursor, Read};
const BASE32_ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz234567"; const BASE32_ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz234567";
@ -136,13 +136,17 @@ impl DnsPacket {
qtype: rtype.clone(), qtype: rtype.clone(),
qclass: 1, // IN qclass: 1, // IN
}], }],
answers: vec![DnsAnswer { answers: if rdata.is_empty() {
name: name.to_string(), vec![]
rtype, } else {
rclass: 1, vec![DnsAnswer {
ttl: 0, // No caching name: name.to_string(),
rdata, rtype,
}], rclass: 1,
ttl: 0, // No caching
rdata,
}]
},
} }
} }

View File

@ -0,0 +1,94 @@
use std::time::Duration;
use tokio::time::Instant;
use crate::dns::{DnsPacket, DnsRecordType, encode_payload_to_domain};
use rand::Rng;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone)]
pub struct DnsProbeResult {
pub name: String,
pub ip: String,
pub latency_ms: Option<u64>,
}
const PUBLIC_DNS_SERVERS: &[(&str, &str)] = &[
("Cloudflare", "1.1.1.1"),
("Cloudflare2", "1.0.0.1"),
("Google", "8.8.8.8"),
("Google2", "8.8.4.4"),
("Quad9", "9.9.9.9"),
("AdGuard", "94.140.14.14"),
("Yandex", "77.88.8.8"),
("Yandex2", "77.88.8.1"),
("SkyDNS", "193.58.251.251"),
("AliDNS", "223.5.5.5"),
("Tencent", "119.29.29.29"),
("114DNS", "114.114.114.114"),
("Shecan", "178.22.122.100"),
("Electro", "78.157.42.100"),
("Begzar", "185.55.226.26"),
];
async fn probe_resolver(domain: &str, resolver_ip: &str) -> Option<u64> {
let (probe_bytes, id) = {
let mut rng = rand::thread_rng();
let probe_bytes: [u8; 4] = rng.gen();
let id: u16 = rng.gen();
(probe_bytes, id)
};
let fqdn = encode_payload_to_domain(&probe_bytes, domain);
let qtype = if rand::thread_rng().gen_bool(0.5) { DnsRecordType::TXT } else { DnsRecordType::NULL };
let packet = DnsPacket::new_query(id, &fqdn, qtype);
let encoded = packet.encode();
let sock = tokio::net::UdpSocket::bind("0.0.0.0:0").await.ok()?;
sock.connect(format!("{}:53", resolver_ip)).await.ok()?;
let start = Instant::now();
sock.send(&encoded).await.ok()?;
let mut buf = [0u8; 4096];
match tokio::time::timeout(Duration::from_secs(2), sock.recv(&mut buf)).await {
Ok(Ok(n)) => {
if let Some(resp) = DnsPacket::decode(&buf[..n]) {
// Check if RCODE == 0 (NOERROR) and it has answers
let rcode = resp.flags & 0x000F;
if rcode == 0 && !resp.answers.is_empty() {
return Some(start.elapsed().as_millis() as u64);
}
}
None
},
_ => None,
}
}
pub async fn run_dns_prober(domain: &str) -> Result<Vec<DnsProbeResult>, String> {
if domain.is_empty() {
return Err("Please enter the tunnel domain first (e.g. tunnel.myvpn.com)".into());
}
let tasks: Vec<_> = PUBLIC_DNS_SERVERS
.iter()
.map(|(name, ip)| {
let domain = domain.to_string();
let name = name.to_string();
let ip = ip.to_string();
tokio::spawn(async move {
let latency_ms = probe_resolver(&domain, &ip).await;
DnsProbeResult { name, ip, latency_ms }
})
})
.collect();
let mut results = Vec::with_capacity(tasks.len());
for task in tasks {
if let Ok(r) = task.await {
results.push(r);
}
}
results.sort_by_key(|r| r.latency_ms.unwrap_or(u64::MAX));
Ok(results)
}

View File

@ -5,6 +5,8 @@ pub mod protocol;
pub mod relay; pub mod relay;
pub mod resumption; pub mod resumption;
pub mod dns; pub mod dns;
pub mod dns_prober;
pub mod dnstt;
pub use crypto::NoiseRole; pub use crypto::NoiseRole;
pub use framing::{TrafficProfile, PaddingStrategy}; pub use framing::{TrafficProfile, PaddingStrategy};

View File

@ -2,7 +2,7 @@ use bytes::Bytes;
use rand::Rng; use rand::Rng;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use thiserror::Error; use thiserror::Error;
use std::collections::{BTreeMap, VecDeque}; use std::collections::BTreeMap;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use crate::congestion::CongestionController; use crate::congestion::CongestionController;
@ -75,7 +75,7 @@ pub struct ProtocolMachine {
send_nonce: u64, send_nonce: u64,
expected_recv_nonce: u64, expected_recv_nonce: u64,
reorder_buffer: BTreeMap<u64, ProtocolAction>, reorder_buffer: BTreeMap<u64, ProtocolAction>,
sent_history: VecDeque<SentFrame>, sent_history: BTreeMap<u64, SentFrame>,
session_id: u32, session_id: u32,
handshake_payload: Vec<u8>, handshake_payload: Vec<u8>,
padder: AdaptivePadder, padder: AdaptivePadder,
@ -83,7 +83,8 @@ pub struct ProtocolMachine {
max_reorder: u64, max_reorder: u64,
max_reorder_buffer: usize, max_reorder_buffer: usize,
ack_delay: Duration, ack_delay: Duration,
rto: Duration, /// Initial/fallback RTO from config (overridden by cc.rto() after first RTT sample)
rto_initial: Duration,
max_retries: u8, max_retries: u8,
max_sent_history: usize, max_sent_history: usize,
ack_pending: bool, ack_pending: bool,
@ -100,11 +101,11 @@ pub struct ProtocolMachine {
/// Key-derived handshake padding range /// Key-derived handshake padding range
handshake_pad_min: usize, handshake_pad_min: usize,
handshake_pad_max: usize, handshake_pad_max: usize,
_mtu: usize,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct SentFrame { struct SentFrame {
#[allow(dead_code)] // mirrored in BTreeMap key; kept for Debug output
nonce: u64, nonce: u64,
bytes: Bytes, bytes: Bytes,
last_sent: Instant, last_sent: Instant,
@ -128,7 +129,7 @@ impl ProtocolMachine {
send_nonce: 0, send_nonce: 0,
expected_recv_nonce: 0, expected_recv_nonce: 0,
reorder_buffer: BTreeMap::new(), reorder_buffer: BTreeMap::new(),
sent_history: VecDeque::with_capacity(config.max_sent_history.max(1)), sent_history: BTreeMap::new(),
session_id: config.session_id, session_id: config.session_id,
handshake_payload: config.handshake_payload, handshake_payload: config.handshake_payload,
padder: AdaptivePadder::new(config.mtu, config.max_padding, config.padding_strategy), padder: AdaptivePadder::new(config.mtu, config.max_padding, config.padding_strategy),
@ -136,7 +137,7 @@ impl ProtocolMachine {
max_reorder: config.max_reorder.max(1), max_reorder: config.max_reorder.max(1),
max_reorder_buffer: config.max_reorder_buffer.max(1), max_reorder_buffer: config.max_reorder_buffer.max(1),
ack_delay: Duration::from_millis(config.ack_delay_ms.max(1)), ack_delay: Duration::from_millis(config.ack_delay_ms.max(1)),
rto: Duration::from_millis(config.rto_ms.max(1)), rto_initial: Duration::from_millis(config.rto_ms.max(1)),
max_retries: config.max_retries.max(1), max_retries: config.max_retries.max(1),
max_sent_history: config.max_sent_history.max(1), max_sent_history: config.max_sent_history.max(1),
ack_pending: false, ack_pending: false,
@ -146,20 +147,25 @@ impl ProtocolMachine {
cc: CongestionController::new(config.mtu as u64), cc: CongestionController::new(config.mtu as u64),
handshake_pad_min: config.handshake_pad_min.max(8), handshake_pad_min: config.handshake_pad_min.max(8),
handshake_pad_max: config.handshake_pad_max.max(config.handshake_pad_min + 16), handshake_pad_max: config.handshake_pad_max.max(config.handshake_pad_min + 16),
_mtu: config.mtu,
}) })
} }
pub fn in_flight_count(&self) -> usize { pub fn in_flight_count(&self) -> usize {
// COUNT ONLY retransmittable Data frames — control frames (Ack/Nack) must not // COUNT ONLY retransmittable Data frames — control frames (Ack/Nack) must not
// contribute to this counter or they will trigger false backpressure. // contribute to this counter or they will trigger false backpressure.
self.sent_history.iter().filter(|f| f.is_retransmittable).count() self.sent_history.values().filter(|f| f.is_retransmittable).count()
} }
pub fn cwnd_packets(&self) -> usize { pub fn cwnd_packets(&self) -> usize {
self.cc.cwnd_packets() as usize self.cc.cwnd_packets() as usize
} }
/// Returns the current adaptive RTO (from congestion controller after first RTT sample,
/// falls back to the config-specified initial value before any ACK is received).
pub fn rto(&self) -> Duration {
self.cc.rto()
}
pub fn on_send(&mut self, bytes: u64) { pub fn on_send(&mut self, bytes: u64) {
self.cc.on_send(bytes); self.cc.on_send(bytes);
} }
@ -207,13 +213,12 @@ impl ProtocolMachine {
.map(ProtocolAction::SendDatagram) .map(ProtocolAction::SendDatagram)
} }
(OstpState::Closing, OstpEvent::Inbound(raw)) => { (OstpState::Closing, OstpEvent::Inbound(raw)) => {
// Process final in-flight packets to prevent data loss during teardown. // The remote may still have data or ACKs in transit.
// The remote may still have data or ACKs in transit when we initiated Close. // handle_inbound transitions to Closed when it receives a Close frame.
let result = self.handle_inbound(raw); self.handle_inbound(raw)
self.state = OstpState::Closed;
result
} }
(OstpState::Established, OstpEvent::Tick) => self.handle_tick(), (OstpState::Established, OstpEvent::Tick) => self.handle_tick(),
(OstpState::Closing, OstpEvent::Tick) => self.handle_tick(),
(OstpState::Closed, _) => Ok(ProtocolAction::Noop), (OstpState::Closed, _) => Ok(ProtocolAction::Noop),
(_, OstpEvent::Close) => { (_, OstpEvent::Close) => {
self.state = OstpState::Closed; self.state = OstpState::Closed;
@ -392,18 +397,26 @@ 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 self.reorder_buffer.len() >= self.max_reorder_buffer {
self.reorder_buffer.insert(nonce, action); tracing::warn!("Reorder buffer full ({}/{}), dropping new frame nonce={} to wait for recovery of nonce={}",
} else { self.reorder_buffer.len(), self.max_reorder_buffer, nonce, self.expected_recv_nonce
tracing::warn!("Reorder buffer full ({}/{}), dropping frame nonce={}",
self.reorder_buffer.len(), self.max_reorder_buffer, nonce
); );
} }
// Rate-limited NACK: send at most once per 30ms to prevent retransmit storms. if nonce >= self.expected_recv_nonce {
// Under high load with natural UDP reordering, sending a NACK per packet if self.reorder_buffer.len() < self.max_reorder_buffer {
// causes exponential retransmit explosion that saturates the channel. self.reorder_buffer.insert(nonce, action);
let nack_cooldown = Duration::from_millis(30); } else {
tracing::warn!("Reorder buffer still full after gap recovery, dropping frame nonce={}", nonce);
}
} else {
tracing::debug!("Frame nonce={} arrived too late after gap recovery, dropping", nonce);
}
// Rate-limited NACK: send at most once per (rto/2) to prevent retransmit storms.
// Using rto/2 means we send a NACK before the sender's timer fires, prompting
// fast retransmit without flooding. Floor at 10ms to handle very low-RTT links.
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();
@ -511,62 +524,39 @@ 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 = self.cc.rto().max(self.rto_initial);
let base_rto_ms = base_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);
let evicted = before - self.sent_history.len(); let evicted = before - self.sent_history.len();
if evicted > 0 { if evicted > 0 {
tracing::debug!("Evicted {} zombie frames from sent_history (remaining={})", evicted, self.sent_history.len()); tracing::debug!("Evicted {} zombie frames from sent_history (remaining={})", evicted, self.sent_history.len());
} }
// ── Retransmit expired frames ──────────────────────────────── // ── Retransmit expired frames ────────────────────────────────
// 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×
// This ensures we do not flood with retransmits on the first few losses
// while still recovering quickly on a transient single loss.
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.values_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 {
@ -672,7 +662,7 @@ impl ProtocolMachine {
} }
fn lookup_sent_frame(&mut self, nonce: u64) -> Option<Bytes> { fn lookup_sent_frame(&mut self, nonce: u64) -> Option<Bytes> {
if let Some(frame) = self.sent_history.iter_mut().rev().find(|f| f.nonce == nonce) { if let Some(frame) = self.sent_history.get_mut(&nonce) {
frame.last_sent = Instant::now(); frame.last_sent = Instant::now();
frame.retries = frame.retries.saturating_add(1); frame.retries = frame.retries.saturating_add(1);
return Some(frame.bytes.clone()); return Some(frame.bytes.clone());
@ -684,7 +674,7 @@ impl ProtocolMachine {
if is_retransmittable { if is_retransmittable {
self.cc.on_send(bytes.len() as u64); self.cc.on_send(bytes.len() as u64);
} }
self.sent_history.push_back(SentFrame { self.sent_history.insert(nonce, SentFrame {
nonce, nonce,
bytes, bytes,
last_sent: Instant::now(), last_sent: Instant::now(),
@ -697,7 +687,7 @@ impl ProtocolMachine {
overflow, self.max_sent_history overflow, self.max_sent_history
); );
while self.sent_history.len() > self.max_sent_history { while self.sent_history.len() > self.max_sent_history {
self.sent_history.pop_front(); self.sent_history.pop_first();
} }
} }
} }
@ -708,8 +698,8 @@ impl ProtocolMachine {
let mut min_rtt = Duration::from_secs(60); let mut min_rtt = Duration::from_secs(60);
// Compute RTT from the oldest acked frame's send timestamp // Compute RTT from the oldest acked frame's send timestamp
for frame in self.sent_history.iter() { for (&nonce, frame) in &self.sent_history {
if nonce_in_ranges(frame.nonce, ranges) { if nonce_in_ranges(nonce, ranges) {
acked_bytes += frame.bytes.len() as u64; acked_bytes += frame.bytes.len() as u64;
let rtt = now.duration_since(frame.last_sent); let rtt = now.duration_since(frame.last_sent);
if rtt < min_rtt { if rtt < min_rtt {
@ -718,7 +708,7 @@ impl ProtocolMachine {
} }
} }
self.sent_history.retain(|frame| !nonce_in_ranges(frame.nonce, ranges)); self.sent_history.retain(|&nonce, _| !nonce_in_ranges(nonce, ranges));
// Notify congestion controller // Notify congestion controller
if acked_bytes > 0 { if acked_bytes > 0 {

View File

@ -95,6 +95,15 @@ class MainActivity : FlutterActivity() {
result.error("ERROR", e.message, null) result.error("ERROR", e.message, null)
} }
} }
"runDnsProber" -> {
try {
val domain = call.argument<String>("domain") ?: "example.com"
val json = net.ostp.client.OstpClientSdk.nativeRunDnsProber(domain)
result.success(json)
} catch (e: Throwable) {
result.error("ERROR", e.message, null)
}
}
"getInstalledApps" -> { "getInstalledApps" -> {
try { try {
val pm = packageManager val pm = packageManager

View File

@ -50,4 +50,8 @@ object OstpClientSdk {
@Keep @Keep
@JvmStatic @JvmStatic
external fun notifyNetworkChanged() external fun notifyNetworkChanged()
@Keep
@JvmStatic
external fun nativeRunDnsProber(domain: String): String
} }

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -26,11 +26,11 @@ class OstpApp extends StatelessWidget {
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( theme: ThemeData(
brightness: Brightness.dark, brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF08080F), scaffoldBackgroundColor: const Color(0xFF030303),
colorScheme: const ColorScheme.dark( colorScheme: const ColorScheme.dark(
primary: Color(0xFF6C72FF), primary: Color(0xFFF9FAFB),
secondary: Color(0xFF22D3A5), secondary: Color(0xFF10B981),
surface: Color(0xFF151522), surface: Color(0xFF09090B),
), ),
fontFamily: 'Inter', fontFamily: 'Inter',
useMaterial3: true, useMaterial3: true,

View File

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../models/connection_state_enum.dart'; import '../models/connection_state_enum.dart';
import 'settings_screen.dart'; import 'settings_screen.dart';
import 'logs_screen.dart'; import 'logs_screen.dart';
@ -517,31 +518,16 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
return Scaffold( return Scaffold(
body: Stack( body: Stack(
children: [ children: [
Positioned( Center(
top: -150, right: -100, child: Opacity(
child: Container( opacity: theme.brightness == Brightness.dark ? 0.05 : 0.06,
width: 400, height: 400, child: SvgPicture.asset(
decoration: BoxDecoration( 'assets/logo.svg',
shape: BoxShape.circle, width: MediaQuery.of(context).size.width * 0.8,
color: theme.colorScheme.primary.withOpacity(0.15), fit: BoxFit.contain,
), colorFilter: theme.brightness == Brightness.light
child: BackdropFilter( ? const ColorFilter.mode(Colors.black, BlendMode.srcIn)
filter: ImageFilter.blur(sigmaX: 100, sigmaY: 100), : null,
child: Container(),
),
),
),
Positioned(
bottom: -100, left: -100,
child: Container(
width: 350, height: 350,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: theme.colorScheme.secondary.withOpacity(0.1),
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 100, sigmaY: 100),
child: Container(),
), ),
), ),
), ),

View File

@ -38,7 +38,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
bool _obscureKey = true; bool _obscureKey = true;
bool _debugMode = false; bool _debugMode = false;
String _dnsRegion = 'Global'; late TextEditingController _dnsRegionCtrl;
String _transportMode = 'udp'; // 'udp' | 'uot' String _transportMode = 'udp'; // 'udp' | 'uot'
String _tunStack = 'ostp'; // 'system' | 'ostp' String _tunStack = 'ostp'; // 'system' | 'ostp'
bool _muxEnabled = false; bool _muxEnabled = false;
@ -58,9 +58,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
_ipsCtrl = TextEditingController(text: widget.prefs.getString('ex_ips') ?? ''); _ipsCtrl = TextEditingController(text: widget.prefs.getString('ex_ips') ?? '');
_processesCtrl = TextEditingController(text: widget.prefs.getString('ex_processes') ?? ''); _processesCtrl = TextEditingController(text: widget.prefs.getString('ex_processes') ?? '');
_dnsDomainCtrl = TextEditingController(text: widget.prefs.getString('dns_domain') ?? ''); _dnsDomainCtrl = TextEditingController(text: widget.prefs.getString('dns_domain') ?? '');
_pbkCtrl = TextEditingController(text: widget.prefs.getString('pbk') ?? ''); _dnsRegionCtrl = TextEditingController(text: widget.prefs.getString('dns_region') ?? '1.1.1.1');
_pbkCtrl = TextEditingController(text: widget.prefs.getString('tun_pbk') ?? '');
_sidCtrl = TextEditingController(text: widget.prefs.getString('sid') ?? ''); _sidCtrl = TextEditingController(text: widget.prefs.getString('sid') ?? '');
_dnsRegion = widget.prefs.getString('dns_region') ?? 'Global';
_transportMode = widget.prefs.getString('transport_mode') ?? 'udp'; _transportMode = widget.prefs.getString('transport_mode') ?? 'udp';
_tunStack = widget.prefs.getString('tun_stack') ?? 'ostp'; _tunStack = widget.prefs.getString('tun_stack') ?? 'ostp';
_debugMode = widget.prefs.getBool('debug_mode') ?? false; _debugMode = widget.prefs.getBool('debug_mode') ?? false;
@ -81,6 +81,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
_ipsCtrl.dispose(); _ipsCtrl.dispose();
_processesCtrl.dispose(); _processesCtrl.dispose();
_dnsDomainCtrl.dispose(); _dnsDomainCtrl.dispose();
_dnsRegionCtrl.dispose();
_pbkCtrl.dispose(); _pbkCtrl.dispose();
_sidCtrl.dispose(); _sidCtrl.dispose();
_muxSessionsCtrl.dispose(); _muxSessionsCtrl.dispose();
@ -97,11 +98,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
widget.prefs.setString('ex_ips', _ipsCtrl.text.trim()); widget.prefs.setString('ex_ips', _ipsCtrl.text.trim());
widget.prefs.setString('ex_processes', _processesCtrl.text.trim()); widget.prefs.setString('ex_processes', _processesCtrl.text.trim());
widget.prefs.setBool('debug_mode', _debugMode); widget.prefs.setBool('debug_mode', _debugMode);
widget.prefs.setString('dns_region', _dnsRegion);
widget.prefs.setString('transport_mode', _transportMode); widget.prefs.setString('transport_mode', _transportMode);
widget.prefs.setString('tun_stack', _tunStack); widget.prefs.setString('tun_stack', _tunStack);
widget.prefs.setString('dns_domain', _dnsDomainCtrl.text.trim()); widget.prefs.setString('dns_domain', _dnsDomainCtrl.text.trim());
widget.prefs.setString('pbk', _pbkCtrl.text.trim()); widget.prefs.setString('dns_region', _dnsRegionCtrl.text.trim());
widget.prefs.setString('tun_pbk', _pbkCtrl.text.trim());
widget.prefs.setString('sid', _sidCtrl.text.trim()); widget.prefs.setString('sid', _sidCtrl.text.trim());
widget.prefs.setBool('mux_enabled', _muxEnabled); widget.prefs.setBool('mux_enabled', _muxEnabled);
widget.prefs.setString('mux_sessions', _muxSessionsCtrl.text.trim()); widget.prefs.setString('mux_sessions', _muxSessionsCtrl.text.trim());
@ -237,7 +238,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
_serverCtrl.text = host; _serverCtrl.text = host;
_keyCtrl.text = key; _keyCtrl.text = key;
_dnsDomainCtrl.text = uri.queryParameters['domain'] ?? ''; _dnsDomainCtrl.text = uri.queryParameters['domain'] ?? '';
_dnsRegion = uri.queryParameters['region'] ?? 'Global'; _dnsRegionCtrl.text = uri.queryParameters['resolver'] ?? '1.1.1.1';
final type = uri.queryParameters['type']; final type = uri.queryParameters['type'];
_transportMode = type == 'tcp' || type == 'http' ? 'uot' : (type == 'dns' ? 'dns' : 'udp'); _transportMode = type == 'tcp' || type == 'http' ? 'uot' : (type == 'dns' ? 'dns' : 'udp');
@ -349,30 +350,27 @@ class _SettingsScreenState extends State<SettingsScreen> {
const SizedBox(height: 16), const SizedBox(height: 16),
_buildTextField('Domain (Points to Server)', _dnsDomainCtrl, hint: 'tunnel.myvpn.com'), _buildTextField('Domain (Points to Server)', _dnsDomainCtrl, hint: 'tunnel.myvpn.com'),
const SizedBox(height: 16), const SizedBox(height: 16),
DropdownButtonFormField<String>( Row(
value: _dnsRegion, children: [
dropdownColor: const Color(0xFF1E1E2C), Expanded(
style: const TextStyle(color: Colors.white, fontSize: 14), child: _buildTextField('DNS Resolver Server', _dnsRegionCtrl, hint: '1.1.1.1'),
decoration: InputDecoration( ),
labelText: 'DNS Resolver Region', const SizedBox(width: 8),
labelStyle: const TextStyle(color: Colors.white54, fontSize: 13), Padding(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.only(top: 24.0),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: ElevatedButton(
), onPressed: _showDnsProberDialog,
items: ['Global', 'Russia', 'China', 'Iran'].map((String region) { style: ElevatedButton.styleFrom(
return DropdownMenuItem<String>( backgroundColor: Colors.orangeAccent.withOpacity(0.2),
value: region, foregroundColor: Colors.orangeAccent,
child: Text(region), elevation: 0,
); padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
}).toList(), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onChanged: (String? newValue) { ),
if (newValue != null) { child: const Text('PROBER', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)),
setState(() { ),
_dnsRegion = newValue; )
_saveSettings(); ],
});
}
},
), ),
], ],
), ),
@ -529,8 +527,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (_dnsDomainCtrl.text.trim().isNotEmpty) { if (_dnsDomainCtrl.text.trim().isNotEmpty) {
queryParams.add('domain=${Uri.encodeComponent(_dnsDomainCtrl.text.trim())}'); queryParams.add('domain=${Uri.encodeComponent(_dnsDomainCtrl.text.trim())}');
} }
if (_dnsRegion != 'Global') { final resolver = _dnsRegionCtrl.text.trim();
queryParams.add('region=${Uri.encodeComponent(_dnsRegion)}'); if (resolver.isNotEmpty && resolver != '1.1.1.1') {
queryParams.add('resolver=${Uri.encodeComponent(resolver)}');
} }
if (_pbkCtrl.text.trim().isNotEmpty) { if (_pbkCtrl.text.trim().isNotEmpty) {
queryParams.add('pbk=${Uri.encodeComponent(_pbkCtrl.text.trim())}'); queryParams.add('pbk=${Uri.encodeComponent(_pbkCtrl.text.trim())}');
@ -603,6 +602,97 @@ class _SettingsScreenState extends State<SettingsScreen> {
); );
} }
Future<void> _showDnsProberDialog() async {
const channel = MethodChannel('com.ospab.ostp/vpn');
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return StatefulBuilder(
builder: (context, setModalState) {
return AlertDialog(
backgroundColor: Theme.of(context).colorScheme.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: const Text('DNS Prober', textAlign: TextAlign.center),
content: FutureBuilder<String?>(
future: channel.invokeMethod<String>('runDnsProber', {'domain': _dnsDomainCtrl.text.trim()}),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Sending real tunnel probes...', style: TextStyle(color: Colors.white54, fontSize: 13), textAlign: TextAlign.center),
],
);
}
if (snapshot.hasError || !snapshot.hasData) {
return Text('Error: ${snapshot.error}', style: const TextStyle(color: Colors.redAccent));
}
List<dynamic> results = [];
try {
results = jsonDecode(snapshot.data!);
} catch (_) {}
if (results.isEmpty) {
return const Text('No results or all timed out.', style: TextStyle(color: Colors.redAccent));
}
return SizedBox(
width: double.maxFinite,
child: ListView.builder(
shrinkWrap: true,
itemCount: results.length,
itemBuilder: (context, index) {
final res = results[index];
final name = res['name'] ?? '';
final ip = res['ip'] ?? '';
final latency = res['latency_ms'];
final isBest = index == 0 && latency != null;
return ListTile(
onTap: latency != null ? () {
setState(() {
_dnsRegionCtrl.text = ip;
_saveSettings();
});
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('DNS set to $ip')));
} : null,
title: Text('${isBest ? '' : ''}$name', style: const TextStyle(fontSize: 14)),
subtitle: Text(ip, style: const TextStyle(fontSize: 12, color: Colors.white54)),
trailing: Text(
latency != null ? '$latency ms' : 'TIMEOUT',
style: TextStyle(
color: latency == null ? Colors.redAccent : (latency < 100 ? Colors.greenAccent : Colors.orangeAccent),
fontWeight: FontWeight.bold,
),
),
tileColor: isBest ? Colors.blueAccent.withOpacity(0.1) : null,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
);
},
),
);
},
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
)
],
);
}
);
}
);
}
Future<void> _checkForUpdates() async { Future<void> _checkForUpdates() async {
if (_isCheckingUpdates) return; if (_isCheckingUpdates) return;
setState(() { _isCheckingUpdates = true; }); setState(() { _isCheckingUpdates = true; });

View File

@ -134,6 +134,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "6.0.0"
flutter_svg:
dependency: "direct main"
description:
name: flutter_svg
sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -272,6 +280,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@ -581,6 +597,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.5" version: "3.1.5"
vector_graphics:
dependency: transitive
description:
name: vector_graphics
sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
vector_graphics_codec:
dependency: transitive
description:
name: vector_graphics_codec
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
url: "https://pub.dev"
source: hosted
version: "1.1.13"
vector_graphics_compiler:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: "142a9146f447d15b10bdc00e21d5f4d83e5b32bb5f8f8f5a04c75311344923a3"
url: "https://pub.dev"
source: hosted
version: "1.2.6"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 0.3.7+20 version: 0.3.12+25
environment: environment:
sdk: ^3.11.4 sdk: ^3.11.4
@ -34,6 +34,7 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
flutter_svg: ^2.0.10
shared_preferences: ^2.5.5 shared_preferences: ^2.5.5
mobile_scanner: ^5.0.0 mobile_scanner: ^5.0.0
window_manager: ^0.5.1 window_manager: ^0.5.1
@ -72,9 +73,8 @@ flutter:
uses-material-design: true uses-material-design: true
# To add assets to your application, add an assets section, like this: # To add assets to your application, add an assets section, like this:
# assets: assets:
# - images/a_dot_burr.jpeg - assets/logo.svg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images # https://flutter.dev/to/resolution-aware-images

View File

@ -2665,7 +2665,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-client" name = "ostp-client"
version = "0.2.98" version = "0.3.12"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.22.1", "base64 0.22.1",
@ -2700,17 +2700,20 @@ dependencies = [
[[package]] [[package]]
name = "ostp-core" name = "ostp-core"
version = "0.2.98" version = "0.3.12"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"byteorder",
"bytes", "bytes",
"chacha20poly1305", "chacha20poly1305",
"hkdf", "hkdf",
"hmac", "hmac",
"rand", "rand",
"serde",
"sha2", "sha2",
"snow", "snow",
"thiserror 1.0.69", "thiserror 1.0.69",
"tokio",
"tracing", "tracing",
"x25519-dalek", "x25519-dalek",
] ]
@ -2720,12 +2723,16 @@ name = "ostp-gui"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chacha20poly1305",
"hex",
"json_comments", "json_comments",
"ostp-client", "ostp-client",
"ostp-core",
"portable-atomic", "portable-atomic",
"rand", "rand",
"serde", "serde",
"serde_json", "serde_json",
"sha2",
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-plugin-opener", "tauri-plugin-opener",
@ -2735,7 +2742,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-tun" name = "ostp-tun"
version = "0.2.98" version = "0.3.12"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"libc", "libc",

View File

@ -26,6 +26,7 @@ tokio = { version = "1", features = ["full"] }
anyhow = "1" anyhow = "1"
tracing = "0.1" tracing = "0.1"
ostp-client = { path = "../../ostp-client" } ostp-client = { path = "../../ostp-client" }
ostp-core = { path = "../../ostp-core" }
portable-atomic = "1" portable-atomic = "1"
json_comments = "0.2" json_comments = "0.2"
rand = "0.8" rand = "0.8"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 710 B

After

Width:  |  Height:  |  Size: 955 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -6,11 +6,15 @@ description = "Enables access to core OSTP commands"
allow = [ allow = [
"start_tunnel", "start_tunnel",
"stop_tunnel", "stop_tunnel",
"reload_tunnel",
"get_tunnel_status", "get_tunnel_status",
"get_metrics", "get_metrics",
"get_config", "get_config",
"save_config", "save_config",
"get_wintun_install_path", "get_wintun_install_path",
"set_autostart", "set_autostart",
"get_autostart" "get_autostart",
"list_running_processes",
"kill_auto_search",
"run_dns_prober"
] ]

View File

@ -0,0 +1,6 @@
use ostp_core::dns_prober::{run_dns_prober as core_run_dns_prober, DnsProbeResult};
#[tauri::command]
pub async fn run_dns_prober(domain: String) -> Result<Vec<DnsProbeResult>, String> {
core_run_dns_prober(&domain).await
}

View File

@ -1,41 +1,3 @@
use anyhow::{anyhow, Result}; // Re-export the shared IPC crypto from ostp-client so that GUI and tun-helper
use chacha20poly1305::{ChaCha20Poly1305, Nonce}; // always use identical encrypt/decrypt logic.
use chacha20poly1305::aead::{Aead, KeyInit}; pub use ostp_client::ipc_crypto::{derive_key, IpcCrypto};
use sha2::{Sha256, Digest};
pub struct IpcCrypto {
cipher: ChaCha20Poly1305,
nonce: [u8; 12],
}
impl IpcCrypto {
pub fn new(key: &[u8; 32]) -> Self {
let cipher = ChaCha20Poly1305::new_from_slice(key)
.expect("valid key size");
let nonce = [0u8; 12];
Self { cipher, nonce }
}
pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
let nonce = Nonce::from_slice(&self.nonce);
let ciphertext = self.cipher.encrypt(nonce, plaintext)
.map_err(|e| anyhow!("Encryption failed: {}", e))?;
Ok(ciphertext)
}
pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
let nonce = Nonce::from_slice(&self.nonce);
let plaintext = self.cipher.decrypt(nonce, ciphertext)
.map_err(|e| anyhow!("Decryption failed: {}", e))?;
Ok(plaintext)
}
}
pub fn derive_key(token: &str) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(token.as_bytes());
let result = hasher.finalize();
let mut key = [0u8; 32];
key.copy_from_slice(&result);
key
}

View File

@ -8,6 +8,7 @@ use portable_atomic::Ordering;
use tauri::Emitter; use tauri::Emitter;
mod ipc_crypto; mod ipc_crypto;
mod dns_prober;
// ── Config types ───────────────────────────────────────────────────────────── // ── Config types ─────────────────────────────────────────────────────────────
@ -39,7 +40,7 @@ struct UIMetrics {
#[serde(tag = "type", rename_all = "lowercase")] #[serde(tag = "type", rename_all = "lowercase")]
enum HelperMsg { enum HelperMsg {
Status { value: u8 }, Status { value: u8 },
Log { message: String }, Log { #[allow(dead_code)] message: String },
Metrics { bytes_sent: u64, bytes_recv: u64, rtt_ms: u32 }, Metrics { bytes_sent: u64, bytes_recv: u64, rtt_ms: u32 },
Error { message: String }, Error { message: String },
} }
@ -58,6 +59,7 @@ struct HelperState {
pipe_state: Arc<Mutex<HelperPipeState>>, pipe_state: Arc<Mutex<HelperPipeState>>,
cmd_tx: tokio::sync::mpsc::Sender<String>, cmd_tx: tokio::sync::mpsc::Sender<String>,
token: String, token: String,
#[allow(dead_code)]
port: u16, port: u16,
} }
@ -778,7 +780,7 @@ static SINGLE_INSTANCE_LOCK: std::sync::OnceLock<std::net::TcpListener> = std::s
pub fn run() { 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 if !cfg!(debug_assertions) {
show_error_dialog("Приложение OSTP GUI уже запущено!"); show_error_dialog("Приложение OSTP GUI уже запущено!");
return; return;
} }
@ -874,7 +876,7 @@ pub fn run() {
} }
_ => {} _ => {}
}) })
.invoke_handler(tauri::generate_handler![start_tunnel, stop_tunnel, reload_tunnel, get_tunnel_status, get_metrics, get_config, save_config, get_wintun_install_path, set_autostart, get_autostart, list_running_processes]) .invoke_handler(tauri::generate_handler![start_tunnel, stop_tunnel, reload_tunnel, get_tunnel_status, get_metrics, get_config, save_config, get_wintun_install_path, set_autostart, get_autostart, list_running_processes, dns_prober::run_dns_prober])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@ -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.3.7", "version": "0.3.12",
"identifier": "com.ospab.ostp", "identifier": "com.ospab.ostp",
"build": { "build": {
"frontendDist": "../src" "frontendDist": "../src"

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -12,10 +12,9 @@
<body> <body>
<div class="app-root"> <div class="app-root">
<!-- Ambient light blobs --> <!-- Eagle Watermark -->
<div class="ambient" aria-hidden="true"> <div class="watermark" aria-hidden="true">
<div class="blob blob-1"></div> <img src="assets/logo.svg" alt="" />
<div class="blob blob-2"></div>
</div> </div>
<!-- ── HOME SCREEN ──────────────────────────────────────────── --> <!-- ── HOME SCREEN ──────────────────────────────────────────── -->
@ -202,24 +201,20 @@
</select> </select>
</div> </div>
<div id="group-dns-proxy" style="display: none;"> <div id="group-dns-proxy" style="display: none; flex-direction: column; gap: 14px;">
<div class="field-group"> <div class="field-group">
<label class="field-label" for="in-dns-domain" style="color: var(--c-warning);" data-i18n="label_dns_domain">Domain (Points to Server)</label> <label class="field-label" for="in-dns-domain" style="color: var(--c-warning);" data-i18n="label_dns_domain">Domain (Points to Server)</label>
<input id="in-dns-domain" class="field-input" type="text" placeholder="tunnel.myvpn.com" spellcheck="false" /> <input id="in-dns-domain" class="field-input" type="text" placeholder="tunnel.myvpn.com" spellcheck="false" />
<div style="font-size: 0.8rem; color: var(--c-sub); margin-top: 4px;">
<span data-i18n="dns_domain_hint">This is the "last resort" over public DNS servers. You need a domain pointing to your server (NS/A record).</span>
<span data-i18n="dns_guide">Detailed setup guide available in</span> <a href="https://github.com/ospab/ostp/wiki/DNS-Transport" target="_blank" style="color: var(--c-accent)" data-i18n="wiki_link">GitHub Wiki</a>.
</div>
</div> </div>
<div class="field-group"> <div class="field-group">
<label class="field-label" for="in-dns-region" data-i18n="label_dns_region">DNS Resolver Region (Prober)</label> <label class="field-label" for="in-dns-region" data-i18n="label_dns_region">DNS Resolver Server</label>
<select id="in-dns-region" class="field-input"> <div class="input-wrap">
<option value="Global" data-i18n="opt_global">Global (Cloudflare, Google, etc)</option> <input id="in-dns-region" class="field-input" type="text" placeholder="1.1.1.1" spellcheck="false" />
<option value="Russia" data-i18n="opt_russia">Russia (Yandex, VK, etc)</option> <button id="btn-dns-prober" class="peek-btn" tabindex="-1" title="Find fastest DNS server" style="width:auto; padding: 0 8px; font-size: 0.7rem; color: var(--c-accent);">
<option value="China" data-i18n="opt_china">China (AliDNS, DNSPod, etc)</option> PROBER
<option value="Iran" data-i18n="opt_iran">Iran (Shatel, Electro, etc)</option> </button>
</select> </div>
</div> </div>
</div> </div>
@ -380,6 +375,23 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
<!-- DNS Prober Modal -->
<div id="dns-prober-modal" class="modal-overlay hidden">
<div class="modal-content" style="min-width: 300px; max-width: 420px;">
<h3 class="modal-title">DNS Prober</h3>
<p class="modal-text" style="font-size: 0.8rem; color: var(--c-txt-2); margin-bottom: 12px;">
Sends a real DNS tunnel probe through each resolver and measures the round-trip time to your server. The fastest one is selected automatically.
</p>
<div id="prober-status" style="font-size: 0.75rem; color: var(--c-accent); margin-bottom: 10px; min-height: 18px;"></div>
<div id="prober-list" style="display: flex; flex-direction: column; gap: 5px; max-height: 280px; overflow-y: auto;"></div>
<div class="modal-actions" style="margin-top: 16px;">
<button id="btn-prober-close" class="btn secondary">Close</button>
</div>
</div>
</div>
</div> </div>
<script type="module" src="main.js"></script> <script type="module" src="main.js"></script>

Some files were not shown because too many files have changed in this diff Show More