feat: migrate to v0.3.1 with multi-server architecture

This commit is contained in:
ospab 2026-06-16 20:22:00 +03:00
parent 8ed66f9553
commit 67f9c06935
18 changed files with 748 additions and 423 deletions

1
Cargo.lock generated
View File

@ -1416,6 +1416,7 @@ dependencies = [
"futures-util", "futures-util",
"hex", "hex",
"hmac", "hmac",
"ipnet",
"json_comments", "json_comments",
"libc", "libc",
"netstack-smoltcp", "netstack-smoltcp",

View File

@ -5,8 +5,8 @@ members = [
"ostp-server", "ostp-server",
"ostp-jni", "ostp", "ostp-jni", "ostp",
"ostp-tun-helper" "ostp-tun-helper"
, "ostp-tun"] ]
exclude = ["ostp-gui/src-tauri", "ostp-brain", "ostp-prober"] exclude = ["ostp-gui/src-tauri", "ostp-brain", "ostp-prober", "ostp-sandbox"]
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]

View File

@ -5,10 +5,20 @@ The Obfuscated Secure Transport Protocol (OSTP) is a high-performance, asynchron
--- ---
## Kerckhoffs's Principle and DPI Resilience
The OSTP architecture strictly adheres to **Kerckhoffs's Principle**: a cryptosystem should be secure even if everything about the system, except the key, is public knowledge.
All encryption and obfuscation algorithms are fully open source. The security and indistinguishability of the traffic rely entirely on the secrecy of the pre-shared key (`access_key` / PSK).
Through cryptographic transformations using this key (Noise Protocol + ChaCha20Poly1305 + Blake2s) and adaptive padding, every transmitted packet is visually indistinguishable from completely random white noise.
The protocol lacks any static headers or plaintext handshakes. This makes it impossible for Deep Packet Inspection (DPI) systems, such as state censors, to create a static filter or signature to block OSTP within minutes. Blocking the protocol would require either blocking all unknown UDP traffic globally (which breaks many legitimate services) or possessing the secret key.
---
## Workspace Structure ## Workspace Structure
The project is modularized into the following crates: The project is modularized into the following crates:
1. **ostp-core**: The core engine. Contains protocol state machines, Noise Protocol Framework handshakes, data framing serialization, dynamic obfuscation algorithms, and reliable packet delivery (ARQ). 1. **ostp-core**: The core engine. Contains protocol state machines, Noise Protocol Framework handshakes, data framing serialization, dynamic obfuscation algorithms, and reliable packet delivery (ARQ).
2. **ostp-client**: The client daemon. Manages local traffic interception via dual-mode SOCKS5/HTTP proxies or virtualized network adapters (TUN/Wintun), multiplexing active host streams into a single UDP tunnel, and interfacing with TURN servers. 2. **ostp-client**: The client daemon. Manages routing configuration via arrays of `inbounds` (e.g., SOCKS5, TUN) and `outbounds` (e.g., OSTP, direct, block), handling multiplexing of streams and interacting with TURN servers.
3. **ostp-server**: The high-concurrency connection dispatcher, responsible for demultiplexing data from multiple sessions, handling seamless IP roaming, and forwarding traffic to the broader internet. 3. **ostp-server**: The high-concurrency connection dispatcher, responsible for demultiplexing data from multiple sessions, handling seamless IP roaming, and forwarding traffic to the broader internet.
4. **ostp-obfuscator**: Utility crate for static traffic shaping and dynamic obfuscation key derivation tools. 4. **ostp-obfuscator**: Utility crate for static traffic shaping and dynamic obfuscation key derivation tools.
5. **ostp-jni**: Android JNI bindings that allow embedding OSTP inside mobile applications via an isolated runtime. 5. **ostp-jni**: Android JNI bindings that allow embedding OSTP inside mobile applications via an isolated runtime.

View File

@ -46,16 +46,29 @@ The client is engineered to maintain persistence without requiring user interven
--- ---
## Routing Exclusions (Bypass Mode) ## Modular Routing Architecture (Inbounds / Outbounds)
To minimize latency and overhead for trusted resources, the OSTP client incorporates an integrated direct-routing bypass engine. This is configured inside the `"exclude"` block of the `config.json` file: Starting from version `0.3.1`, the OSTP client utilizes a modular configuration architecture based on inbound and outbound arrays, similar to Xray or Sing-box.
- **`domains`**: A list of domain suffixes (e.g., `["trusted-site.com", "local.lan"]`). Traffic bound for these domains is instantly channeled via the default local gateway, bypassing encryption entirely. - **`inbounds`**: Defines how local traffic enters the client. Supported types include `tun` (virtual network interface) and `local_proxy` (SOCKS5/HTTP proxy).
- **`ips`**: A list of target subnet destinations in CIDR format (e.g., `["192.168.1.0/24", "10.0.0.0/8"]`), ensuring local area networks maintain full wire-speed throughput. - **`outbounds`**: Defines where the client sends the traffic. The main type is `ostp` (encapsulation and transmission to the server), but it also supports `direct` (bypassing the VPN to connect directly to the internet) and `block` (dropping traffic).
- **`processes`**: A list of OS executable filenames (e.g., `["discord.exe", "steam.exe"]`). Applications specified here will automatically evade the VPN's virtual network driver. - **`routing`**: The mechanism replacing the legacy `exclude` block. It allows for flexible traffic routing based on advanced rules.
Routing rule example in `config.json`:
```json
"routing": {
"rules": [
{
"domain_suffix": ["trusted-site.com", "local.lan"],
"outbound": "direct"
}
],
"default_outbound": "proxy"
}
```
> [!NOTE] > [!NOTE]
> The exclusion/bypass logic is fully operational, rigorously optimized, and ready for immediate production deployment. > This architecture enables the client to connect to multiple OSTP servers simultaneously, split traffic by domain, or block telemetry directly at the VPN routing level.
--- ---

103
docs/en/migration-v0.3.1.md Normal file
View File

@ -0,0 +1,103 @@
# OSTP Configuration Migration to v0.3.1
The OSTP `config.json` schema has been significantly redesigned in version `v0.3.1` to support a modern multi-server architecture. The new schema provides greater flexibility by splitting configuration into `inbounds`, `outbounds`, and flexible `routing` rules, replacing the monolithic architecture of previous versions.
## Automatic Migration
The OSTP core and GUI clients are equipped with an automatic migrator. When launching OSTP `v0.3.1` with a `config.json` from a previous version, the migrator will automatically transform the legacy schema into the new `v0.3.1` schema.
The migrated file will be overwritten with the new format and will begin with:
```json
// OSTP Configuration v0.3.1
// DO NOT EDIT THIS COMMENT - Migrator relies on it
{
"version": "0.3.1",
"mode": "client",
...
}
```
## Manual Schema Reference
If you prefer to configure manually, the following is a reference of the new modular configuration format:
### Legacy Configuration (v0.2.x)
```json
{
"mode": "client",
"server": "192.168.1.100:50000",
"access_key": "mysecretkey",
"socks5_bind": "127.0.0.1:1088",
"tun": {
"enable": true,
"kill_switch": true
},
"exclude": {
"domains": ["localhost"],
"ips": ["192.168.1.0/24"]
}
}
```
### New Configuration (v0.3.1)
```json
{
"version": "0.3.1",
"mode": "client",
"log": {
"level": "info"
},
"inbounds": [
{
"type": "tun",
"tag": "tun-in",
"auto_route": true,
"mtu": 1140
},
{
"type": "socks",
"tag": "socks-in",
"bind_addr": "127.0.0.1:1088"
}
],
"outbounds": [
{
"type": "ostp",
"tag": "proxy",
"server": "192.168.1.100",
"port": 50000,
"access_key": "mysecretkey",
"transport": {
"type": "udp"
},
"multiplex": {
"enabled": false
}
},
{
"type": "direct",
"tag": "direct"
},
{
"type": "block",
"tag": "block"
}
],
"routing": {
"rules": [
{
"domain_suffix": ["localhost"],
"ip_cidr": ["192.168.1.0/24"],
"outbound": "direct"
}
],
"default_outbound": "proxy"
}
}
```
### Key Changes
- **Outbounds List**: Multiple proxy servers can now be defined.
- **Inbounds List**: TUN and SOCKS5 are now independent listeners.
- **Routing**: Fine-grained traffic routing between inbounds and outbounds based on domains, IPs, and processes.
- **Comments**: The GUI and migrator now use JS-style `//` comments in `config.json` instead of the legacy `"_comment"` JSON keys.

View File

@ -5,10 +5,20 @@ Obfuscated Secure Transport Protocol (OSTP) — это высокопроизв
--- ---
## Принцип Керкгоффса и устойчивость к DPI (Kerckhoffs's Principle)
Архитектура OSTP строго следует **Принципу Керкгоффса**: система должна оставаться безопасной, даже если все о ней, кроме ключа, является общедоступным знанием.
Весь исходный код алгоритмов шифрования и обфускации полностью открыт. Безопасность и нераспознаваемость трафика базируются исключительно на секретности предварительно согласованного ключа (`access_key` / PSK).
Благодаря криптографическим преобразованиям с использованием этого ключа (Noise Protocol + ChaCha20Poly1305 + Blake2s) и адаптивному паддингу, каждый пакет передаваемых данных визуально неотличим от абсолютно случайного белого шума.
Протокол не имеет статических заголовков или "рукопожатий" в открытом виде. Это делает невозможным для систем глубокого анализа трафика (DPI), таких как ТСПУ Роскомнадзора, создать статический фильтр или сигнатуру для блокировки OSTP за считанные минуты. Блокировка протокола потребовала бы либо полной блокировки всего неизвестного UDP-трафика (что нарушает работу многих легитимных сервисов), либо знания секретного ключа.
---
## Структура проекта ## Структура проекта
Проект состоит из следующих специализированных модулей (crates): Проект состоит из следующих специализированных модулей (crates):
1. **ostp-core**: Основа протокола. Содержит конечные автоматы состояний, реализацию рукопожатия (Noise Protocol Framework), механизмы сериализации кадров (framing), алгоритмы обфускации и логику надежной доставки пакетов (ARQ). 1. **ostp-core**: Основа протокола. Содержит конечные автоматы состояний, реализацию рукопожатия (Noise Protocol Framework), механизмы сериализации кадров (framing), алгоритмы обфускации и логику надежной доставки пакетов (ARQ).
2. **ostp-client**: Клиентский демон, управляющий перехватом трафика хоста через двухрежимный SOCKS5/HTTP-прокси или виртуальные адаптеры (TUN/Wintun), мультиплексированием потоков в единый UDP-туннель и взаимодействием с TURN для обхода NAT. 2. **ostp-client**: Клиентский демон. Управляет конфигурацией маршрутизации через массивы входящих (`inbounds`, например, SOCKS5, TUN) и исходящих (`outbounds`, например, OSTP, direct) соединений. Выполняет мультиплексирование потоков и взаимодействие с TURN для обхода NAT.
3. **ostp-server**: Высоконагруженный диспетчер соединений, отвечающий за демультиплексирование данных от множества сессий, прозрачный роуминг адресов и проксирование трафика в интернет. 3. **ostp-server**: Высоконагруженный диспетчер соединений, отвечающий за демультиплексирование данных от множества сессий, прозрачный роуминг адресов и проксирование трафика в интернет.
4. **ostp-obfuscator**: Утилиты для статического шейпинга трафика и генерации динамических ключей маскировки. 4. **ostp-obfuscator**: Утилиты для статического шейпинга трафика и генерации динамических ключей маскировки.
5. **ostp-jni**: Нативный SDK для интеграции в мобильные платформы Android. 5. **ostp-jni**: Нативный SDK для интеграции в мобильные платформы Android.

View File

@ -46,16 +46,29 @@
--- ---
## Маршрутизация исключений (Bypass / Exclusions) ## Модульная архитектура маршрутизации (Inbounds / Outbounds)
Для снижения задержек и оптимизации трафика клиент OSTP поддерживает механизм прямых подключений в обход туннеля. Настройка производится в блоке `"exclude"` конфигурационного файла `config.json`: Начиная с версии `0.3.1`, клиент OSTP использует модульную архитектуру конфигурации на базе массивов точек входа и выхода, аналогичную Xray или Sing-box.
- **`domains`**: Список доменных имен (например, `["trusted-site.com", "yandex.ru"]`). Любой запрос к этим доменам или их поддоменам направляется напрямую через системный шлюз провайдера. - **`inbounds` (Входящие точки)**: Определяет, как локальный трафик попадает в клиент. Поддерживаются типы `tun` (создание виртуального интерфейса) и `local_proxy` (SOCKS5/HTTP прокси).
- **`ips`**: Список диапазонов IP-адресов в формате CIDR (например, `["192.168.1.0/24", "10.0.0.0/8"]`). Полезно для доступа к ресурсам локальной сети. - **`outbounds` (Исходящие точки)**: Определяет, куда клиент отправляет трафик. Основной тип — `ostp` (инкапсуляция и отправка на сервер), но также поддерживаются `direct` (прямое подключение к интернету в обход VPN) и `block` (блокировка трафика).
- **`processes`**: Список имен исполняемых файлов процессов ОС (например, `["discord.exe", "steam.exe"]`), чьи сетевые запросы должны игнорировать VPN. - **`routing` (Правила маршрутизации)**: Механизм, заменяющий старый блок `exclude`. Позволяет гибко перенаправлять трафик.
Пример правила маршрутизации в `config.json`:
```json
"routing": {
"rules": [
{
"domain_suffix": ["trusted-site.com", "local.lan"],
"outbound": "direct"
}
],
"default_outbound": "proxy"
}
```
> [!NOTE] > [!NOTE]
> Механизм исключений полностью отлажен и готов к промышленной эксплуатации, обеспечивая нулевые задержки для доверенных ресурсов. > Такая архитектура позволяет подключать клиента сразу к нескольким серверам OSTP, разделять трафик по доменам или блокировать телеметрию на уровне роутера VPN.
--- ---

103
docs/ru/migration-v0.3.1.md Normal file
View File

@ -0,0 +1,103 @@
# Миграция конфигурации OSTP на версию 0.3.1
В версии `v0.3.1` формат `config.json` проекта OSTP был значительно переработан для поддержки современной архитектуры мульти-серверных подключений. Новый формат конфигурации обеспечивает большую гибкость: теперь он разделен на входящие подключения (`inbounds`), исходящие подключения (`outbounds`) и гибкие правила маршрутизации (`routing`), заменяя устаревшую монолитную структуру прошлых версий.
## Автоматическая миграция
Ядро OSTP и GUI клиенты оснащены автоматическим мигратором. При запуске OSTP `v0.3.1` с файлом `config.json` от предыдущей версии, мигратор автоматически преобразует старый формат в новый.
После успешной миграции файл будет перезаписан в новом формате, и его заголовок будет содержать комментарий:
```json
// OSTP Configuration v0.3.1
// DO NOT EDIT THIS COMMENT - Migrator relies on it
{
"version": "0.3.1",
"mode": "client",
...
}
```
## Справочник по новому формату
Если вы предпочитаете настраивать OSTP вручную, ниже приведено сравнение и примеры нового формата.
### Устаревшая конфигурация (v0.2.x)
```json
{
"mode": "client",
"server": "192.168.1.100:50000",
"access_key": "mysecretkey",
"socks5_bind": "127.0.0.1:1088",
"tun": {
"enable": true,
"kill_switch": true
},
"exclude": {
"domains": ["localhost"],
"ips": ["192.168.1.0/24"]
}
}
```
### Новая конфигурация (v0.3.1)
```json
{
"version": "0.3.1",
"mode": "client",
"log": {
"level": "info"
},
"inbounds": [
{
"type": "tun",
"tag": "tun-in",
"auto_route": true,
"mtu": 1140
},
{
"type": "socks",
"tag": "socks-in",
"bind_addr": "127.0.0.1:1088"
}
],
"outbounds": [
{
"type": "ostp",
"tag": "proxy",
"server": "192.168.1.100",
"port": 50000,
"access_key": "mysecretkey",
"transport": {
"type": "udp"
},
"multiplex": {
"enabled": false
}
},
{
"type": "direct",
"tag": "direct"
},
{
"type": "block",
"tag": "block"
}
],
"routing": {
"rules": [
{
"domain_suffix": ["localhost"],
"ip_cidr": ["192.168.1.0/24"],
"outbound": "direct"
}
],
"default_outbound": "proxy"
}
}
```
### Основные изменения
- **Outbounds (Исходящие)**: Теперь можно задать сразу несколько прокси-серверов.
- **Inbounds (Входящие)**: TUN и SOCKS5 выделены в отдельные независимые модули.
- **Routing (Маршрутизация)**: Точная маршрутизация трафика между входящими и исходящими узлами на основе доменов, IP-адресов и имен процессов.
- **Комментарии**: GUI и ядро теперь поддерживают JS-комментарии (с помощью `//`) в `config.json` вместо устаревших полей вида `"_comment"`.

View File

@ -32,4 +32,8 @@ libc = "0.2.186"
x25519-dalek = "2.0.1" x25519-dalek = "2.0.1"
chacha20poly1305.workspace = true chacha20poly1305.workspace = true
hex = "0.4.3" hex = "0.4.3"
winapi = { version = "0.3.9", features = ["iphlpapi", "tcpmib", "processthreadsapi", "psapi", "handleapi", "winerror", "minwindef", "winnt", "iptypes", "ws2def"] } winapi = { version = "0.3.9", features = ["iphlpapi", "tcpmib", "processthreadsapi", "psapi", "handleapi", "winerror", "minwindef", "winnt", "iptypes", "ws2def", "ws2tcpip", "winsock2"] }
ipnet = "2.12.0"
[target."cfg(unix)".dependencies]
libc = "0.2.186"

View File

@ -42,6 +42,8 @@ pub enum InboundConfig {
auto_route: bool, auto_route: bool,
#[serde(default = "default_mtu")] #[serde(default = "default_mtu")]
mtu: usize, mtu: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
fd: Option<i32>,
}, },
LocalProxy { LocalProxy {
tag: String, tag: String,
@ -163,7 +165,9 @@ impl ClientConfig {
if was_migrated { if was_migrated {
tracing::info!("Config was migrated to v0.3.1. Saving to {}", path.display()); tracing::info!("Config was migrated to v0.3.1. Saving to {}", path.display());
let serialized = serde_json::to_string_pretty(&migrated_json)?; let serialized = serde_json::to_string_pretty(&migrated_json)?;
std::fs::write(&path, serialized) 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()))?; .with_context(|| format!("failed to save migrated config to {}", path.display()))?;
} }

View File

@ -18,7 +18,7 @@ pub async fn run_tun_inbound(
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use futures::{StreamExt, SinkExt}; use futures::{StreamExt, SinkExt};
let InboundConfig::Tun { tag, auto_route, mtu } = inbound_config else { let InboundConfig::Tun { tag, auto_route, mtu, .. } = inbound_config else {
return Err(anyhow!("Invalid config for TUN inbound")); return Err(anyhow!("Invalid config for TUN inbound"));
}; };
@ -51,25 +51,6 @@ pub async fn run_tun_inbound(
} }
} }
let dummy_server_ip = bypass_ips.first().copied().unwrap_or_else(|| "8.8.8.8".parse().unwrap());
// Create TUN device
let opts = ostp_tun::OstpTunOptions {
server_ip: dummy_server_ip,
bypass_ips,
dns_server: None,
kill_switch: false,
mtu: mtu as u16,
wintun_path: None,
};
let tun_interface = ostp_tun::OstpTunInterface::create(opts)
.await
.map_err(|e| anyhow!("Failed to create OstpTunInterface: {}", e))?;
let dev = tun_interface.device;
let _route_guard = tun_interface.guard; // Drops when TUN drops
// Build smoltcp network stack // Build smoltcp network stack
let (stack, tcp_runner, udp_socket, tcp_listener) = StackBuilder::default() let (stack, tcp_runner, udp_socket, tcp_listener) = StackBuilder::default()
.stack_buffer_size(1024) .stack_buffer_size(1024)
@ -87,35 +68,115 @@ pub async fn run_tun_inbound(
}); });
let (mut stack_sink, mut stack_stream) = stack.split(); let (mut stack_sink, mut stack_stream) = stack.split();
let (mut tun_read, mut tun_write) = tokio::io::split(dev);
let mut tun_to_stack = tokio::spawn(async move { #[allow(unused_variables)]
let mut _route_guard = None;
let (mut tun_to_stack, mut stack_to_tun) = {
#[cfg(target_os = "android")]
{
if let Some(fd) = fd {
use std::os::fd::{FromRawFd, AsRawFd};
use tokio::io::unix::AsyncFd;
use std::os::unix::io::OwnedFd;
let async_fd = AsyncFd::new(unsafe { OwnedFd::from_raw_fd(fd) })?;
let async_fd_shared = std::sync::Arc::new(async_fd);
let afd1 = async_fd_shared.clone();
let tun_to_stack = tokio::spawn(async move {
let mut frame = vec![0u8; 65535];
loop {
let mut guard = match afd1.readable().await {
Ok(g) => g,
Err(_) => break,
};
match guard.try_io(|inner| {
let res = unsafe { libc::read(inner.as_raw_fd(), frame.as_mut_ptr() as *mut libc::c_void, frame.len()) };
if res < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::WouldBlock { Err(err) } else { Ok(res as isize) }
} else { Ok(res as isize) }
}) {
Ok(Ok(n)) if n > 0 => {
if let Err(_) = stack_sink.send(frame[..n as usize].to_vec()).await { break; }
}
Ok(Ok(_)) => break,
Ok(Err(_)) => break,
Err(_) => continue,
}
}
});
let afd2 = async_fd_shared.clone();
let stack_to_tun = tokio::spawn(async move {
while let Some(Ok(frame)) = stack_stream.next().await {
let mut written = 0;
while written < frame.len() {
let mut guard = match afd2.writable().await {
Ok(g) => g,
Err(_) => break,
};
match guard.try_io(|inner| {
let res = unsafe { libc::write(inner.as_raw_fd(), frame[written..].as_ptr() as *const libc::c_void, frame.len() - written) };
if res < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::WouldBlock { Err(err) } else { Ok(res as isize) }
} else { Ok(res as isize) }
}) {
Ok(Ok(n)) if n > 0 => written += n as usize,
Ok(Ok(_)) => break,
Ok(Err(_)) => break,
Err(_) => continue,
}
}
}
});
(tun_to_stack, stack_to_tun)
} else {
return Err(anyhow!("FD is required on Android but not provided"));
}
}
#[cfg(not(target_os = "android"))]
{
let opts = ostp_tun::OstpTunOptions {
server_ip: bypass_ips.first().copied().unwrap_or_else(|| "127.0.0.1".parse().unwrap()),
bypass_ips: bypass_ips,
dns_server: None,
kill_switch: false,
mtu: mtu as u16,
wintun_path: None,
};
let tun_interface = ostp_tun::OstpTunInterface::create(opts)
.await
.map_err(|e| anyhow!("Failed to create OstpTunInterface: {}", e))?;
let dev = tun_interface.device;
_route_guard = Some(tun_interface.guard);
let (mut tun_read, mut tun_write) = tokio::io::split(dev);
let tun_to_stack = tokio::spawn(async move {
let mut buf = vec![0u8; 65536]; let mut buf = vec![0u8; 65536];
loop { loop {
match tun_read.read(&mut buf).await { match tun_read.read(&mut buf).await {
Ok(0) => break, Ok(0) => break,
Ok(n) => { Ok(n) => {
let frame = buf[..n].to_vec(); if let Err(_) = stack_sink.send(buf[..n].to_vec()).await { break; }
if let Err(e) = stack_sink.send(frame).await {
if e.kind() == std::io::ErrorKind::BrokenPipe {
break;
}
}
}
Err(e) => {
tracing::debug!("tun_read error: {e}");
} }
Err(e) => tracing::debug!("tun_read error: {e}"),
} }
} }
}); });
let stack_to_tun = tokio::spawn(async move {
let mut stack_to_tun = tokio::spawn(async move {
while let Some(Ok(frame)) = stack_stream.next().await { while let Some(Ok(frame)) = stack_stream.next().await {
if let Err(e) = tun_write.write(&frame).await { if let Err(e) = tun_write.write(&frame).await { tracing::debug!("tun_write error: {e}"); }
tracing::debug!("tun_write error: {e}");
}
} }
}); });
(tun_to_stack, stack_to_tun)
}
};
// ── TCP Handler ── // ── TCP Handler ──
let outbound_manager_tcp = outbound_manager.clone(); let outbound_manager_tcp = outbound_manager.clone();

View File

@ -2,16 +2,82 @@ 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::{NoiseRole, OstpEvent, ProtocolAction, ProtocolConfig, ProtocolMachine};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::UdpSocket;
pub async fn dial_tcp( pub async fn dial_tcp(
_server: &str, server: &str,
_port: u16, port: u16,
_access_key: &str, access_key: &str,
_transport: &TransportConfig, _transport: &TransportConfig,
_multiplex: &MultiplexConfig, _multiplex: &MultiplexConfig,
) -> Result<TcpStream> { ) -> Result<TcpStream> {
// Ostp dialer implementation. let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
// For now returning an error until we migrate the local_proxy connection logic here. let local_addr = listener.local_addr()?;
Err(anyhow!("OSTP TCP dialer not yet fully migrated")) let client_stream = tokio::net::TcpStream::connect(local_addr).await?;
let (mut server_stream, _) = listener.accept().await?;
let udp = tokio::net::UdpSocket::bind("0.0.0.0:0").await?;
udp.connect((server, port)).await?;
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::None,
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 mut machine = ProtocolMachine::new(config).unwrap();
// Spawn bridge task
tokio::spawn(async move {
if let Ok(action) = machine.on_event(OstpEvent::Start) {
handle_action(action, &udp, &mut server_stream).await;
}
let mut buf = [0u8; 65535];
let mut udp_buf = [0u8; 65535];
loop {
tokio::select! {
Ok(n) = server_stream.read(&mut buf) => {
if n == 0 { break; }
if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::copy_from_slice(&buf[..n]))) {
handle_action(action, &udp, &mut server_stream).await;
}
}
Ok(n) = udp.recv(&mut udp_buf) => {
if let Ok(action) = machine.on_event(OstpEvent::Inbound(bytes::Bytes::copy_from_slice(&udp_buf[..n]))) {
handle_action(action, &udp, &mut server_stream).await;
}
}
_ = tokio::time::sleep(std::time::Duration::from_millis(10)) => {
if let Ok(action) = machine.on_event(OstpEvent::Tick) {
handle_action(action, &udp, &mut server_stream).await;
}
}
}
}
});
Ok(client_stream)
} }
pub async fn handle_udp( pub async fn handle_udp(
@ -26,3 +92,24 @@ pub async fn handle_udp(
) -> Result<()> { ) -> Result<()> {
Err(anyhow!("OSTP UDP handler not yet fully migrated")) Err(anyhow!("OSTP UDP handler not yet fully migrated"))
} }
async fn handle_action(action: ProtocolAction, udp: &UdpSocket, server_stream: &mut tokio::net::TcpStream) {
match action {
ProtocolAction::SendDatagram(data) => {
let _ = udp.send(&data).await;
}
ProtocolAction::DeliverApp(_stream_id, payload) => {
let _ = server_stream.write_all(&payload).await;
}
ProtocolAction::Multiple(actions) => {
for a in actions {
match a {
ProtocolAction::SendDatagram(data) => { let _ = udp.send(&data).await; }
ProtocolAction::DeliverApp(_stream_id, payload) => { let _ = server_stream.write_all(&payload).await; }
_ => {}
}
}
}
_ => {}
}
}

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.2.97+12 version: 0.2.98+13
environment: environment:
sdk: ^3.11.4 sdk: ^3.11.4

View File

@ -2665,7 +2665,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-client" name = "ostp-client"
version = "0.2.97" version = "0.2.98"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.22.1", "base64 0.22.1",
@ -2676,6 +2676,7 @@ dependencies = [
"futures-util", "futures-util",
"hex", "hex",
"hmac", "hmac",
"ipnet",
"json_comments", "json_comments",
"libc", "libc",
"netstack-smoltcp", "netstack-smoltcp",
@ -2699,7 +2700,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-core" name = "ostp-core"
version = "0.2.97" version = "0.2.98"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -2734,7 +2735,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-tun" name = "ostp-tun"
version = "0.2.97" version = "0.2.98"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"libc", "libc",

View File

@ -13,7 +13,8 @@ use tauri::Emitter;
#[serde(tag = "mode", rename_all = "lowercase")] #[serde(tag = "mode", rename_all = "lowercase")]
enum AppMode { enum AppMode {
Server(serde_json::Value), Server(serde_json::Value),
Client(ClientConfigRaw), #[serde(rename = "client")]
Client(serde_json::Value),
} }
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
@ -23,56 +24,6 @@ struct UnifiedConfig {
log_level: Option<String>, log_level: Option<String>,
} }
#[derive(Debug, Deserialize, Serialize, Clone)]
struct ClientConfigRaw {
server: String,
access_key: String,
socks5_bind: Option<String>,
tun: Option<TunConfig>,
transport: Option<TransportConfigRaw>,
debug: Option<bool>,
exclude: Option<ExcludeConfig>,
mux: Option<MuxConfig>,
gui: Option<GuiConfig>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
struct GuiConfig {
autoconnect: Option<bool>,
launch_startup: Option<bool>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
struct TunConfig {
enable: bool,
wintun_path: Option<String>,
ipv4_address: Option<String>,
dns: Option<String>,
stack: Option<String>,
kill_switch: Option<bool>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
struct TransportConfigRaw {
mode: Option<String>,
stealth_sni: Option<String>,
wss: Option<bool>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
struct ExcludeConfig {
domains: Option<Vec<String>>,
ips: Option<Vec<String>>,
processes: Option<Vec<String>>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
struct MuxConfig {
enabled: Option<bool>,
sessions: Option<usize>,
}
#[derive(Serialize)] #[derive(Serialize)]
struct UIMetrics { struct UIMetrics {
bytes_sent: u64, bytes_sent: u64,
@ -143,44 +94,7 @@ fn get_config_path() -> PathBuf {
PathBuf::from("config.json") PathBuf::from("config.json")
} }
fn map_to_client_config(raw: &ClientConfigRaw, mode: &str) -> ostp_client::config::ClientConfig {
ostp_client::config::ClientConfig {
mode: mode.to_string(),
debug: raw.debug.unwrap_or(false),
ostp: ostp_client::config::OstpConfig {
server_addr: raw.server.clone(),
local_bind_addr: "0.0.0.0:0".to_string(),
access_key: raw.access_key.clone(),
handshake_timeout_ms: 5000,
io_timeout_ms: 5000,
mtu: 1350,
keepalive_interval_sec: 5,
},
local_proxy: ostp_client::config::LocalProxyConfig {
bind_addr: raw.socks5_bind.clone().unwrap_or_else(|| "127.0.0.1:1088".to_string()),
connect_timeout_ms: 5000,
},
transport: ostp_client::config::TransportConfig {
mode: raw.transport.as_ref().and_then(|t| t.mode.clone()).unwrap_or_else(|| "udp".to_string()),
stealth_sni: raw.transport.as_ref().and_then(|t| t.stealth_sni.clone()).unwrap_or_else(|| "microsoft.com".to_string()),
wss: raw.transport.as_ref().and_then(|t| t.wss).unwrap_or(false),
},
exclusions: ostp_client::config::ExclusionConfig {
domains: raw.exclude.as_ref().and_then(|e| e.domains.clone()).unwrap_or_default(),
ips: raw.exclude.as_ref().and_then(|e| e.ips.clone()).unwrap_or_default(),
processes: raw.exclude.as_ref().and_then(|e| e.processes.clone()).unwrap_or_default(),
},
multiplex: ostp_client::config::MultiplexConfig {
enabled: raw.mux.as_ref().and_then(|m| m.enabled).unwrap_or(false),
sessions: raw.mux.as_ref().and_then(|m| m.sessions).unwrap_or(1),
},
dns_server: raw.tun.as_ref().and_then(|t| t.dns.clone()),
tun_stack: raw.tun.as_ref().and_then(|t| t.stack.clone()).unwrap_or_else(|| "system".to_string()),
kill_switch: raw.tun.as_ref().and_then(|t| t.kill_switch).unwrap_or(false),
gui: raw.gui.as_ref().map(|g| serde_json::to_value(g).unwrap()),
}
}
// ── Tauri commands ──────────────────────────────────────────────────────────── // ── Tauri commands ────────────────────────────────────────────────────────────
@ -356,7 +270,14 @@ async fn save_config(json_content: String) -> Result<bool, String> {
let _parsed: UnifiedConfig = serde_json::from_reader(&mut stripped) let _parsed: UnifiedConfig = serde_json::from_reader(&mut stripped)
.map_err(|e| format!("Invalid configuration: {}", e))?; .map_err(|e| format!("Invalid configuration: {}", e))?;
let path = get_config_path(); let path = get_config_path();
std::fs::write(path, json_content).map_err(|e| format!("Failed to write config: {}", e))?;
let mut final_content = json_content;
if !final_content.trim_start().starts_with("// OSTP") {
let header = "// OSTP Configuration v0.3.1\n// DO NOT EDIT THIS COMMENT - Migrator relies on it\n";
final_content = format!("{}{}", header, final_content);
}
std::fs::write(path, final_content).map_err(|e| format!("Failed to write config: {}", e))?;
Ok(true) Ok(true)
} }
@ -431,8 +352,9 @@ async fn reload_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String
AppMode::Client(c) => c, AppMode::Client(c) => c,
AppMode::Server(_) => return Err("GUI only supports Client mode.".into()), AppMode::Server(_) => return Err("GUI only supports Client mode.".into()),
}; };
let mode_str = if client_cfg.tun.as_ref().map(|t| t.enable).unwrap_or(false) { "tun" } else { "proxy" }; let (migrated, _) = ostp_client::config::ClientConfig::migrate_json(client_cfg);
let core_cfg = map_to_client_config(&client_cfg, mode_str); let core_cfg: ostp_client::config::ClientConfig = serde_json::from_value(migrated)
.map_err(|e| format!("Failed to parse migrated config: {}", e))?;
let config_str = serde_json::to_string(&core_cfg).unwrap(); let config_str = serde_json::to_string(&core_cfg).unwrap();
match &guard.tunnel { match &guard.tunnel {
@ -507,7 +429,15 @@ async fn start_tunnel(state: tauri::State<'_, AppState>, app: tauri::AppHandle)
AppMode::Server(_) => return Err("GUI only supports Client mode.".into()), AppMode::Server(_) => return Err("GUI only supports Client mode.".into()),
}; };
let is_tun_enabled = client_cfg.tun.as_ref().map(|t| t.enable).unwrap_or(false); let (migrated, _) = ostp_client::config::ClientConfig::migrate_json(client_cfg);
let is_tun_enabled = migrated.get("inbounds")
.and_then(|i| i.as_array())
.map(|i| i.iter().any(|v| v.get("type").and_then(|t| t.as_str()) == Some("tun")))
.unwrap_or(false);
let parsed_config: ostp_client::config::ClientConfig = serde_json::from_value(migrated)
.map_err(|e| format!("Failed to parse migrated config: {}", e))?;
eprintln!("[OSTP] start_tunnel: is_tun_enabled={}", is_tun_enabled); eprintln!("[OSTP] start_tunnel: is_tun_enabled={}", is_tun_enabled);
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
@ -538,19 +468,19 @@ async fn start_tunnel(state: tauri::State<'_, AppState>, app: tauri::AppHandle)
if is_tun_enabled { if is_tun_enabled {
eprintln!("[OSTP] starting TUN via helper"); eprintln!("[OSTP] starting TUN via helper");
start_tun_via_helper(&mut guard, &client_cfg, app).await start_tun_via_helper(&mut guard, &parsed_config, app).await
} else { } else {
eprintln!("[OSTP] starting proxy in-process"); eprintln!("[OSTP] starting proxy in-process");
start_proxy_in_process(&mut guard, &client_cfg, app).await start_proxy_in_process(&mut guard, &parsed_config, app).await
} }
} }
async fn start_proxy_in_process( async fn start_proxy_in_process(
guard: &mut AppStateInner, guard: &mut AppStateInner,
raw: &ClientConfigRaw, parsed_config: &ostp_client::config::ClientConfig,
app: tauri::AppHandle, app: tauri::AppHandle,
) -> Result<bool, String> { ) -> Result<bool, String> {
let mapped = map_to_client_config(raw, "proxy"); let mapped = parsed_config.clone();
let metrics = Arc::new(BridgeMetrics { let metrics = Arc::new(BridgeMetrics {
bytes_sent: portable_atomic::AtomicU64::new(0), bytes_sent: portable_atomic::AtomicU64::new(0),
bytes_recv: portable_atomic::AtomicU64::new(0), bytes_recv: portable_atomic::AtomicU64::new(0),
@ -591,7 +521,7 @@ async fn start_proxy_in_process(
async fn start_tun_via_helper( async fn start_tun_via_helper(
guard: &mut AppStateInner, guard: &mut AppStateInner,
raw: &ClientConfigRaw, parsed_config: &ostp_client::config::ClientConfig,
app: tauri::AppHandle, app: tauri::AppHandle,
) -> Result<bool, String> { ) -> Result<bool, String> {
let port = { let port = {
@ -614,8 +544,8 @@ async fn start_tun_via_helper(
}).await.map_err(|_| "Timeout connecting to helper.".to_string())? }).await.map_err(|_| "Timeout connecting to helper.".to_string())?
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
// Send the correctly MAPPED config // Send the config
let mapped = map_to_client_config(raw, "tun"); let mapped = parsed_config.clone();
let start_cmd = serde_json::json!({ let start_cmd = serde_json::json!({
"cmd": "start", "cmd": "start",
"config": serde_json::to_string(&mapped).unwrap_or_default(), "config": serde_json::to_string(&mapped).unwrap_or_default(),
@ -674,11 +604,16 @@ struct HelperPipeState {
error_msg: Option<String>, error_msg: Option<String>,
} }
#[cfg(target_os = "windows")]
const HELPER_EXE_NAME: &str = "ostp-tun-helper.exe";
#[cfg(not(target_os = "windows"))]
const HELPER_EXE_NAME: &str = "ostp-tun-helper";
fn find_helper_exe() -> Option<PathBuf> { fn find_helper_exe() -> Option<PathBuf> {
if let Ok(exe) = std::env::current_exe() { if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() { if let Some(dir) = exe.parent() {
// 1. Release/Production adjacent // 1. Release/Production adjacent
let candidate = dir.join("ostp-tun-helper.exe"); let candidate = dir.join(HELPER_EXE_NAME);
if candidate.exists() { return Some(candidate); } if candidate.exists() { return Some(candidate); }
// 2. Tauri target directory fallback // 2. Tauri target directory fallback
@ -686,9 +621,9 @@ fn find_helper_exe() -> Option<PathBuf> {
let mut parent = dir; let mut parent = dir;
while let Some(p) = parent.parent() { while let Some(p) = parent.parent() {
if p.file_name().map(|n| n == "target").unwrap_or(false) { if p.file_name().map(|n| n == "target").unwrap_or(false) {
let deb = p.join("debug").join("ostp-tun-helper.exe"); let deb = p.join("debug").join(HELPER_EXE_NAME);
if deb.exists() { return Some(deb); } if deb.exists() { return Some(deb); }
let rel = p.join("release").join("ostp-tun-helper.exe"); let rel = p.join("release").join(HELPER_EXE_NAME);
if rel.exists() { return Some(rel); } if rel.exists() { return Some(rel); }
} }
parent = p; parent = p;
@ -698,13 +633,13 @@ fn find_helper_exe() -> Option<PathBuf> {
// 3. Current working directory target fallback // 3. Current working directory target fallback
let cwd = std::env::current_dir().unwrap_or_default(); let cwd = std::env::current_dir().unwrap_or_default();
let candidates = [ let candidates = [
cwd.join("ostp-tun-helper.exe"), cwd.join(HELPER_EXE_NAME),
cwd.join("target").join("debug").join("ostp-tun-helper.exe"), cwd.join("target").join("debug").join(HELPER_EXE_NAME),
cwd.join("target").join("release").join("ostp-tun-helper.exe"), cwd.join("target").join("release").join(HELPER_EXE_NAME),
cwd.join("..").join("target").join("debug").join("ostp-tun-helper.exe"), cwd.join("..").join("target").join("debug").join(HELPER_EXE_NAME),
cwd.join("..").join("target").join("release").join("ostp-tun-helper.exe"), cwd.join("..").join("target").join("release").join(HELPER_EXE_NAME),
cwd.join("..").join("..").join("target").join("debug").join("ostp-tun-helper.exe"), cwd.join("..").join("..").join("target").join("debug").join(HELPER_EXE_NAME),
cwd.join("..").join("..").join("target").join("release").join("ostp-tun-helper.exe"), cwd.join("..").join("..").join("target").join("release").join(HELPER_EXE_NAME),
]; ];
for path in &candidates { for path in &candidates {
if path.exists() { return Some(path.clone()); } if path.exists() { return Some(path.clone()); }
@ -740,9 +675,45 @@ fn launch_as_admin(exe: &std::path::PathBuf, token: &str, port: u16) -> anyhow::
Ok(()) Ok(())
} }
#[cfg(not(target_os = "windows"))] #[cfg(target_os = "macos")]
fn launch_as_admin(_exe: &PathBuf, _token: &str, _port: u16) -> Result<()> { anyhow::bail!("Windows only."); } fn launch_as_admin(exe: &std::path::PathBuf, token: &str, port: u16) -> anyhow::Result<()> {
let temp_dir = std::env::temp_dir();
let token_file = temp_dir.join(format!("ostp_auth_{}.tmp", rand::random::<u32>()));
std::fs::write(&token_file, token)?;
let cmd = format!("'{}' --port {} --token-file '{}'", exe.display(), port, token_file.display());
let script = format!("do shell script \"{}\" with administrator privileges", cmd);
let status = std::process::Command::new("osascript")
.arg("-e")
.arg(&script)
.status()?;
if !status.success() {
anyhow::bail!("osascript failed");
}
Ok(())
}
#[cfg(target_os = "linux")]
fn launch_as_admin(exe: &std::path::PathBuf, token: &str, port: u16) -> anyhow::Result<()> {
let temp_dir = std::env::temp_dir();
let token_file = temp_dir.join(format!("ostp_auth_{}.tmp", rand::random::<u32>()));
std::fs::write(&token_file, token)?;
let status = std::process::Command::new("pkexec")
.arg(exe)
.arg("--port")
.arg(port.to_string())
.arg("--token-file")
.arg(&token_file)
.status()?;
if !status.success() {
anyhow::bail!("pkexec failed");
}
Ok(())
}
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn show_error_dialog(msg: &str) { fn show_error_dialog(msg: &str) {
use std::os::windows::ffi::OsStrExt; use std::os::windows::ffi::OsStrExt;

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

View File

@ -331,6 +331,53 @@ async function loadConfigIntoForm() {
const c = rawConfig.mode === 'client' ? rawConfig : null; const c = rawConfig.mode === 'client' ? rawConfig : null;
if (!c) return; if (!c) return;
if (c.version === '0.3.1' || c.outbounds !== undefined) {
// NEW FORMAT
const ostpOut = (c.outbounds || []).find(o => o.type === 'ostp');
if (ostpOut) {
inServer.value = ostpOut.server ? `${ostpOut.server}:${ostpOut.port || 50000}` : '';
inKey.value = ostpOut.access_key || '';
inTransport.value = ostpOut.transport?.type || 'udp';
inSni.value = ostpOut.transport?.stealth_sni || '';
inWss.checked = !!ostpOut.transport?.wss;
inMux.checked = !!ostpOut.multiplex?.enabled;
inMuxSessions.value = ostpOut.multiplex?.sessions || '';
}
const tunIn = (c.inbounds || []).find(i => i.type === 'tun');
if (tunIn) {
inTun.checked = true;
inMtu.value = tunIn.mtu || '';
} else {
inTun.checked = false;
}
const socksIn = (c.inbounds || []).find(i => i.type === 'local_proxy');
if (socksIn) {
inSocks.value = `${socksIn.listen || '127.0.0.1'}:${socksIn.port || 1088}`;
}
inDns.value = ''; // DNS handling is manual in routing now, ignore here
if (inKillSwitch) inKillSwitch.checked = !!c.gui?.kill_switch;
inDebug.checked = c.log?.level === 'debug';
const ex = c.routing?.rules || [];
const doms = new Set();
const ips = new Set();
const procs = new Set();
ex.forEach(r => {
if (r.outbound === 'direct') {
if (r.domain_suffix) r.domain_suffix.forEach(d => doms.add(d));
if (r.ip_cidr) r.ip_cidr.forEach(ip => ips.add(ip));
if (r.process_name) r.process_name.forEach(p => procs.add(p));
}
});
tagState.domains = doms;
tagState.ips = ips;
tagState.processes = procs;
} else {
// OLD FORMAT
inServer.value = c.server || ''; inServer.value = c.server || '';
inKey.value = c.access_key || ''; inKey.value = c.access_key || '';
inSocks.value = c.socks5_bind || '127.0.0.1:1088'; inSocks.value = c.socks5_bind || '127.0.0.1:1088';
@ -345,17 +392,18 @@ async function loadConfigIntoForm() {
inMuxSessions.value = c.mux?.sessions || ''; inMuxSessions.value = c.mux?.sessions || '';
inDns.value = c.tun?.dns || ''; inDns.value = c.tun?.dns || '';
updateKillSwitchVisibility();
inDebug.checked = !!c.debug; inDebug.checked = !!c.debug;
if (inAutoconnect) inAutoconnect.checked = !!c.gui?.autoconnect;
if (inLaunchStartup) inLaunchStartup.checked = !!c.gui?.launch_startup;
const ex = c.exclude || {}; const ex = c.exclude || {};
tagState.domains = new Set(ex.domains || []); tagState.domains = new Set(ex.domains || []);
tagState.ips = new Set(ex.ips || []); tagState.ips = new Set(ex.ips || []);
tagState.processes = new Set(ex.processes || []); tagState.processes = new Set(ex.processes || []);
}
if (inAutoconnect) inAutoconnect.checked = !!c.gui?.autoconnect;
if (inLaunchStartup) inLaunchStartup.checked = !!c.gui?.launch_startup;
updateKillSwitchVisibility();
renderTagList('domains'); renderTagList('domains');
renderTagList('ips'); renderTagList('ips');
renderTagList('processes'); renderTagList('processes');
@ -380,53 +428,79 @@ async function handleSave(silent = false) {
if (!server) { if (!silent) showToast(t('err_server_req') || 'Server address required', 'error'); return; } if (!server) { if (!silent) showToast(t('err_server_req') || 'Server address required', 'error'); return; }
if (!key) { if (!silent) showToast(t('err_key_req') || 'Access key required', 'error'); return; } if (!key) { if (!silent) showToast(t('err_key_req') || 'Access key required', 'error'); return; }
rawConfig.mode = 'client';
rawConfig.server = server;
rawConfig.access_key = key;
rawConfig.socks5_bind = inSocks.value.trim() || null;
rawConfig.debug = inDebug.checked;
if (inAutoconnect || inLaunchStartup) {
rawConfig.gui = rawConfig.gui || {};
if (inAutoconnect) rawConfig.gui.autoconnect = inAutoconnect.checked;
if (inLaunchStartup) rawConfig.gui.launch_startup = inLaunchStartup.checked;
}
if (inLaunchStartup) { if (inLaunchStartup) {
try { await invoke('set_autostart', { enable: inLaunchStartup.checked }); } catch (err) { console.error('autostart error', err); } try { await invoke('set_autostart', { enable: inLaunchStartup.checked }); } catch (err) { console.error('autostart error', err); }
} }
const sHost = server.includes(':') ? server.substring(0, server.lastIndexOf(':')) : server;
const sPort = server.includes(':') ? parseInt(server.substring(server.lastIndexOf(':') + 1), 10) : 50000;
const socksStr = inSocks.value.trim() || '127.0.0.1:1088';
const socksHost = socksStr.includes(':') ? socksStr.substring(0, socksStr.lastIndexOf(':')) : '127.0.0.1';
const socksPort = socksStr.includes(':') ? parseInt(socksStr.substring(socksStr.lastIndexOf(':') + 1), 10) : 1088;
rawConfig.transport = rawConfig.transport || {}; const inbounds = [];
rawConfig.transport.mode = inTransport.value; inbounds.push({
rawConfig.transport.stealth_sni = inSni.value.trim() || undefined; type: "local_proxy",
rawConfig.transport.wss = inWss.checked; tag: "socks-in",
protocol: "socks",
listen: socksHost,
port: socksPort
});
const mtuStr = inMtu.value.trim(); if (inTun.checked) {
if (mtuStr) rawConfig.mtu = parseInt(mtuStr, 10); inbounds.push({
else delete rawConfig.mtu; type: "tun",
tag: "tun-in",
if (inMux.checked) { auto_route: !(inKillSwitch && inKillSwitch.checked),
const s = parseInt(inMuxSessions.value.trim(), 10); mtu: parseInt(inMtu.value, 10) || 1140
rawConfig.mux = { enabled: true, sessions: isNaN(s) ? 1 : s }; });
} else {
delete rawConfig.mux;
} }
rawConfig.tun = rawConfig.tun || {}; const outbounds = [
rawConfig.tun.enable = inTun.checked; {
rawConfig.tun.kill_switch = inKillSwitch ? inKillSwitch.checked : false; type: "ostp",
rawConfig.tun.wintun_path = rawConfig.tun.wintun_path || './wintun.dll'; tag: "proxy",
rawConfig.tun.ipv4_address = rawConfig.tun.ipv4_address || '10.1.0.2/24'; server: sHost,
rawConfig.tun.stack = 'ostp'; port: sPort,
rawConfig.tun.dns = inDns.value.trim() || null; access_key: key,
transport: {
type: inTransport.value,
stealth_sni: inSni.value.trim() || undefined,
wss: inWss.checked ? true : undefined
},
multiplex: inMux.checked ? {
enabled: true,
sessions: parseInt(inMuxSessions.value, 10) || 1
} : { enabled: false, sessions: 1 }
},
{ type: "direct", tag: "direct" },
{ type: "block", tag: "block" }
];
rawConfig.exclude = { const rules = [];
domains: [...tagState.domains], if (tagState.domains.size > 0) rules.push({ domain_suffix: Array.from(tagState.domains), outbound: "direct" });
ips: [...tagState.ips], if (tagState.ips.size > 0) rules.push({ ip_cidr: Array.from(tagState.ips), outbound: "direct" });
processes: [...tagState.processes], if (tagState.processes.size > 0) rules.push({ process_name: Array.from(tagState.processes), outbound: "direct" });
if (inKillSwitch && inKillSwitch.checked && inTun.checked) {
rules.push({ ip_cidr: ["0.0.0.0/0", "::/0"], outbound: "proxy" });
}
rawConfig = {
mode: 'client',
version: '0.3.1',
log: { level: inDebug.checked ? 'debug' : 'info' },
inbounds,
outbounds,
routing: { rules, default_outbound: "proxy" },
gui: rawConfig.gui || {}
}; };
if (inAutoconnect) rawConfig.gui.autoconnect = inAutoconnect.checked;
if (inLaunchStartup) rawConfig.gui.launch_startup = inLaunchStartup.checked;
if (inKillSwitch) rawConfig.gui.kill_switch = inKillSwitch.checked;
try { try {
const ok = await invoke('save_config', { jsonContent: JSON.stringify(rawConfig, null, 2) }); const ok = await invoke('save_config', { jsonContent: JSON.stringify(rawConfig, null, 2) });
if (!ok && !silent) { if (!ok && !silent) {
@ -544,11 +618,21 @@ window.addEventListener('DOMContentLoaded', async () => {
for (let mtu of mtus) { for (let mtu of mtus) {
showToast(`Testing: ${mode.t} | WSS: ${mode.w} | XTLS: ${mode.r} | MTU: ${mtu}`); showToast(`Testing: ${mode.t} | WSS: ${mode.w} | XTLS: ${mode.r} | MTU: ${mtu}`);
rawConfig.ostp = rawConfig.ostp || {}; const ostpOut = (rawConfig.outbounds || []).find(o => o.type === 'ostp');
rawConfig.ostp.mtu = mtu; if (ostpOut) {
rawConfig.transport = rawConfig.transport || {}; ostpOut.transport = ostpOut.transport || { type: 'udp' };
rawConfig.transport.mode = mode.t; ostpOut.transport.type = mode.t;
rawConfig.transport.wss = mode.w; ostpOut.transport.wss = mode.w ? true : undefined;
} else {
rawConfig.transport = { mode: mode.t, wss: mode.w };
}
const tunIn = (rawConfig.inbounds || []).find(i => i.type === 'tun');
if (tunIn) {
tunIn.mtu = mtu;
} else {
rawConfig.mtu = mtu;
}

View File

@ -3,13 +3,11 @@ use jni::sys::{jboolean, jstring};
use jni::JNIEnv; use jni::JNIEnv;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::sync::{atomic::Ordering, Arc, RwLock}; use std::sync::{Arc, RwLock};
use std::sync::atomic::Ordering;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use tokio::sync::{mpsc, watch}; use tokio::sync::{mpsc, watch};
use ostp_client::bridge::{Bridge, BridgeMetrics}; use ostp_client::bridge::BridgeMetrics;
use ostp_client::config::ClientConfig;
use ostp_client::tunnel;
use ostp_client::app::{BridgeCommand, UiEvent};
use std::io::Write; use std::io::Write;
static LOG_TX: std::sync::OnceLock<std::sync::mpsc::Sender<String>> = std::sync::OnceLock::new(); static LOG_TX: std::sync::OnceLock<std::sync::mpsc::Sender<String>> = std::sync::OnceLock::new();
@ -65,7 +63,6 @@ struct SdkState {
shutdown_tx: Option<watch::Sender<bool>>, shutdown_tx: Option<watch::Sender<bool>>,
metrics: Option<Arc<BridgeMetrics>>, metrics: Option<Arc<BridgeMetrics>>,
tun_child: Option<std::process::Child>, tun_child: Option<std::process::Child>,
cmd_tx: Option<mpsc::Sender<BridgeCommand>>,
} }
impl SdkState { impl SdkState {
@ -75,7 +72,6 @@ impl SdkState {
shutdown_tx: None, shutdown_tx: None,
metrics: None, metrics: None,
tun_child: None, tun_child: None,
cmd_tx: None,
} }
} }
} }
@ -100,19 +96,9 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeStartClient(
_class: JClass, _class: JClass,
config_json: JString, config_json: JString,
fd: jni::sys::jint, fd: jni::sys::jint,
t2s_bin_path: JString, _t2s_bin_path: JString,
local_proxy: JString, _local_proxy: JString,
) -> jboolean { ) -> jboolean {
let mut state = match STATE.write() {
Ok(s) => s,
Err(_) => return jni::sys::JNI_FALSE,
};
if state.runtime.is_some() {
add_log("Client is already running!".to_string());
return jni::sys::JNI_TRUE;
}
init_tracing(); init_tracing();
if let Ok(jvm) = env.get_java_vm() { if let Ok(jvm) = env.get_java_vm() {
@ -160,26 +146,37 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeStartClient(
Err(_) => return jni::sys::JNI_FALSE, Err(_) => return jni::sys::JNI_FALSE,
}; };
let t2s_path: String = match env.get_string(&t2s_bin_path) {
Ok(s) => s.into(),
Err(_) => return jni::sys::JNI_FALSE,
};
let proxy_addr: String = match env.get_string(&local_proxy) {
Ok(s) => s.into(),
Err(_) => return jni::sys::JNI_FALSE,
};
// Parse config from JSON // Parse config from JSON
let config: ClientConfig = match serde_json::from_str(&config_str) { let parsed_val: serde_json::Value = match serde_json::from_str(&config_str) {
Ok(cfg) => cfg, Ok(v) => v,
Err(e) => { Err(e) => {
add_log(format!("Failed to parse config JSON: {e}")); add_log(format!("Failed to parse config JSON: {e}"));
return jni::sys::JNI_FALSE; return jni::sys::JNI_FALSE;
} }
}; };
let debug = config.debug; let (mut migrated, _) = ostp_client::config::ClientConfig::migrate_json(parsed_val);
// Insert fd into TUN inbound
if fd > 0 {
if let Some(inbounds) = migrated.get_mut("inbounds").and_then(|v| v.as_array_mut()) {
for inbound in inbounds.iter_mut() {
if inbound.get("type").and_then(|t| t.as_str()) == Some("tun") {
if let Some(obj) = inbound.as_object_mut() {
obj.insert("fd".to_string(), serde_json::json!(fd));
}
}
}
}
}
let config: ostp_client::config::ClientConfig = match serde_json::from_value(migrated) {
Ok(cfg) => cfg,
Err(e) => {
add_log(format!("Failed to build ClientConfig: {e}"));
return jni::sys::JNI_FALSE;
}
};
// Create tokio runtime // Create tokio runtime
let rt = match Runtime::new() { let rt = match Runtime::new() {
@ -190,162 +187,31 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeStartClient(
} }
}; };
let (proxy_events_tx, proxy_events_rx) = mpsc::channel(512); let metrics = Arc::new(ostp_client::bridge::BridgeMetrics {
let (client_msgs_tx, client_msgs_rx) = mpsc::unbounded_channel();
let metrics = Arc::new(BridgeMetrics {
bytes_sent: portable_atomic::AtomicU64::new(0), bytes_sent: portable_atomic::AtomicU64::new(0),
bytes_recv: portable_atomic::AtomicU64::new(0), bytes_recv: portable_atomic::AtomicU64::new(0),
connection_state: portable_atomic::AtomicU8::new(0), connection_state: portable_atomic::AtomicU8::new(0),
rtt_ms: portable_atomic::AtomicU32::new(0), rtt_ms: portable_atomic::AtomicU32::new(0),
}); });
let bridge = match Bridge::new(&config, Arc::clone(&metrics)) { let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
Ok(b) => b,
Err(e) => {
add_log(format!("Failed to initialize Bridge: {e}"));
return jni::sys::JNI_FALSE;
}
};
let (ui_tx, mut ui_rx) = mpsc::channel(512);
let (cmd_tx, cmd_rx) = mpsc::channel(128);
let (shutdown_tx, shutdown_rx) = watch::channel(false);
let proxy_shutdown_rx = shutdown_tx.subscribe();
// Create exclusions channel
let (exclusions_tx, exclusions_rx) = watch::channel(config.exclusions.clone());
let _exclusions_rx_tun = exclusions_tx.subscribe();
let metrics_clone = Arc::clone(&metrics); let metrics_clone = Arc::clone(&metrics);
// Spawn async tasks inside runtime
rt.spawn(async move { rt.spawn(async move {
bridge.run(ui_tx, cmd_rx, shutdown_rx, proxy_events_rx, client_msgs_tx).await if let Err(e) = ostp_client::runner::run_client_core(config, metrics_clone, shutdown_rx, None).await {
}); add_log(format!("OSTP Core exited with error: {}", e));
let config_proxy = config.clone();
rt.spawn(async move {
tunnel::run_local_proxy(
config_proxy.local_proxy,
config_proxy.ostp,
exclusions_rx,
config_proxy.debug,
proxy_shutdown_rx,
proxy_events_tx,
client_msgs_rx,
)
.await
});
// Start logs receiver task
rt.spawn(async move {
while let Some(msg) = ui_rx.recv().await {
match msg {
UiEvent::Log(text) => add_log(text),
UiEvent::ProfileChanged(p) => add_log(format!("Profile changed: {p:?}")),
UiEvent::TunnelStopped => add_log("Tunnel stopped".to_string()),
_ => {}
}
} }
}); });
// Toggle tunnel to initiate handshake let mut state = match STATE.write() {
let cmd_tx_clone = cmd_tx.clone(); Ok(s) => s,
rt.spawn(async move { Err(_) => return jni::sys::JNI_FALSE,
let _ = cmd_tx_clone.send(BridgeCommand::ToggleTunnel).await;
});
if config.tun_stack == "system" {
// Spawn tun2socks
let fd_str = format!("fd://{}", fd);
let proxy_str = format!("socks5://{}", proxy_addr);
if debug {
add_log(format!("Spawning tun2socks: {} -device {} -proxy {}", t2s_path, fd_str, proxy_str));
}
let mut cmd = std::process::Command::new(&t2s_path);
cmd.arg("-device")
.arg(&fd_str)
.arg("-proxy")
.arg(&proxy_str);
if config.ostp.mtu > 0 {
cmd.arg("-mtu").arg(config.ostp.mtu.to_string());
}
cmd.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {
add_log(format!("Failed to spawn tun2socks from Rust: {e}"));
return jni::sys::JNI_FALSE;
}
}; };
let stdout = match child.stdout.take() {
Some(s) => s,
None => {
add_log("Failed to capture tun2socks stdout".to_string());
return jni::sys::JNI_FALSE;
}
};
let stderr = match child.stderr.take() {
Some(s) => s,
None => {
add_log("Failed to capture tun2socks stderr".to_string());
return jni::sys::JNI_FALSE;
}
};
// Read stdout
std::thread::spawn(move || {
use std::io::{BufRead, BufReader};
let reader = BufReader::new(stdout);
for line in reader.lines() {
if let Ok(l) = line {
if debug {
add_log(format!("tun2socks: {}", l));
}
}
}
});
// Read stderr & wait
std::thread::spawn(move || {
use std::io::{BufRead, BufReader};
let reader = BufReader::new(stderr);
for line in reader.lines() {
if let Ok(l) = line {
if debug {
add_log(format!("tun2socks ERROR: {}", l));
}
}
}
});
state.tun_child = Some(child);
} else {
if debug {
add_log("Using OSTP native TUN stack. Bypassing tun2socks.".to_string());
}
let shutdown_rx_clone = shutdown_tx.subscribe();
let config_clone = config.clone();
let (exclusions_tx, exclusions_rx) = tokio::sync::watch::channel(config.exclusions.clone());
rt.spawn(async move {
let _tx = exclusions_tx; // keep tx alive
if let Err(e) = tunnel::native_handler::run_native_tunnel_from_fd(config_clone, shutdown_rx_clone, exclusions_rx, fd).await {
add_log(format!("Native TUN exited with error: {}", e));
}
});
}
state.runtime = Some(rt); state.runtime = Some(rt);
state.shutdown_tx = Some(shutdown_tx); state.shutdown_tx = Some(shutdown_tx);
state.metrics = Some(metrics_clone); state.metrics = Some(metrics);
state.cmd_tx = Some(cmd_tx); state.tun_child = None;
add_log("OSTP SDK: Client successfully started".to_string()); add_log("OSTP SDK: Client successfully started".to_string());
jni::sys::JNI_TRUE jni::sys::JNI_TRUE
@ -376,7 +242,6 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeStopClient(
let c = state.tun_child.take(); let c = state.tun_child.take();
let s = state.shutdown_tx.take(); let s = state.shutdown_tx.take();
let r = state.runtime.take(); let r = state.runtime.take();
state.cmd_tx = None;
state.metrics = None; state.metrics = None;
(c, s, r) (c, s, r)
}; };
@ -496,15 +361,10 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_notifyNetworkChanged(
_env: JNIEnv, _env: JNIEnv,
_class: JClass, _class: JClass,
) { ) {
let state = match STATE.read() { let _state = match STATE.read() {
Ok(s) => s, Ok(s) => s,
Err(_) => return, Err(_) => return,
}; };
if let Some(ref cmd_tx) = state.cmd_tx { // No-op for now; multi-server handles network drops via keep-alives and reconnection
// Use try_send since we're likely on a background thread from Android's ConnectivityManager
let _ = cmd_tx.try_send(ostp_client::app::BridgeCommand::NetworkChanged);
add_log("notifyNetworkChanged: BridgeCommand::NetworkChanged sent".to_string());
}
} }