Compare commits

..

No commits in common. "master" and "v0.3.8" have entirely different histories.

128 changed files with 2196 additions and 2426 deletions

BIN
.gitignore vendored

Binary file not shown.

View File

@ -22,7 +22,6 @@ 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,7 +22,6 @@
Для локальной сборки и тестирования 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,16 +388,6 @@ 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"
@ -1262,12 +1252,6 @@ 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"
@ -1447,18 +1431,16 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]] [[package]]
name = "ostp" name = "ostp"
version = "0.3.12" version = "0.3.8"
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",
@ -1471,7 +1453,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-client" name = "ostp-client"
version = "0.3.12" version = "0.3.8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
@ -1506,7 +1488,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-core" name = "ostp-core"
version = "0.3.12" version = "0.3.8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"byteorder", "byteorder",
@ -1515,11 +1497,9 @@ 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",
] ]
@ -1543,7 +1523,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-server" name = "ostp-server"
version = "0.3.12" version = "0.3.8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
@ -1576,7 +1556,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-tun" name = "ostp-tun"
version = "0.3.12" version = "0.3.8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"libc", "libc",
@ -1588,7 +1568,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-tun-helper" name = "ostp-tun-helper"
version = "0.3.12" version = "0.3.8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -1612,12 +1592,6 @@ 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.12" version = "0.3.8"
[workspace.dependencies] [workspace.dependencies]
anyhow = "1.0" anyhow = "1.0"

View File

@ -142,13 +142,8 @@ 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 ±300-second synchronization window to accommodate clock drift and mobile roaming scenarios. The initial handshake payload includes a Unix timestamp to mitigate replay attacks. The server enforces a strict ±30-second synchronization window.
--- ---

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). Сервер контролирует окно синхронизации (±300 секунд) с учётом дрейфа часов и смены сети при роуминге. Первичная полезная нагрузка рукопожатия содержит Unix-отметку времени для защиты от атак повторного воспроизведения (Replay Attacks). Сервер строго контролирует окно синхронизации (±30 секунд).
--- ---

36
fix_delimiter.py Normal file
View File

@ -0,0 +1,36 @@
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

File diff suppressed because one or more lines are too long

Before

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", "time"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
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,8 +50,6 @@ 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,
}, },
} }
@ -174,42 +172,33 @@ 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::warn!( tracing::info!("Config was migrated to v0.3.1. Saving to {}", path.display());
"Config at {} is in an outdated format. Run 'ostp --migrate' to upgrade it.", let serialized = serde_json::to_string_pretty(&migrated_json)?;
path.display() let header = "// OSTP Configuration v0.3.1\n// DO NOT EDIT THIS COMMENT - Migrator relies on it\n";
); 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 config from {}", path.display()))?; .with_context(|| format!("failed to deserialize migrated 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(json: serde_json::Value) -> (serde_json::Value, bool) { pub fn migrate_json(mut json: serde_json::Value) -> (serde_json::Value, bool) {
// Consider the config already migrated if: let is_migrated = json.get("version").and_then(|v| v.as_str()) == Some("0.3.1");
// 1. Version matches exactly, OR if is_migrated {
// 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": env!("CARGO_PKG_VERSION"), "version": "0.3.1",
}); });
// 1. Log level // 1. Log level

View File

@ -1,41 +0,0 @@
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,4 +9,3 @@ pub mod tunnel;
pub mod runner; pub mod runner;
pub mod logging; pub mod logging;
pub mod ipc_crypto;

View File

@ -74,20 +74,16 @@ 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(false) .with_line_number(true)
.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()
@ -111,7 +107,6 @@ 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,7 +1,8 @@
use anyhow::Result; use anyhow::Result;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::watch; use tokio::sync::{mpsc, 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;
@ -9,11 +10,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<()> {
tracing::info!("starting client core"); println!("[ostp] Starting run_client_core with multi-server architecture");
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 +1,103 @@
// Left empty by request use std::sync::Arc;
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,3 +1,4 @@
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;
@ -9,10 +10,9 @@ 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>>>,
}, },
Dnstt { Dns {
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::Dnstt { tx, .. } => { Self::Uot { tx, .. } | Self::Dns { 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,40 +30,31 @@ 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::Dnstt { .. } => self.send(frame).await, Self::Uot { .. } | Self::Dns { .. } => 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::Dnstt { rx, .. } => { Self::Uot { rx, .. } | Self::Dns { rx, .. } => {
let mut rx = rx.lock().await; let mut rx = rx.lock().await;
if let Some(frame) = rx.recv().await { match rx.recv().await {
let len = frame.len().min(buf.len()); Some(bytes) => {
buf[..len].copy_from_slice(&frame[..len]); let len = bytes.len().min(buf.len());
Ok(len) buf[..len].copy_from_slice(&bytes[..len]);
} else { Ok(len)
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::Dnstt { .. } => Ok("0.0.0.0:0".parse().unwrap()), Self::Uot { .. } | Self::Dns { .. } => Ok("0.0.0.0:0".parse().unwrap()),
} }
} }
} }

View File

@ -1,5 +1,6 @@
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>,
@ -59,7 +60,6 @@ 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,20 +14,13 @@ 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, set_system_proxy } = inbound_config else { let InboundConfig::LocalProxy { tag, protocol, listen, port } = 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 {
@ -92,7 +85,7 @@ async fn handle_socks5_connection(
} }
let atyp = buf[3]; let atyp = buf[3];
let (target_host, ip_addr) = match atyp { let (target_host, mut 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,7 +1,6 @@
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;
@ -14,7 +13,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};
@ -73,7 +72,7 @@ pub async fn run_tun_inbound(
#[allow(unused_variables)] #[allow(unused_variables)]
let mut _route_guard = None; let mut _route_guard = None;
let (tun_to_stack, stack_to_tun) = { let (mut tun_to_stack, mut stack_to_tun) = {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
{ {
if let Some(fd) = fd { if let Some(fd) = fd {
@ -184,7 +183,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 tcp_accept_task = tokio::spawn(async move { let mut 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();
@ -250,7 +249,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 udp_proxy_task = tokio::spawn(async move { let mut 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<
}; };
#[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,5 +1,6 @@
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;
@ -11,7 +12,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 {
@ -23,7 +24,7 @@ impl OutboundManager {
Self { Self {
balancer, balancer,
phys_if_index, phys_if_index,
_phys_if_name: phys_if_name, phys_if_name,
} }
} }
@ -39,7 +40,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(target_host, target_port, server, *port, access_key, transport, multiplex).await ostp::dial_tcp(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,215 +1,76 @@
use anyhow::Result; use anyhow::{anyhow, Result};
use tokio::net::TcpStream; use tokio::net::TcpStream;
use crate::config::{TransportConfig, MultiplexConfig}; use crate::config::{TransportConfig, MultiplexConfig};
use ostp_core::{OstpEvent, ProtocolAction, ProtocolConfig, ProtocolMachine}; use ostp_core::{NoiseRole, 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 = make_transport(transport_cfg, server, port).await?; let transport = match transport_cfg.r#type.as_str() {
"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();
let target_host_str = target_host.to_string(); // Spawn bridge task
tokio::spawn(async move {
let server_str = server.to_string(); if let Ok(action) = machine.on_event(OstpEvent::Start) {
// 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! {
// Only read from the application TCP stream when cwnd allows Ok(n) = server_stream.read(&mut buf) => {
Ok(n) = server_stream.read(&mut buf), if can_send => {
if n == 0 { break; } if n == 0 { break; }
let data_msg = ostp_core::relay::RelayMessage::Data(buf[..n].to_vec()); if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::copy_from_slice(&buf[..n]))) {
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;
} }
} }
@ -218,7 +79,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(tick_ms)) => { _ = tokio::time::sleep(std::time::Duration::from_millis(10)) => {
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;
} }
@ -227,7 +88,6 @@ pub async fn dial_tcp(
} }
}); });
Ok(client_stream) Ok(client_stream)
} }
@ -241,55 +101,64 @@ pub async fn handle_udp(
transport_cfg: &TransportConfig, transport_cfg: &TransportConfig,
_multiplex: &MultiplexConfig, _multiplex: &MultiplexConfig,
) -> Result<()> { ) -> Result<()> {
let transport = make_transport(transport_cfg, server, port).await?; let transport = match transport_cfg.r#type.as_str() {
"dns" => {
// Derive session_id from client source addr for stable per-flow sessions let domain = transport_cfg.domain.clone().unwrap_or_else(|| "tunnel.example.com".to_string());
let ip_bytes = match client_src.ip() { let resolver = transport_cfg.resolver.clone().unwrap_or_else(|| "8.8.8.8".to_string());
std::net::IpAddr::V4(v4) => { crate::transport::dns::start_dns_transport(domain, resolver, transport_cfg.pubkey.clone()).await?
let o = v4.octets();
u32::from_be_bytes(o)
} }
std::net::IpAddr::V6(v6) => { _ => {
let o = v6.octets(); let udp = tokio::net::UdpSocket::bind("0.0.0.0:0").await?;
u32::from_be_bytes([o[12], o[13], o[14], o[15]]) udp.connect((server, port)).await?;
crate::transport::Transport::Udp(std::sync::Arc::new(udp))
} }
}; };
let session_id = ip_bytes ^ (client_src.port() as u32);
let config = make_initiator_config(session_id, access_key, transport_cfg); 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: 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 handshake first // Send initial packet with UDP payload
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;
} }
// Wait for handshake response (server sends HandshakePayload back) // Send the actual UDP payload
let mut buf = [0u8; 8192]; let relay_msg = ostp_core::relay::RelayMessage::Connect(format!("{}:{}", target_dst.ip(), target_dst.port()));
match tokio::time::timeout( let encoded = relay_msg.encode();
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;
} }
let data_msg = ostp_core::relay::RelayMessage::UdpData( // Send data packet
format!("{}:{}", target_dst.ip(), target_dst.port()), let data_msg = ostp_core::relay::RelayMessage::Data(payload.to_vec());
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;
@ -297,15 +166,13 @@ 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)) => {
if let Ok(action) = machine.on_event(OstpEvent::Inbound(bytes::Bytes::copy_from_slice(&buf[..n]))) { let _ = machine.on_event(OstpEvent::Inbound(bytes::Bytes::copy_from_slice(&buf[..n])));
// Just process incoming UDP response internally
let _ = action;
}
} }
_ => break, _ => break,
} }
@ -314,76 +181,6 @@ 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) => {
@ -406,53 +203,17 @@ 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) => {
if let Ok(msg) = ostp_core::relay::RelayMessage::decode(&payload) { let _ = server_stream.write_all(&payload).await;
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 {
Box::pin(handle_action(a, transport, server_stream)).await; match a {
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,6 +126,7 @@ 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> {
@ -145,11 +146,12 @@ pub fn get_process_name_from_port(port: u16) -> Option<String> {
None None
}; };
let target_inode = check_net_file("/proc/net/tcp") 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,5 +17,3 @@ 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,12 +4,6 @@
//! 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};
@ -21,14 +15,8 @@ pub struct CongestionController {
ssthresh: u64, ssthresh: u64,
/// Current phase /// Current phase
phase: Phase, phase: Phase,
/// Minimum RTT observed (for BBR-style bandwidth estimation) /// Minimum RTT observed
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)
@ -49,43 +37,31 @@ pub struct CongestionController {
enum Phase { enum Phase {
/// Exponential growth until loss or ssthresh /// Exponential growth until loss or ssthresh
SlowStart, SlowStart,
/// Probe bandwidth: additive increase /// Probe bandwidth: cycle through pacing gains
ProbeBandwidth, ProbeBandwidth,
} }
/// Initial congestion window: 32 packets × MTU (IW10 is too conservative for modern links) /// Initial congestion window: 10 packets × MTU
const INITIAL_CWND_PACKETS: u64 = 32; const INITIAL_CWND_PACKETS: u64 = 10;
/// 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: INITIAL_RTT, min_rtt: Duration::from_millis(100), // Conservative initial estimate
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_pacing, pacing_rate: initial_cwnd * 10, // initial: ~10 windows/sec
mtu, mtu,
min_rtt_stamp: now, min_rtt_stamp: now,
} }
@ -106,20 +82,9 @@ impl CongestionController {
self.pacing_rate self.pacing_rate
} }
/// Returns the smoothed RTT estimate (SRTT). /// Returns the smoothed RTT estimate.
pub fn smoothed_rtt(&self) -> Duration { pub fn smoothed_rtt(&self) -> Duration {
self.srtt self.min_rtt
}
/// 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.
@ -150,13 +115,16 @@ 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 measurements // Update RTT
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 (doubles per RTT) // Exponential growth: increase cwnd by acked bytes
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;
@ -196,49 +164,32 @@ 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) {
// Update windowed minimum RTT (for pacing) // Track windowed minimum RTT
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;
} }
// Update SRTT and RTTVAR per RFC 6298
if !self.rtt_initialized {
// First measurement: initialize directly
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_bandwidth(&mut self, _acked_bytes: u64, now: Instant) {
let elapsed = now.duration_since(self.last_ack_time);
if elapsed.as_micros() > 0 {
// Removed bw_samples tracking
}
}
fn update_pacing_rate(&mut self) { fn update_pacing_rate(&mut self) {
// Pacing rate = cwnd / min_rtt (delivery rate target) // Pacing rate = cwnd / min_rtt (with gain)
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;
} }
@ -251,18 +202,19 @@ 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(), 32 * 1200); // 32 * 1200 assert_eq!(cc.cwnd(), 12000); // 10 * 1200
assert!(cc.can_send()); assert!(cc.can_send());
assert_eq!(cc.cwnd_packets(), 32); assert_eq!(cc.cwnd_packets(), 10);
} }
#[test] #[test]
fn test_slow_start_growth() { fn test_slow_start_growth() {
let mut cc = CongestionController::new(1200); let mut cc = CongestionController::new(1200);
let initial = cc.cwnd(); // Simulate sending and ACKing
cc.on_send(1200); cc.on_send(1200);
cc.on_ack(1200, Duration::from_millis(50)); cc.on_ack(1200, Duration::from_millis(50));
assert!(cc.cwnd() > initial); // cwnd should grow
assert!(cc.cwnd() > 12000);
} }
#[test] #[test]
@ -277,7 +229,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..32 { for _ in 0..10 {
cc.on_send(1200); cc.on_send(1200);
} }
assert!(!cc.can_send()); // cwnd exhausted assert!(!cc.can_send()); // cwnd exhausted
@ -292,46 +244,10 @@ mod tests {
} }
#[test] #[test]
fn test_rtt_tracking_first_sample() { fn test_rtt_tracking() {
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}; use std::io::{Cursor, Read, Write};
const BASE32_ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz234567"; const BASE32_ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz234567";
@ -136,17 +136,13 @@ impl DnsPacket {
qtype: rtype.clone(), qtype: rtype.clone(),
qclass: 1, // IN qclass: 1, // IN
}], }],
answers: if rdata.is_empty() { answers: vec![DnsAnswer {
vec![] name: name.to_string(),
} else { rtype,
vec![DnsAnswer { rclass: 1,
name: name.to_string(), ttl: 0, // No caching
rtype, rdata,
rclass: 1, }],
ttl: 0, // No caching
rdata,
}]
},
} }
} }

View File

@ -1,94 +0,0 @@
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,8 +5,6 @@ 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; use std::collections::{BTreeMap, VecDeque};
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: BTreeMap<u64, SentFrame>, sent_history: VecDeque<SentFrame>,
session_id: u32, session_id: u32,
handshake_payload: Vec<u8>, handshake_payload: Vec<u8>,
padder: AdaptivePadder, padder: AdaptivePadder,
@ -83,8 +83,7 @@ pub struct ProtocolMachine {
max_reorder: u64, max_reorder: u64,
max_reorder_buffer: usize, max_reorder_buffer: usize,
ack_delay: Duration, ack_delay: Duration,
/// Initial/fallback RTO from config (overridden by cc.rto() after first RTT sample) rto: Duration,
rto_initial: Duration,
max_retries: u8, max_retries: u8,
max_sent_history: usize, max_sent_history: usize,
ack_pending: bool, ack_pending: bool,
@ -101,11 +100,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,
@ -129,7 +128,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: BTreeMap::new(), sent_history: VecDeque::with_capacity(config.max_sent_history.max(1)),
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),
@ -137,7 +136,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_initial: Duration::from_millis(config.rto_ms.max(1)), rto: 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,
@ -147,25 +146,20 @@ 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.values().filter(|f| f.is_retransmittable).count() self.sent_history.iter().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);
} }
@ -213,12 +207,13 @@ impl ProtocolMachine {
.map(ProtocolAction::SendDatagram) .map(ProtocolAction::SendDatagram)
} }
(OstpState::Closing, OstpEvent::Inbound(raw)) => { (OstpState::Closing, OstpEvent::Inbound(raw)) => {
// The remote may still have data or ACKs in transit. // Process final in-flight packets to prevent data loss during teardown.
// handle_inbound transitions to Closed when it receives a Close frame. // The remote may still have data or ACKs in transit when we initiated Close.
self.handle_inbound(raw) let result = 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;
@ -397,26 +392,18 @@ 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 {
tracing::warn!("Reorder buffer full ({}/{}), dropping new frame nonce={} to wait for recovery of nonce={}", self.reorder_buffer.insert(nonce, action);
self.reorder_buffer.len(), self.max_reorder_buffer, nonce, self.expected_recv_nonce } else {
tracing::warn!("Reorder buffer full ({}/{}), dropping frame nonce={}",
self.reorder_buffer.len(), self.max_reorder_buffer, nonce
); );
} }
if nonce >= self.expected_recv_nonce { // Rate-limited NACK: send at most once per 30ms to prevent retransmit storms.
if self.reorder_buffer.len() < self.max_reorder_buffer { // Under high load with natural UDP reordering, sending a NACK per packet
self.reorder_buffer.insert(nonce, action); // causes exponential retransmit explosion that saturates the channel.
} else { let nack_cooldown = Duration::from_millis(30);
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();
@ -524,39 +511,62 @@ 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();
// Use the adaptive RTO from the congestion controller (RFC 6298 SRTT + 4*RTTVAR). let base_rto_ms = self.rto.as_millis().max(1) as u64;
// 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 ────────────────────────────────
// Backoff starts from retry #0 (immediately effective): // Limit retransmits per tick to prevent bandwidth saturation
// 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.values_mut() { for frame in self.sent_history.iter_mut() {
if !frame.is_retransmittable { if !frame.is_retransmittable {
continue; continue;
} }
let backoff_factor = 1u64 << (frame.retries as u64).min(6); let retry_over = frame.retries.saturating_sub(self.max_retries);
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 {
@ -662,7 +672,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.get_mut(&nonce) { if let Some(frame) = self.sent_history.iter_mut().rev().find(|f| f.nonce == 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());
@ -674,7 +684,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.insert(nonce, SentFrame { self.sent_history.push_back(SentFrame {
nonce, nonce,
bytes, bytes,
last_sent: Instant::now(), last_sent: Instant::now(),
@ -687,7 +697,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_first(); self.sent_history.pop_front();
} }
} }
} }
@ -698,8 +708,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 (&nonce, frame) in &self.sent_history { for frame in self.sent_history.iter() {
if nonce_in_ranges(nonce, ranges) { if nonce_in_ranges(frame.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 {
@ -708,7 +718,7 @@ impl ProtocolMachine {
} }
} }
self.sent_history.retain(|&nonce, _| !nonce_in_ranges(nonce, ranges)); self.sent_history.retain(|frame| !nonce_in_ranges(frame.nonce, ranges));
// Notify congestion controller // Notify congestion controller
if acked_bytes > 0 { if acked_bytes > 0 {

View File

@ -95,15 +95,6 @@ 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,8 +50,4 @@ 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

Before

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(0xFF030303), scaffoldBackgroundColor: const Color(0xFF08080F),
colorScheme: const ColorScheme.dark( colorScheme: const ColorScheme.dark(
primary: Color(0xFFF9FAFB), primary: Color(0xFF6C72FF),
secondary: Color(0xFF10B981), secondary: Color(0xFF22D3A5),
surface: Color(0xFF09090B), surface: Color(0xFF151522),
), ),
fontFamily: 'Inter', fontFamily: 'Inter',
useMaterial3: true, useMaterial3: true,

View File

@ -6,7 +6,6 @@ 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';
@ -518,16 +517,31 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
return Scaffold( return Scaffold(
body: Stack( body: Stack(
children: [ children: [
Center( Positioned(
child: Opacity( top: -150, right: -100,
opacity: theme.brightness == Brightness.dark ? 0.05 : 0.06, child: Container(
child: SvgPicture.asset( width: 400, height: 400,
'assets/logo.svg', decoration: BoxDecoration(
width: MediaQuery.of(context).size.width * 0.8, shape: BoxShape.circle,
fit: BoxFit.contain, color: theme.colorScheme.primary.withOpacity(0.15),
colorFilter: theme.brightness == Brightness.light ),
? const ColorFilter.mode(Colors.black, BlendMode.srcIn) child: BackdropFilter(
: null, filter: ImageFilter.blur(sigmaX: 100, sigmaY: 100),
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;
late TextEditingController _dnsRegionCtrl; String _dnsRegion = 'Global';
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') ?? '');
_dnsRegionCtrl = TextEditingController(text: widget.prefs.getString('dns_region') ?? '1.1.1.1'); _pbkCtrl = TextEditingController(text: widget.prefs.getString('pbk') ?? '');
_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,7 +81,6 @@ 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();
@ -98,11 +97,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('dns_region', _dnsRegionCtrl.text.trim()); widget.prefs.setString('pbk', _pbkCtrl.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());
@ -238,7 +237,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'] ?? '';
_dnsRegionCtrl.text = uri.queryParameters['resolver'] ?? '1.1.1.1'; _dnsRegion = uri.queryParameters['region'] ?? 'Global';
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');
@ -350,27 +349,30 @@ 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),
Row( DropdownButtonFormField<String>(
children: [ value: _dnsRegion,
Expanded( dropdownColor: const Color(0xFF1E1E2C),
child: _buildTextField('DNS Resolver Server', _dnsRegionCtrl, hint: '1.1.1.1'), style: const TextStyle(color: Colors.white, fontSize: 14),
), decoration: InputDecoration(
const SizedBox(width: 8), labelText: 'DNS Resolver Region',
Padding( labelStyle: const TextStyle(color: Colors.white54, fontSize: 13),
padding: const EdgeInsets.only(top: 24.0), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
child: ElevatedButton( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
onPressed: _showDnsProberDialog, ),
style: ElevatedButton.styleFrom( items: ['Global', 'Russia', 'China', 'Iran'].map((String region) {
backgroundColor: Colors.orangeAccent.withOpacity(0.2), return DropdownMenuItem<String>(
foregroundColor: Colors.orangeAccent, value: region,
elevation: 0, child: Text(region),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), );
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), }).toList(),
), onChanged: (String? newValue) {
child: const Text('PROBER', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)), if (newValue != null) {
), setState(() {
) _dnsRegion = newValue;
], _saveSettings();
});
}
},
), ),
], ],
), ),
@ -527,9 +529,8 @@ 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())}');
} }
final resolver = _dnsRegionCtrl.text.trim(); if (_dnsRegion != 'Global') {
if (resolver.isNotEmpty && resolver != '1.1.1.1') { queryParams.add('region=${Uri.encodeComponent(_dnsRegion)}');
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())}');
@ -602,97 +603,6 @@ 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,14 +134,6 @@ 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
@ -280,14 +272,6 @@ 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:
@ -597,30 +581,6 @@ 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.12+25 version: 0.3.7+20
environment: environment:
sdk: ^3.11.4 sdk: ^3.11.4
@ -34,7 +34,6 @@ 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
@ -73,8 +72,9 @@ 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:
- assets/logo.svg # - images/a_dot_burr.jpeg
# - 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.3.12" version = "0.2.98"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.22.1", "base64 0.22.1",
@ -2700,20 +2700,17 @@ dependencies = [
[[package]] [[package]]
name = "ostp-core" name = "ostp-core"
version = "0.3.12" version = "0.2.98"
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",
] ]
@ -2723,16 +2720,12 @@ 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",
@ -2742,7 +2735,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-tun" name = "ostp-tun"
version = "0.3.12" version = "0.2.98"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"libc", "libc",

View File

@ -26,7 +26,6 @@ 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: 9.4 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 955 B

After

Width:  |  Height:  |  Size: 710 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 4.4 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: 8.7 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -6,15 +6,11 @@ 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

@ -1,6 +0,0 @@
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,3 +1,41 @@
// Re-export the shared IPC crypto from ostp-client so that GUI and tun-helper use anyhow::{anyhow, Result};
// always use identical encrypt/decrypt logic. use chacha20poly1305::{ChaCha20Poly1305, Nonce};
pub use ostp_client::ipc_crypto::{derive_key, IpcCrypto}; use chacha20poly1305::aead::{Aead, KeyInit};
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,7 +8,6 @@ use portable_atomic::Ordering;
use tauri::Emitter; use tauri::Emitter;
mod ipc_crypto; mod ipc_crypto;
mod dns_prober;
// ── Config types ───────────────────────────────────────────────────────────── // ── Config types ─────────────────────────────────────────────────────────────
@ -40,7 +39,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 { #[allow(dead_code)] message: String }, Log { 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 },
} }
@ -59,7 +58,6 @@ 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,
} }
@ -780,7 +778,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 if !cfg!(debug_assertions) { } else {
show_error_dialog("Приложение OSTP GUI уже запущено!"); show_error_dialog("Приложение OSTP GUI уже запущено!");
return; return;
} }
@ -876,7 +874,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, dns_prober::run_dns_prober]) .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])
.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.12", "version": "0.3.7",
"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

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@ -12,9 +12,10 @@
<body> <body>
<div class="app-root"> <div class="app-root">
<!-- Eagle Watermark --> <!-- Ambient light blobs -->
<div class="watermark" aria-hidden="true"> <div class="ambient" aria-hidden="true">
<img src="assets/logo.svg" alt="" /> <div class="blob blob-1"></div>
<div class="blob blob-2"></div>
</div> </div>
<!-- ── HOME SCREEN ──────────────────────────────────────────── --> <!-- ── HOME SCREEN ──────────────────────────────────────────── -->
@ -201,20 +202,24 @@
</select> </select>
</div> </div>
<div id="group-dns-proxy" style="display: none; flex-direction: column; gap: 14px;"> <div id="group-dns-proxy" style="display: none;">
<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 Server</label> <label class="field-label" for="in-dns-region" data-i18n="label_dns_region">DNS Resolver Region (Prober)</label>
<div class="input-wrap"> <select id="in-dns-region" class="field-input">
<input id="in-dns-region" class="field-input" type="text" placeholder="1.1.1.1" spellcheck="false" /> <option value="Global" data-i18n="opt_global">Global (Cloudflare, Google, 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="Russia" data-i18n="opt_russia">Russia (Yandex, VK, etc)</option>
PROBER <option value="China" data-i18n="opt_china">China (AliDNS, DNSPod, etc)</option>
</button> <option value="Iran" data-i18n="opt_iran">Iran (Shatel, Electro, etc)</option>
</div> </select>
</div> </div>
</div> </div>
@ -375,23 +380,6 @@
</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