diff --git a/.gitignore b/.gitignore index 44e9d4d..f906cb3 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ turn-harvesting-idea.md ostp-prober/ ostp-brain/ + +# Management panel built assets (built separately; dummy dist created for rust-embed build) +ostp-control/ diff --git a/Cargo.lock b/Cargo.lock index 9d80c69..33d5f25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1384,7 +1384,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "ostp" -version = "0.2.98" +version = "0.4.0" dependencies = [ "anyhow", "base64", @@ -1405,16 +1405,14 @@ dependencies = [ [[package]] name = "ostp-client" -version = "0.2.98" +version = "0.4.0" dependencies = [ "anyhow", "base64", "bytes", - "chacha20poly1305", "chrono", "futures", "futures-util", - "hex", "hmac", "json_comments", "libc", @@ -1434,12 +1432,11 @@ dependencies = [ "tun", "webpki-roots 0.26.11", "winapi", - "x25519-dalek", ] [[package]] name = "ostp-core" -version = "0.2.98" +version = "0.4.0" dependencies = [ "anyhow", "bytes", @@ -1473,7 +1470,7 @@ dependencies = [ [[package]] name = "ostp-server" -version = "0.2.98" +version = "0.4.0" dependencies = [ "anyhow", "axum", @@ -1505,7 +1502,7 @@ dependencies = [ [[package]] name = "ostp-tun" -version = "0.2.98" +version = "0.4.0" dependencies = [ "anyhow", "libc", @@ -1517,7 +1514,7 @@ dependencies = [ [[package]] name = "ostp-tun-helper" -version = "0.2.98" +version = "0.4.0" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 2486df5..96df4c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,8 +11,8 @@ resolver = "2" [workspace.package] edition = "2021" -license = "BSL 1.1" -version = "0.2.98" +license = "AGPL-3.0" +version = "0.4.0" [workspace.dependencies] anyhow = "1.0" diff --git a/REBUILD_PLAN.md b/REBUILD_PLAN.md new file mode 100644 index 0000000..d9e968c --- /dev/null +++ b/REBUILD_PLAN.md @@ -0,0 +1,185 @@ +# Чистая переборка на базе v0.2.98 + +База: `v0.2.98` (commit `31d0020`) — последняя версия, которая **стабильно работает**. +Ветка: `clean-rebuild`. Всё, что появилось после (0.3.1 … 0.3.21), переносим +**выборочно и с чистой головой**, а не копируем рефактор целиком. + +Принцип: 0.3.1 принёс «модульный multi-server рефактор» + лавину фич — и вместе с +ними нестабильность. Берём только проверенное и нужное. + +--- + +## Решения (зафиксировано пользователем) +- **Junk-пакеты + TCP-фрагментация — ОСТАВЛЯЕМ** (нравятся). НО починить вредную + часть: junk по UDP не должен выглядеть для сервера как `Unauthorized probe` + (rate-limit/гейт на сервере), иначе флуд лога и риск самобана клиента. Фича + остаётся — чиним поведение, а не выпиливаем. Тонкая настройка — §E. +- **Версия переборки — 0.4.0** (решено; 0.3.x сожжены в pre-release). +- **WSS и Reality (TLS-мимикрия) — ВЫКИНУТЬ.** Путь проекта — **zapret-like**: + обфускация/DPI-evasion на уровне пакетов (junk, фрагментация, обфускация), а НЕ + мимикрия под TLS. Reality с нуля тяжела и не вписывается. +- **Multi-server — НЕ НУЖЕН.** Режем до одного сервера → уходит urltest-группа и + половина сложности 0.3.1. +- **Конфиг — ПЛОСКИЙ по сути, но оформлен красиво/секционно как сейчас** (решено). + Сохраняем читаемую секционную структуру (server / transport / tun / dns / exclude + и т.п.), но **выпиливаем модульную машинерию**: массивы `inbounds[]`/`outbounds[]`, + `routing.rules[]` с тегами, `default_outbound`, urltest, мульти-сервер. Один сервер + на конфиг. Исключения = плоский список внутри секции `exclude`. +- **Профили — ОСТАВЛЯЕМ, single-select, в UI-слое** (решено). Профиль = сохранённый + конфиг одного сервера; активен ровно один (radio). Список/выбор/share живут во + фронте (prefs GUI / Flutter); **ядро о профилях не знает** — на «Подключить» из + выбранного профиля генерится плоский конфиг на один сервер. Никаких чекбоксов/ + мульти-актив/urltest. +- **Derived-secrets — ОСТАВЛЯЕМ, но ОБЯЗАТЕЛЬНО проверить, что он РЕАЛЬНО работает:** + старый клиент НЕ должен подключаться к новому серверу. В прошлой реализации это + НЕ соблюдалось (старый клиент → новый сервер подключался) — значит сервер всё ещё + принимал старый формат handshake / obfuscation-key. Это **баг**, закрыть в первую + очередь: сервер обязан отвергать всё, что не прошло derived-secrets. +- **Лицензия — AGPLv3.** +- **Брендинг — ОСТАВЛЯЕМ**: тёмная тема + орёл на фоне (watermark/логотип). +- **Стелс-философия (north-star): zapret-like** — «нет узнаваемого заголовка + + манипуляции пакетами» (обфускация, junk, фрагментация, DNS/UoT-транспорты), а НЕ + «притворись известным протоколом» (Reality/WSS — выкинуты). + +--- + +## 0. Корневая причина нестабильности 0.3.x +**Модульный multi-server рефактор (0.3.1)** — `580faf6`, `8ed66f9`, `67f9c06`. +Сменил формат конфига (inbounds/outbounds/routing/urltest), session-модель, +hot-reload. Источник большинства багов (мёртвые маршруты, фейк-коннект, +рассинхрон конфига). **НЕ копировать целиком.** Если multi-server реально нужен — +добавлять минимально и поверх рабочей одно-серверной модели 0.2.98. + +--- + +## A. ВЫКИНУТЬ / не переносить +1. **WSS-фрейминг и Reality (TLS-мимикрия)** — оба выкинуть. Путь zapret-like, а не + маскировка под TLS-сайт; Reality (`reality.rs`) к тому же сложно сделать корректно + с нуля. Удалить из базы 0.2.98 целиком. +2. **Multi-server / urltest-группа** — не нужен. Один сервер на конфиг. +3. Остатки **tun2socks** на Android (`libtun2socks.so`, `tun2socks-arm64`, + `tun_child`, `t2sBinPath`) — давно мёртвый код, только раздувает APK. Не тащить. + +> ⚠️ Junk-пакеты и TCP-фрагментация **ОСТАЮТСЯ** (см. Решения и §E) — это уже не +> «мусор». Но junk по UDP нужно сделать так, чтобы сервер его не считал +> `Unauthorized probe` (rate-limit/гейт), иначе лог-флуд и риск самобана. + +--- + +## B. ОБЯЗАТЕЛЬНО перенести (фиксы стабильности) +- **fd limits / EMFILE** — `922cf0b`. +- **Lifecycle хелпера**: принудительный `std::process::exit` после остановки, чтобы + не оставался зомби-процесс, держащий адаптер `ostp_tun` и дефолтный маршрут — `b6e78c1`. +- **Bypass-маршрут сервера через `route.exe` по шлюзу** (а не legacy + `CreateIpForwardEntry`, который падал с err 160 из-за рассинхрона индексов + интерфейсов) — `b6e78c1`. +- **IPC хелпера** (ChaCha20Poly1305 + hex) + **единый формат логов** — `ee38b15`. +- **Closing-state fix** + `sent_history` на `BTreeMap` (O(log n) NACK) — `47d44fa`. +- **Handshake timeout fixes** — `d65af35`, `6eb7b36` (ждать ответ до отправки данных). +- **Buffer / UDP handler** — `b5e830a`. +- **Логи**: UoT и unauthorized-probe → debug; rate-limit probe-лога — `1151726`, `fc339b3`. + +--- + +## C. Протокол / крипто — решить и перенести +- **Derived secrets handshake** — `f8f27d3`. PSK и obfuscation-key выводятся из + access-key через HKDF; handshake-payload = `[timestamp][session_id][access_key]`; + параметры паддинга деривируются; timestamp anti-replay (±300с). + ⚠️ **Ломает совместимость с 0.2.98 wire** (старый клиент не подключится). + Безопаснее старого (raw-PSK + нулевой obfuscation-key). **РЕШЕНИЕ:** переносим ли + (тогда нужен ребилд всех клиентов) — ДА, скорее всего, но осознанно. +- **l4_protocol** для server outbound — `2997bfd`, `ad3a8cb`, `aae9d22`. + +--- + +## D. Транспорты — перенести аккуратно (большие куски) +- **DNS transport (dnstt)** как fallback — `3f1adbc`, `3ced4a1`, `d031b15`, + `10c1772`, `b31da29`. Полезно против блокировок, но объёмно и со своей + фрагментацией/reassembly. Переносить отдельным изолированным модулем. +- UoT (UDP-over-TCP) — уже есть в 0.2.98, проверить что не сломан. + +--- + +## E. Тонкая настройка junk/фрагментации (как в AmneziaWG) +Junk и фрагментацию **оставляем** (Решения), а это — их параметризация. Главное +условие: **координация клиент↔сервер**, иначе junk превращается в probe-флуд. +- `Jc` — кол-во junk-пакетов, `Jmin`/`Jmax` — размеры; **сервер знает и молча отбрасывает**. +- `S1`/`S2` — размеры init/response подгоняются. +- Магические заголовки/сигнатуры пакетов (`H1..H4`). +Реализовать как явные настраиваемые поля (не хардкод). Сервер ОБЯЗАН их понимать. +Сам факт junk/frag — в базе; это «желание» — сделать их настраиваемыми. Можно потом. + +--- + +## F. GUI (desktop) — перенести нужное, без хаоса +- Профили на странице **настроек** (пусто + «Create a new profile» + «+» когда нет + профиля; «+» → меню «из ссылки / вручную»). Главный экран не усложнять. +- **Share** профиля: QR (генерить локально, ключ наружу не отдавать — крейт `qrcode`) + + копируемая `ostp://` ссылка. +- **Метрики**: байты считать в TUN-инбаунде; rtt брать из round-trip handshake + (а не отдельным TCP-probe). +- **Health/состояние**: «connected» по реальной достижимости сервера на ПРАВИЛЬНОМ + порту (не хардкод :443), а не по факту «процесс запустился». +- **routing**: всегда задавать `default_outbound: "proxy"`; ключи правил — + `domain_suffix` / `ip_cidr` / `process_name` (не `domains/ips/processes`). +- Смена сервера = полный **stop+start**, а не hot-reload (иначе остаётся старый сервер). +- Никаких непрогарженных `addEventListener` на удалённые элементы (краш init). + +--- + +## G. Мобилка (Flutter + JNI) — перенести нужное +- **routing**: тот же `default_outbound` + правильные ключи правил + (без них трафик шёл мимо туннеля — реальный IP). +- **Байты на Android**: считать в обеих задачах fd-пути (read=upload, write=download). +- **rtt**: из handshake (health-probe сокет на Android не protected → до сервера не доходит). +- **Смена сети (WiFi↔LTE)**: реальный reconnect (сейчас `notifyNetworkChanged` — no-op). +- **fd ownership**: НЕ двойное закрытие (Rust `OwnedFd` + Kotlin `close()`) → `detachFd()`. +- **Share** профиля: QR (`qr_flutter`) + ссылка. +- Выкинуть tun2socks (см. §A.3). + +--- + +## H. Инфра / лицензия / брендинг +- Лицензия: **AGPLv3** ✅ (зафиксировано). В 0.2.98 был BSL 1.1 → заменить (`9ce9e6d`). +- **Брендинг — ОСТАВЛЯЕМ** ✅: тёмная тема + орёл на фоне (watermark/логотип) в GUI. + Перенести из текущего `ostp-gui` (assets/logo.svg, тёмная палитра) в чистую переборку. +- Панель/license-check: open-source без license-check — `5782107`, `99ff76d` (если нужно). +- Версионирование/CI build-script — `774d926` и пр. + +--- + +## Инвентаризация базы 0.2.98 (что уже есть / что портировать) +- **Есть в 0.2.98**: WSS (→ удалить), Reality/`reality.rs` (→ удалить), + инфра derived-secrets (`derive_all_secrets`, `obfuscation_key`) — но клиент юзал + dummy-ключи до `f8f27d3`. +- **Нет в 0.2.98 — портировать из пост-0.2.98 кода**: junk-пакеты, TCP-фрагментация, + DNS-transport (dnstt), фикс derived-secrets `f8f27d3`, все фиксы §B, GUI/мобилка §F/§G. + +## Что легко упустить (решить до старта) +1. **Версия переборки — 0.4.0** (решено). Сожжённые 0.3.x не переиспользуем. +2. **Серверный конфиг — тоже плоский** и согласован с клиентским. Сервер обязан + поддерживать всё оставленное: derived-secrets (и **отвергать** старый формат), + корректную обработку junk (не probe-флуд), UoT, DNS-transport, management API. +3. **Версия/магический байт протокола ДО крипто-слоя.** Сейчас нельзя отличить старый + handshake от нового — отсюда баг «старый клиент → новый сервер подключился». + Добавить версию в wire → будущие изменения управляемы, сервер чётко режет + несовместимое. Это системный фикс проблемы derived-secrets. +4. **Клиент и сервер обновляются ВМЕСТЕ** — derived-secrets ломает совместимость, + смешивать старое и новое нельзя. Координировать выкладку. +5. **Reality — выкинуть** (решено; в базе 0.2.98 есть `reality.rs` → удалить целиком). +6. **Verify-loop = критерий «готово».** Каждая фича проверяется реальным тестом, не + «на словах»: connect → `curl` показывает IP **сервера**; старый клиент к новому + серверу **не** подключается; смена сети на мобилке восстанавливает туннель. + +## Порядок переборки (предложение, 1 сессия) +1. §B (фиксы стабильности) — на чистый 0.2.98. +2. §C (derived-secrets) — и СРАЗУ проверить: старый клиент к новому серверу НЕ + подключается (в прошлый раз был баг — подключался). +3. **Junk + TCP-фрагментация** — перенести (оставляем), но junk по UDP не должен + читаться сервером как `Unauthorized probe` (rate-limit/гейт на сервере). +4. §F/§G по минимуму (routing, метрики, состояние, share) **+ брендинг** (тёмная + тема, орёл на фоне). +5. §D (DNS transport) — если нужно. +6. ВЫКИНУТЬ: **WSS, multi-server, tun2socks** (§A). §E (тюнинг junk) — позже. +7. Конфиг: плоский по сути, секционно-оформленный, один сервер (РЕШЕНО — без + inbounds/outbounds/routing-движка). diff --git a/ostp-client/Cargo.toml b/ostp-client/Cargo.toml index 632c30b..ca7c567 100644 --- a/ostp-client/Cargo.toml +++ b/ostp-client/Cargo.toml @@ -29,7 +29,4 @@ tun = { version = "0.8.9", features = ["async"] } netstack-smoltcp = "0.2.2" futures = "0.3.32" libc = "0.2.186" -x25519-dalek = "2.0.1" -chacha20poly1305.workspace = true -hex = "0.4.3" winapi = { version = "0.3.9", features = ["iphlpapi", "tcpmib", "processthreadsapi", "psapi", "handleapi", "winerror", "minwindef", "winnt", "iptypes", "ws2def"] } diff --git a/ostp-client/src/bridge.rs b/ostp-client/src/bridge.rs index 0ba9b1e..0a804cd 100644 --- a/ostp-client/src/bridge.rs +++ b/ostp-client/src/bridge.rs @@ -66,7 +66,6 @@ pub struct Bridge { pub transport_mode: String, pub stealth_sni: String, - pub wss: bool, pub mtu: usize, pub kill_switch: bool, pub reload_tx: Option>, @@ -99,7 +98,6 @@ impl Bridge { transport_mode: config.transport.mode.clone(), stealth_sni: config.transport.stealth_sni.clone(), - wss: config.transport.wss, mtu: config.ostp.mtu, kill_switch: config.kill_switch, reload_tx: None, @@ -1026,7 +1024,6 @@ impl Bridge { self.mux_sessions = cfg.multiplex.sessions.max(1); self.transport_mode = cfg.transport.mode.clone(); self.stealth_sni = cfg.transport.stealth_sni.clone(); - self.wss = cfg.transport.wss; // Fix: wss was not updated on hot-reload self.mtu = cfg.ostp.mtu; self.keepalive_interval_sec = cfg.ostp.keepalive_interval_sec; self.kill_switch = cfg.kill_switch; diff --git a/ostp-client/src/config.rs b/ostp-client/src/config.rs index 45bf19b..9f8c903 100644 --- a/ostp-client/src/config.rs +++ b/ostp-client/src/config.rs @@ -70,18 +70,15 @@ pub struct LocalProxyConfig { } /// Transport layer configuration. -/// `mode` = "udp" (default) or "uot" (UDP over TCP with xHTTP stealth). +/// `mode` = "udp" (default) or "uot" (UDP over TCP с xHTTP-транспортом). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TransportConfig { /// "udp" or "uot" #[serde(default = "default_transport_mode")] pub mode: String, - /// TLS SNI and HTTP Host for stealth routing + /// TLS SNI and HTTP Host for xHTTP routing #[serde(default)] pub stealth_sni: String, - /// Enable strict RFC 6455 WebSocket framing - #[serde(default)] - pub wss: bool, } fn default_transport_mode() -> String { "udp".to_string() } @@ -91,7 +88,6 @@ impl Default for TransportConfig { Self { mode: default_transport_mode(), stealth_sni: String::new(), - wss: false, } } } @@ -173,7 +169,6 @@ struct RawUnifiedConfig { struct RawTransportSection { mode: Option, stealth_sni: Option, - wss: Option, } #[derive(Debug, Deserialize)] @@ -247,7 +242,6 @@ impl ClientConfig { transport: TransportConfig { mode: raw.transport.as_ref().and_then(|t| t.mode.clone()).unwrap_or_else(default_transport_mode), stealth_sni: raw.transport.as_ref().and_then(|t| t.stealth_sni.clone()).unwrap_or_default(), - wss: raw.transport.as_ref().and_then(|t| t.wss).unwrap_or(false), }, exclusions: ExclusionConfig { domains: exclusions.domains.unwrap_or_default(), diff --git a/ostp-client/src/transport/xhttp.rs b/ostp-client/src/transport/xhttp.rs deleted file mode 100644 index fbe31f4..0000000 --- a/ostp-client/src/transport/xhttp.rs +++ /dev/null @@ -1,394 +0,0 @@ -use std::net::IpAddr; -use std::sync::Arc; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::TcpStream; -use bytes::{Buf, BufMut, Bytes, BytesMut}; -use anyhow::{Result, Context}; -use tokio::sync::mpsc; -use hmac::Hmac; -use sha2::Sha256; -use base64::Engine; -use std::pin::Pin; -use std::task::{Context as TaskContext, Poll}; -use x25519_dalek::PublicKey; -use chacha20poly1305::{aead::Aead, ChaCha20Poly1305, Nonce}; - -use ostp_core::crypto::reality::{build_client_hello, derive_keys, generate_session_id, generate_x25519_keypair, REALITY_SERVER_HANDSHAKE_RECORDS}; -use ostp_core::framing::wss::{encode_wss_frame, decode_wss_frame, WssFrameResult}; - -type HmacSha256 = Hmac; - -pub async fn connect_xhttp( - target_ip: IpAddr, - port: u16, - sni: &str, - access_key: &[u8], - reality_enabled: bool, - wss: bool, - reality_pbk: &str, - reality_sid: &str, -) -> Result<(mpsc::Sender, Arc>>)> { - let addr = std::net::SocketAddr::new(target_ip, port); - - #[cfg(not(target_os = "android"))] - let mut tcp_stream = tokio::time::timeout( - std::time::Duration::from_secs(10), - tokio::net::TcpStream::connect(addr), - ) - .await - .map_err(|_| anyhow::anyhow!("TCP connect timeout to {}", addr))? - .with_context(|| format!("failed to connect to {}", addr))?; - - #[cfg(target_os = "android")] - let mut tcp_stream = { - let domain = if target_ip.is_ipv6() { socket2::Domain::IPV6 } else { socket2::Domain::IPV4 }; - let sock = socket2::Socket::new(domain, socket2::Type::STREAM, Some(socket2::Protocol::TCP))?; - - use std::os::unix::io::AsRawFd; - crate::bridge::protect_socket(sock.as_raw_fd()); - - sock.set_nonblocking(true)?; - let tcp_socket = tokio::net::TcpSocket::from_std_stream(sock.into()); - tokio::time::timeout( - std::time::Duration::from_secs(10), - tcp_socket.connect(addr), - ) - .await - .map_err(|_| anyhow::anyhow!("TCP connect timeout to {}", addr))? - .with_context(|| format!("failed to connect to {}", addr))? - }; - - tcp_stream.set_nodelay(true)?; - - if reality_enabled { - let pbk_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(reality_pbk) - .context("invalid reality_pbk base64")?; - if pbk_bytes.len() != 32 { - anyhow::bail!("reality_pbk must be 32 bytes"); - } - let pbk = PublicKey::from(<[u8; 32]>::try_from(pbk_bytes.as_slice()).unwrap()); - - let sid_bytes_vec = hex::decode(reality_sid).context("invalid reality_sid hex")?; - if sid_bytes_vec.len() != 8 { - anyhow::bail!("reality_sid must be 8 bytes"); - } - let sid: [u8; 8] = sid_bytes_vec.try_into().unwrap(); - - let (c_priv, c_pub) = generate_x25519_keypair(); - let shared_secret = c_priv.diffie_hellman(&pbk); - let (auth_key, data_key) = derive_keys(shared_secret.as_bytes()); - - let session_id = generate_session_id(&auth_key, &sid); - let client_hello = build_client_hello(if sni.is_empty() { "www.microsoft.com" } else { sni }, &session_id, &c_pub); - - tcp_stream.write_all(&client_hello).await?; - - // Drain all server handshake records (ServerHello, CCS, fake encrypted records). - // The server sends exactly REALITY_SERVER_HANDSHAKE_RECORDS records before data starts. - // Reading them explicitly prevents RealityStream from seeing non-AppData bytes. - for i in 0..REALITY_SERVER_HANDSHAKE_RECORDS { - let mut head = [0u8; 5]; - tcp_stream.read_exact(&mut head).await - .with_context(|| format!("reality handshake: failed reading record {} header", i))?; - if i == 0 && head[0] != 0x16 { - anyhow::bail!("expected ServerHello (0x16), got 0x{:02x}", head[0]); - } - let record_len = u16::from_be_bytes([head[3], head[4]]) as usize; - if record_len > 16384 { - anyhow::bail!("reality handshake: record {} too large: {} bytes", i, record_len); - } - let mut _payload = vec![0u8; record_len]; - tcp_stream.read_exact(&mut _payload).await - .with_context(|| format!("reality handshake: failed reading record {} payload", i))?; - } - - let reality_stream = RealityStream::new(tcp_stream, data_key); - xhttp_handshake_and_loop(reality_stream, target_ip, sni, access_key, wss).await - } else { - xhttp_handshake_and_loop(tcp_stream, target_ip, sni, access_key, wss).await - } -} - -// ----------------------------------------------------------------------- -// RealityStream: Wraps a TCP stream in fake TLS Application Data Records -// ----------------------------------------------------------------------- -struct RealityStream { - inner: TcpStream, - data_key: ChaCha20Poly1305, - rx_nonce: u64, - tx_nonce: u64, - rx_buf: BytesMut, - plaintext_buf: BytesMut, - tx_buf: BytesMut, -} - -impl RealityStream { - fn new(inner: TcpStream, data_key: ChaCha20Poly1305) -> Self { - Self { - inner, - data_key, - rx_nonce: 0, - tx_nonce: 0, - rx_buf: BytesMut::with_capacity(16384), - plaintext_buf: BytesMut::new(), - tx_buf: BytesMut::new(), - } - } - - fn make_nonce(seq: u64) -> [u8; 12] { - let mut nonce = [0u8; 12]; - nonce[4..12].copy_from_slice(&seq.to_le_bytes()); - nonce - } -} - -impl tokio::io::AsyncRead for RealityStream { - fn poll_read(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>, buf: &mut tokio::io::ReadBuf<'_>) -> Poll> { - loop { - if !self.plaintext_buf.is_empty() { - let out_len = std::cmp::min(buf.remaining(), self.plaintext_buf.len()); - buf.put_slice(&self.plaintext_buf[..out_len]); - self.plaintext_buf.advance(out_len); - return Poll::Ready(Ok(())); - } - - if self.rx_buf.len() >= 5 { - let len = u16::from_be_bytes([self.rx_buf[3], self.rx_buf[4]]) as usize; - if self.rx_buf.len() >= 5 + len { - if self.rx_buf[0] != 0x17 { - return Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "expected application data record"))); - } - - let ciphertext = &self.rx_buf[5..5+len]; - let nonce_bytes = Self::make_nonce(self.rx_nonce); - let nonce = Nonce::from_slice(&nonce_bytes); - - match self.data_key.decrypt(nonce, ciphertext) { - Ok(plaintext) => { - self.rx_nonce += 1; - self.plaintext_buf.put_slice(&plaintext); - self.rx_buf.advance(5 + len); - continue; - } - Err(_) => { - return Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "reality decrypt failed"))); - } - } - } - } - - let mut read_buf = [0u8; 8192]; - let mut tokio_buf = tokio::io::ReadBuf::new(&mut read_buf); - match Pin::new(&mut self.inner).poll_read(cx, &mut tokio_buf) { - Poll::Ready(Ok(())) => { - if tokio_buf.filled().is_empty() { - return Poll::Ready(Ok(())); - } - self.rx_buf.put_slice(tokio_buf.filled()); - } - Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), - Poll::Pending => return Poll::Pending, - } - } - } -} - -impl tokio::io::AsyncWrite for RealityStream { - fn poll_write(self: Pin<&mut Self>, cx: &mut TaskContext<'_>, buf: &[u8]) -> Poll> { - let this = self.get_mut(); - while !this.tx_buf.is_empty() { - match Pin::new(&mut this.inner).poll_write(cx, &this.tx_buf) { - Poll::Ready(Ok(n)) => this.tx_buf.advance(n), - Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), - Poll::Pending => return Poll::Pending, - } - } - - let nonce_bytes = Self::make_nonce(this.tx_nonce); - let nonce = Nonce::from_slice(&nonce_bytes); - - match this.data_key.encrypt(nonce, buf) { - Ok(ciphertext) => { - this.tx_nonce += 1; - this.tx_buf.reserve(5 + ciphertext.len()); - this.tx_buf.put_u8(0x17); - this.tx_buf.put_u16(0x0303); - this.tx_buf.put_u16(ciphertext.len() as u16); - this.tx_buf.put_slice(&ciphertext); - - match Pin::new(&mut this.inner).poll_write(cx, &this.tx_buf) { - Poll::Ready(Ok(n)) => this.tx_buf.advance(n), - Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), - Poll::Pending => {} - } - Poll::Ready(Ok(buf.len())) - } - Err(_) => Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::Other, "reality encrypt failed"))), - } - } - - fn poll_flush(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - let this = self.get_mut(); - while !this.tx_buf.is_empty() { - match Pin::new(&mut this.inner).poll_write(cx, &this.tx_buf) { - Poll::Ready(Ok(n)) => this.tx_buf.advance(n), - Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), - Poll::Pending => return Poll::Pending, - } - } - Pin::new(&mut this.inner).poll_flush(cx) - } - - fn poll_shutdown(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - let this = self.get_mut(); - while !this.tx_buf.is_empty() { - match Pin::new(&mut this.inner).poll_write(cx, &this.tx_buf) { - Poll::Ready(Ok(n)) => this.tx_buf.advance(n), - Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), - Poll::Pending => return Poll::Pending, - } - } - Pin::new(&mut this.inner).poll_shutdown(cx) - } -} - -async fn xhttp_handshake_and_loop( - mut stream: S, - target_ip: IpAddr, - sni: &str, - access_key: &[u8], - wss: bool, -) -> Result<(mpsc::Sender, Arc>>)> -where - S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static, -{ - // 1. Generate auth token: [8-byte timestamp BE] ++ [HMAC-SHA256] - let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(); - let ts_bytes = timestamp.to_be_bytes(); - use hmac::Mac; - let mut mac = ::new_from_slice(access_key).unwrap_or_else(|_| ::new_from_slice(b"").unwrap()); - mac.update(&ts_bytes); - let mac_bytes = mac.finalize().into_bytes(); - - let mut sig_bytes = Vec::with_capacity(8 + mac_bytes.len()); - sig_bytes.extend_from_slice(&ts_bytes); - sig_bytes.extend_from_slice(&mac_bytes); - - let auth_token = base64::engine::general_purpose::STANDARD_NO_PAD.encode(&sig_bytes); - - let http_host = if sni.is_empty() { target_ip.to_string() } else { sni.to_string() }; - - let req = if wss { - format!( - "GET /wss HTTP/1.1\r\n\ - Host: {}\r\n\ - Upgrade: websocket\r\n\ - Connection: upgrade\r\n\ - Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\ - Sec-WebSocket-Version: 13\r\n\ - Authorization: Bearer {}\r\n\ - \r\n", - http_host, auth_token - ) - } else { - format!( - "GET /stream HTTP/1.1\r\n\ - Host: {}\r\n\ - Authorization: Bearer {}\r\n\ - \r\n", - http_host, auth_token - ) - }; - - stream.write_all(req.as_bytes()).await?; - - // Wait for HTTP 200 OK or 101 Switching Protocols - let mut header_buf = Vec::new(); - let mut temp = [0u8; 1]; - loop { - let n = stream.read(&mut temp).await?; - if n == 0 { - anyhow::bail!("connection closed by server during handshake"); - } - header_buf.push(temp[0]); - if header_buf.ends_with(b"\r\n\r\n") { - break; - } - if header_buf.len() > 8192 { - anyhow::bail!("server response too long"); - } - } - - let resp_str = String::from_utf8_lossy(&header_buf); - if wss { - if !resp_str.starts_with("HTTP/1.1 101 ") { - anyhow::bail!("failed to switch protocols: {}", resp_str.lines().next().unwrap_or("")); - } - } else { - if !resp_str.starts_with("HTTP/1.1 200 OK") { - anyhow::bail!("server rejected stream: {}", resp_str.lines().next().unwrap_or("")); - } - } - - let (tx, mut rx) = mpsc::channel::(16384); - let (mut read_half, mut write_half) = tokio::io::split(stream); - - let writer_task = tokio::spawn(async move { - while let Some(packet) = rx.recv().await { - if wss { - let header = encode_wss_frame(&packet, true); - if write_half.write_all(&header).await.is_err() { break; } - } else { - let mut out = BytesMut::with_capacity(2 + packet.len()); - out.put_u16(packet.len() as u16); - out.put_slice(&packet); - if write_half.write_all(&out).await.is_err() { break; } - } - } - }); - - let (in_tx, in_rx) = mpsc::channel::(16384); - let in_rx_arc = Arc::new(tokio::sync::Mutex::new(in_rx)); - - let in_tx_clone = in_tx.clone(); - let reader_task = tokio::spawn(async move { - if wss { - let mut read_buf = BytesMut::with_capacity(65536); - let mut tmp = [0u8; 8192]; - loop { - match read_half.read(&mut tmp).await { - Ok(0) => break, - Ok(n) => { - read_buf.put_slice(&tmp[..n]); - loop { - match decode_wss_frame(&mut read_buf) { - WssFrameResult::Frame { payload, total_len } => { - if in_tx_clone.send(Bytes::from(payload)).await.is_err() { return; } - read_buf.advance(total_len); - } - WssFrameResult::Incomplete => break, - } - } - } - Err(_) => break, - } - } - } else { - let mut len_buf = [0u8; 2]; - loop { - if read_half.read_exact(&mut len_buf).await.is_err() { break; } - let len = u16::from_be_bytes(len_buf) as usize; - if len > 65535 { break; } - let mut data = vec![0u8; len]; - if read_half.read_exact(&mut data).await.is_err() { break; } - if in_tx_clone.send(Bytes::from(data)).await.is_err() { break; } - } - } - }); - - tokio::spawn(async move { - let _ = tokio::join!(writer_task, reader_task); - }); - - Ok((tx, in_rx_arc)) -} diff --git a/ostp-core/src/crypto/reality.rs b/ostp-core/src/crypto/reality.rs deleted file mode 100644 index 0998351..0000000 --- a/ostp-core/src/crypto/reality.rs +++ /dev/null @@ -1,279 +0,0 @@ -use bytes::{Buf, BufMut, Bytes, BytesMut}; -use chacha20poly1305::{aead::{Aead, KeyInit}, ChaCha20Poly1305, Nonce}; -use hkdf::Hkdf; -use sha2::Sha256; -use x25519_dalek::{PublicKey, StaticSecret}; -use rand::{rngs::OsRng, RngCore}; -use std::time::{SystemTime, UNIX_EPOCH}; - -const REALITY_INFO: &[u8] = b"ostp-reality-v1"; -const RECORD_HEADER_LEN: usize = 5; -const HANDSHAKE_HEADER_LEN: usize = 4; - -/// Number of TLS records sent by the server during the fake handshake phase. -/// Client must read and discard this many records before starting RealityStream. -/// Layout: 1× ServerHello (0x16) + 1× CCS (0x14) + 3× fake encrypted records (0x17) -pub const REALITY_SERVER_HANDSHAKE_RECORDS: usize = 5; - -/// Generates an X25519 keypair -pub fn generate_x25519_keypair() -> (StaticSecret, PublicKey) { - let secret = StaticSecret::random_from_rng(OsRng); - let public = PublicKey::from(&secret); - (secret, public) -} - -/// Derives the Auth Key and Data Key from the X25519 shared secret -pub fn derive_keys(shared_secret: &[u8; 32]) -> (ChaCha20Poly1305, ChaCha20Poly1305) { - let hk = Hkdf::::new(None, shared_secret); - let mut okm = [0u8; 64]; - hk.expand(REALITY_INFO, &mut okm).expect("HKDF expand failed"); - - let auth_key = ChaCha20Poly1305::new_from_slice(&okm[0..32]).unwrap(); - let data_key = ChaCha20Poly1305::new_from_slice(&okm[32..64]).unwrap(); - (auth_key, data_key) -} - -/// Creates an authenticated Session ID payload (32 bytes) -/// sid: 8 bytes, timestamp: 8 bytes. Encrypted with ChaCha20Poly1305 (16 byte tag). Total = 32 bytes. -pub fn generate_session_id(auth_aead: &ChaCha20Poly1305, sid: &[u8; 8]) -> [u8; 32] { - let ts = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); - let mut plaintext = [0u8; 16]; - plaintext[0..8].copy_from_slice(sid); - plaintext[8..16].copy_from_slice(&ts.to_be_bytes()); - - let nonce = Nonce::from_slice(&[0u8; 12]); // Fixed nonce since auth key is ephemeral per connection - let ciphertext = auth_aead.encrypt(nonce, plaintext.as_ref()).expect("encryption failed"); - - let mut session_id = [0u8; 32]; - session_id.copy_from_slice(&ciphertext); - session_id -} - -/// Verifies and decrypts the Session ID payload. Returns (sid, timestamp) -pub fn verify_session_id(auth_aead: &ChaCha20Poly1305, session_id: &[u8; 32]) -> Option<([u8; 8], u64)> { - let nonce = Nonce::from_slice(&[0u8; 12]); - let plaintext = auth_aead.decrypt(nonce, session_id.as_ref()).ok()?; - - if plaintext.len() != 16 { - return None; - } - - let mut sid = [0u8; 8]; - sid.copy_from_slice(&plaintext[0..8]); - let mut ts_bytes = [0u8; 8]; - ts_bytes.copy_from_slice(&plaintext[8..16]); - let ts = u64::from_be_bytes(ts_bytes); - - let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); - // Allow up to 60 seconds of clock drift - if ts > now + 60 || ts < now.saturating_sub(60) { - return None; // Replay protection / stale connection - } - - Some((sid, ts)) -} - -/// Builds a fake TLS 1.3 ClientHello matching Chrome's fingerprint -pub fn build_client_hello(sni: &str, session_id: &[u8; 32], c_pub: &PublicKey) -> Bytes { - let mut ext = BytesMut::new(); - - // SNI Extension - let sni_bytes = sni.as_bytes(); - ext.put_u16(0x0000); // Type: server_name - ext.put_u16((sni_bytes.len() + 5) as u16); - ext.put_u16((sni_bytes.len() + 3) as u16); // Server Name list length - ext.put_u8(0x00); // Name Type: host_name - ext.put_u16(sni_bytes.len() as u16); - ext.put_slice(sni_bytes); - - // Supported Groups - ext.put_u16(0x000a); // Type - ext.put_u16(8); // Length - ext.put_u16(6); // List length - ext.put_u16(0x001d); // x25519 - ext.put_u16(0x0017); // secp256r1 - ext.put_u16(0x0018); // secp384r1 - - // Key Share - let pub_bytes = c_pub.as_bytes(); - ext.put_u16(0x0033); // Type - ext.put_u16((pub_bytes.len() + 6) as u16); // Length - ext.put_u16((pub_bytes.len() + 4) as u16); // ClientShares length - ext.put_u16(0x001d); // Group: x25519 - ext.put_u16(pub_bytes.len() as u16); - ext.put_slice(pub_bytes); - - // Supported Versions - ext.put_u16(0x002b); // Type - ext.put_u16(5); // Length - ext.put_u8(4); // List length - ext.put_u16(0x0304); // TLS 1.3 - ext.put_u16(0x0303); // TLS 1.2 - - // ALPN - let alpn = b"\x02h2\x08http/1.1"; - ext.put_u16(0x0010); // Type - ext.put_u16((alpn.len() + 2) as u16); - ext.put_u16(alpn.len() as u16); - ext.put_slice(alpn); - - // Signature Algorithms - ext.put_u16(0x000d); // Type - ext.put_u16(10); // Length - ext.put_u16(8); // List length - ext.put_u16(0x0403); // ecdsa_secp256r1_sha256 - ext.put_u16(0x0804); // rsa_pss_rsae_sha256 - ext.put_u16(0x0401); // rsa_pkcs1_sha256 - ext.put_u16(0x0503); // ecdsa_secp384r1_sha384 - - let mut handshake = BytesMut::new(); - handshake.put_u16(0x0303); // Client Version - let mut random = [0u8; 32]; - OsRng.fill_bytes(&mut random); - handshake.put_slice(&random); // Random - - handshake.put_u8(32); // Session ID length - handshake.put_slice(session_id); // Session ID - - // Cipher Suites - handshake.put_u16(6); // Length - handshake.put_u16(0x1301); // TLS_AES_128_GCM_SHA256 - handshake.put_u16(0x1303); // TLS_CHACHA20_POLY1305_SHA256 - handshake.put_u16(0x1302); // TLS_AES_256_GCM_SHA384 - - // Compression - handshake.put_u8(1); // Length - handshake.put_u8(0); // null - - // Extensions - handshake.put_u16(ext.len() as u16); - handshake.put_slice(&ext); - - let handshake_len = handshake.len(); - - let mut record = BytesMut::new(); - record.put_u8(0x16); // Handshake - record.put_u16(0x0301); // TLS 1.0 (Compatibility) - record.put_u16((handshake_len + HANDSHAKE_HEADER_LEN) as u16); // Length - - record.put_u8(0x01); // ClientHello - record.put_u8((handshake_len >> 16) as u8); - record.put_u8((handshake_len >> 8) as u8); - record.put_u8(handshake_len as u8); - record.put_slice(&handshake); - - // Append ChangeCipherSpec for TLS 1.3 middlebox compatibility (RFC 8446 §D.4) - // This makes the flow look like: ClientHello → ServerHello → CCS → AppData - // instead of the DPI-suspicious: ClientHello → AppData directly. - let mut out = BytesMut::new(); - out.put_slice(&record); - out.put_slice(&[0x14, 0x03, 0x03, 0x00, 0x01, 0x01]); - out.freeze() -} - -pub struct ParsedClientHello { - pub sni: String, - pub session_id: [u8; 32], - pub c_pub: PublicKey, -} - -/// Parses a TLS ClientHello. Returns None if invalid or missing required fields. -pub fn parse_client_hello(mut buf: &[u8]) -> Option { - if buf.len() < RECORD_HEADER_LEN + HANDSHAKE_HEADER_LEN { - return None; - } - - // Record Header - let typ = buf.get_u8(); - if typ != 0x16 { return None; } // Not a handshake - let _version = buf.get_u16(); - let record_len = buf.get_u16() as usize; - - if buf.len() < record_len { - return None; // Incomplete record - } - - let mut payload = &buf[..record_len]; - - // Handshake Header - let hs_type = payload.get_u8(); - if hs_type != 0x01 { return None; } // Not ClientHello - let hs_len_hi = payload.get_u8() as usize; - let hs_len_mid = payload.get_u8() as usize; - let hs_len_lo = payload.get_u8() as usize; - let hs_len = (hs_len_hi << 16) | (hs_len_mid << 8) | hs_len_lo; - - if payload.len() < hs_len { return None; } - - let mut ch = &payload[..hs_len]; - let _client_version = ch.get_u16(); - if ch.len() < 32 { return None; } - ch.advance(32); // Skip Random - - let sid_len = ch.get_u8() as usize; - if sid_len != 32 || ch.len() < 32 { return None; } - - let mut session_id = [0u8; 32]; - session_id.copy_from_slice(&ch[..32]); - ch.advance(32); - - let ciphers_len = ch.get_u16() as usize; - if ch.len() < ciphers_len { return None; } - ch.advance(ciphers_len); - - let comp_len = ch.get_u8() as usize; - if ch.len() < comp_len { return None; } - ch.advance(comp_len); - - let ext_len = ch.get_u16() as usize; - if ch.len() < ext_len { return None; } - - let mut exts = &ch[..ext_len]; - - let mut parsed_sni = None; - let mut parsed_c_pub = None; - - while exts.len() >= 4 { - let ext_type = exts.get_u16(); - let ext_len = exts.get_u16() as usize; - if exts.len() < ext_len { break; } - - let mut ext_data = &exts[..ext_len]; - - if ext_type == 0x0000 { // SNI - let _list_len = ext_data.get_u16() as usize; - if ext_data.len() >= 3 { - let name_type = ext_data.get_u8(); - if name_type == 0x00 { // Hostname - let name_len = ext_data.get_u16() as usize; - if ext_data.len() >= name_len { - if let Ok(name) = std::str::from_utf8(&ext_data[..name_len]) { - parsed_sni = Some(name.to_string()); - } - } - } - } - } else if ext_type == 0x0033 { // Key Share - let _client_shares_len = ext_data.get_u16() as usize; - while ext_data.len() >= 4 { - let group = ext_data.get_u16(); - let key_ex_len = ext_data.get_u16() as usize; - if ext_data.len() < key_ex_len { break; } - - if group == 0x001d && key_ex_len == 32 { // X25519 - let mut pub_bytes = [0u8; 32]; - pub_bytes.copy_from_slice(&ext_data[..32]); - parsed_c_pub = Some(PublicKey::from(pub_bytes)); - } - ext_data.advance(key_ex_len); - } - } - - exts.advance(ext_len); - } - - match (parsed_sni, parsed_c_pub) { - (Some(sni), Some(c_pub)) => Some(ParsedClientHello { sni, session_id, c_pub }), - _ => None, - } -} diff --git a/ostp-core/src/framing/mod.rs b/ostp-core/src/framing/mod.rs index 5cc44af..24eaed0 100644 --- a/ostp-core/src/framing/mod.rs +++ b/ostp-core/src/framing/mod.rs @@ -1,7 +1,5 @@ pub mod frame; pub mod padding; -pub mod wss; pub use frame::{FrameHeader, FrameKind, FramedPacket}; pub use padding::{AdaptivePadder, PaddingStrategy, TrafficProfile}; -pub use wss::{encode_wss_frame, decode_wss_frame, WssFrameResult}; diff --git a/ostp-core/src/framing/wss.rs b/ostp-core/src/framing/wss.rs deleted file mode 100644 index 19d0d77..0000000 --- a/ostp-core/src/framing/wss.rs +++ /dev/null @@ -1,74 +0,0 @@ -use rand::RngCore; - -pub enum WssFrameResult { - Incomplete, - Frame { payload: Vec, total_len: usize }, -} - -pub fn encode_wss_frame(payload: &[u8], masked: bool) -> Vec { - let len = payload.len(); - let mut header = Vec::with_capacity(14 + len); - header.push(0x82); // FIN + Binary - - let mask_bit = if masked { 0x80 } else { 0x00 }; - - if len <= 125 { - header.push(mask_bit | (len as u8)); - } else if len <= 65535 { - header.push(mask_bit | 126); - header.extend_from_slice(&(len as u16).to_be_bytes()); - } else { - header.push(mask_bit | 127); - header.extend_from_slice(&(len as u64).to_be_bytes()); - } - - if masked { - let mut mask = [0u8; 4]; - rand::thread_rng().fill_bytes(&mut mask); - header.extend_from_slice(&mask); - - for (i, &b) in payload.iter().enumerate() { - header.push(b ^ mask[i % 4]); - } - } else { - header.extend_from_slice(payload); - } - - header -} - -pub fn decode_wss_frame(buffer: &[u8]) -> WssFrameResult { - if buffer.len() < 2 { - return WssFrameResult::Incomplete; - } - let is_masked = (buffer[1] & 0x80) != 0; - let payload_len_7 = (buffer[1] & 0x7F) as usize; - - let (header_len, payload_len) = if payload_len_7 == 126 { - if buffer.len() < 4 { return WssFrameResult::Incomplete; } - (4, u16::from_be_bytes([buffer[2], buffer[3]]) as usize) - } else if payload_len_7 == 127 { - if buffer.len() < 10 { return WssFrameResult::Incomplete; } - (10, u64::from_be_bytes([buffer[2], buffer[3], buffer[4], buffer[5], buffer[6], buffer[7], buffer[8], buffer[9]]) as usize) - } else { - (2, payload_len_7) - }; - - let mask_offset = header_len; - let full_header_len = header_len + if is_masked { 4 } else { 0 }; - let total_frame_len = full_header_len + payload_len; - - if buffer.len() < total_frame_len { - return WssFrameResult::Incomplete; - } - - let mut payload = buffer[full_header_len..total_frame_len].to_vec(); - if is_masked { - let mask = [buffer[mask_offset], buffer[mask_offset+1], buffer[mask_offset+2], buffer[mask_offset+3]]; - for (i, b) in payload.iter_mut().enumerate() { - *b ^= mask[i % 4]; - } - } - - WssFrameResult::Frame { payload, total_len: total_frame_len } -} diff --git a/ostp-flutter/lib/ui/home_screen.dart b/ostp-flutter/lib/ui/home_screen.dart index 3d880bc..d0729f8 100644 --- a/ostp-flutter/lib/ui/home_screen.dart +++ b/ostp-flutter/lib/ui/home_screen.dart @@ -84,7 +84,6 @@ class _HomeScreenState extends State with TickerProviderStateMixin { final debugMode = widget.prefs.getBool('debug_mode') ?? false; final transportMode = widget.prefs.getString('transport_mode') ?? 'udp'; final stealthSni = widget.prefs.getString('stealth_sni') ?? 'vk.com'; - final wss = widget.prefs.getBool('wss') ?? false; final mtu = widget.prefs.getString('mtu') ?? '1140'; final muxEnabled = widget.prefs.getBool('mux_enabled') ?? false; final muxSessions = widget.prefs.getString('mux_sessions') ?? '2'; @@ -113,7 +112,6 @@ class _HomeScreenState extends State with TickerProviderStateMixin { "transport": { "mode": transportMode, "stealth_sni": stealthSni, - "wss": wss, }, "multiplex": { "enabled": muxEnabled, @@ -173,7 +171,6 @@ class _HomeScreenState extends State with TickerProviderStateMixin { final debugMode = widget.prefs.getBool('debug_mode') ?? false; final transportMode = widget.prefs.getString('transport_mode') ?? 'udp'; final stealthSni = widget.prefs.getString('stealth_sni') ?? 'vk.com'; - final wss = widget.prefs.getBool('wss') ?? false; final mtu = widget.prefs.getString('mtu') ?? '1140'; final muxEnabled = widget.prefs.getBool('mux_enabled') ?? false; final muxSessions = widget.prefs.getString('mux_sessions') ?? '2'; @@ -201,7 +198,6 @@ class _HomeScreenState extends State with TickerProviderStateMixin { "transport": { "mode": transportMode, "stealth_sni": stealthSni, - "wss": wss, }, "multiplex": { "enabled": muxEnabled, @@ -296,10 +292,8 @@ class _HomeScreenState extends State with TickerProviderStateMixin { Future _runAutoMode() async { final mtus = [1500, 1350, 1280, 1140]; final modes = [ - {'t': 'udp', 'w': false, 'r': false}, - {'t': 'uot', 'w': false, 'r': false}, - {'t': 'uot', 'w': true, 'r': false}, - {'t': 'uot', 'w': false, 'r': true}, + {'t': 'udp'}, + {'t': 'uot'}, ]; if (_serverAddr.isEmpty || _accessKey.isEmpty) { @@ -313,13 +307,12 @@ class _HomeScreenState extends State with TickerProviderStateMixin { for (var mtu in mtus) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Testing: ${mode['t']} | WSS: ${mode['w']} | XTLS: ${mode['r']} | MTU: $mtu'), duration: const Duration(seconds: 2)), + SnackBar(content: Text('Testing: ${mode['t']} | MTU: $mtu'), duration: const Duration(seconds: 2)), ); // Update prefs await widget.prefs.setString('mtu', mtu.toString()); await widget.prefs.setString('transport_mode', mode['t'] as String); - await widget.prefs.setBool('wss', mode['w'] as bool); _updateLatestConfigJson(); setState(() { diff --git a/ostp-flutter/lib/ui/settings_screen.dart b/ostp-flutter/lib/ui/settings_screen.dart index ee1f3a2..f26ca1b 100644 --- a/ostp-flutter/lib/ui/settings_screen.dart +++ b/ostp-flutter/lib/ui/settings_screen.dart @@ -33,12 +33,9 @@ class _SettingsScreenState extends State { late TextEditingController _ipsCtrl; late TextEditingController _processesCtrl; late TextEditingController _stealthSniCtrl; - late TextEditingController _pbkCtrl; - late TextEditingController _sidCtrl; bool _obscureKey = true; bool _debugMode = false; - bool _wss = false; String _transportMode = 'udp'; // 'udp' | 'uot' String _tunStack = 'ostp'; // 'system' | 'ostp' bool _muxEnabled = false; @@ -58,9 +55,6 @@ class _SettingsScreenState extends State { _ipsCtrl = TextEditingController(text: widget.prefs.getString('ex_ips') ?? ''); _processesCtrl = TextEditingController(text: widget.prefs.getString('ex_processes') ?? ''); _stealthSniCtrl = TextEditingController(text: widget.prefs.getString('stealth_sni') ?? ''); - _pbkCtrl = TextEditingController(text: widget.prefs.getString('pbk') ?? ''); - _sidCtrl = TextEditingController(text: widget.prefs.getString('sid') ?? ''); - _wss = widget.prefs.getBool('wss') ?? false; _transportMode = widget.prefs.getString('transport_mode') ?? 'udp'; _tunStack = widget.prefs.getString('tun_stack') ?? 'ostp'; _debugMode = widget.prefs.getBool('debug_mode') ?? false; @@ -81,8 +75,6 @@ class _SettingsScreenState extends State { _ipsCtrl.dispose(); _processesCtrl.dispose(); _stealthSniCtrl.dispose(); - _pbkCtrl.dispose(); - _sidCtrl.dispose(); _muxSessionsCtrl.dispose(); super.dispose(); } @@ -97,12 +89,9 @@ class _SettingsScreenState extends State { widget.prefs.setString('ex_ips', _ipsCtrl.text.trim()); widget.prefs.setString('ex_processes', _processesCtrl.text.trim()); widget.prefs.setBool('debug_mode', _debugMode); - widget.prefs.setBool('wss', _wss); widget.prefs.setString('transport_mode', _transportMode); widget.prefs.setString('tun_stack', _tunStack); widget.prefs.setString('stealth_sni', _stealthSniCtrl.text.trim()); - widget.prefs.setString('pbk', _pbkCtrl.text.trim()); - widget.prefs.setString('sid', _sidCtrl.text.trim()); widget.prefs.setBool('mux_enabled', _muxEnabled); widget.prefs.setString('mux_sessions', _muxSessionsCtrl.text.trim()); } @@ -237,9 +226,6 @@ class _SettingsScreenState extends State { _serverCtrl.text = host; _keyCtrl.text = key; _stealthSniCtrl.text = uri.queryParameters['sni'] ?? ''; - _pbkCtrl.text = uri.queryParameters['pbk'] ?? ''; - _sidCtrl.text = uri.queryParameters['sid'] ?? ''; - _wss = uri.queryParameters['wss'] == 'true'; final type = uri.queryParameters['type'] ?? 'udp'; _transportMode = type == 'tcp' || type == 'http' ? 'uot' : 'udp'; _importCtrl.clear(); @@ -324,12 +310,6 @@ class _SettingsScreenState extends State { ), ), const SizedBox(height: 16), - _buildToggle('WebSocket (WSS)', 'Инкапсулировать транспорт в RFC 6455 (для строгого DPI)', _wss, (val) { - setState(() { - _wss = val; - }); - }), - const SizedBox(height: 16), // Stealth parameters AnimatedCrossFade( @@ -555,15 +535,6 @@ class _SettingsScreenState extends State { if (_stealthSniCtrl.text.trim().isNotEmpty) { queryParams.add('sni=${Uri.encodeComponent(_stealthSniCtrl.text.trim())}'); } - if (_pbkCtrl.text.trim().isNotEmpty) { - queryParams.add('pbk=${Uri.encodeComponent(_pbkCtrl.text.trim())}'); - } - if (_sidCtrl.text.trim().isNotEmpty) { - queryParams.add('sid=${Uri.encodeComponent(_sidCtrl.text.trim())}'); - } - if (_wss) { - queryParams.add('wss=true'); - } if (_transportMode != 'udp') { queryParams.add('type=$_transportMode'); } diff --git a/ostp-gui/src-tauri/src/lib.rs b/ostp-gui/src-tauri/src/lib.rs index 217a21a..e1b3302 100644 --- a/ostp-gui/src-tauri/src/lib.rs +++ b/ostp-gui/src-tauri/src/lib.rs @@ -57,7 +57,6 @@ struct TunConfig { struct TransportConfigRaw { mode: Option, stealth_sni: Option, - wss: Option, } #[derive(Debug, Deserialize, Serialize, Clone)] @@ -164,7 +163,6 @@ fn map_to_client_config(raw: &ClientConfigRaw, mode: &str) -> ostp_client::confi 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(), @@ -322,7 +320,7 @@ async fn get_config() -> Result { "_comment_socks5_bind": "The local port where the system/browser should connect (HTTP/SOCKS5)", "socks5_bind": "127.0.0.1:1088", - "_comment_tun": "Virtual network adapter settings (requires tun2socks.exe to be present)", + "_comment_tun": "Virtual network adapter settings (native OSTP TUN via wintun.dll)", "tun": { "enable": false, "wintun_path": "./wintun.dll", diff --git a/ostp-gui/src/index.html b/ostp-gui/src/index.html index bfece76..c86f4cd 100644 --- a/ostp-gui/src/index.html +++ b/ostp-gui/src/index.html @@ -206,22 +206,6 @@ -
-
- WebSocket (WSS) - Use RFC 6455 framing for strict DPI bypass -
- -
- -
-
-
diff --git a/ostp-gui/src/main.js b/ostp-gui/src/main.js index 7c15650..5c8dc9d 100644 --- a/ostp-gui/src/main.js +++ b/ostp-gui/src/main.js @@ -46,7 +46,6 @@ const inDns = $('in-dns'); const groupCustomDns = $('group-custom-dns'); const inTransport = $('in-transport'); const inSni = $('in-stealth-sni'); -const inWss = $('in-wss'); const inMtu = $('in-mtu'); const inTun = $('in-tun-mode'); const inKillSwitch = $('in-kill-switch'); @@ -336,7 +335,6 @@ async function loadConfigIntoForm() { inSocks.value = c.socks5_bind || '127.0.0.1:1088'; inTransport.value = c.transport?.mode || 'udp'; inSni.value = c.transport?.stealth_sni || ''; - inWss.checked = !!c.transport?.wss; inMtu.value = c.mtu || ''; inTun.checked = !!c.tun?.enable; @@ -400,7 +398,6 @@ async function handleSave(silent = false) { rawConfig.transport = rawConfig.transport || {}; rawConfig.transport.mode = inTransport.value; rawConfig.transport.stealth_sni = inSni.value.trim() || undefined; - rawConfig.transport.wss = inWss.checked; const mtuStr = inMtu.value.trim(); if (mtuStr) rawConfig.mtu = parseInt(mtuStr, 10); @@ -534,22 +531,19 @@ window.addEventListener('DOMContentLoaded', async () => { try { const mtus = [1500, 1350, 1280]; const modes = [ - { t: 'udp', w: false, r: false }, - { t: 'uot', w: false, r: false }, - { t: 'uot', w: true, r: false }, - { t: 'uot', w: false, r: true } + { t: 'udp' }, + { t: 'uot' } ]; for (let mode of modes) { for (let mtu of mtus) { - showToast(`Testing: ${mode.t} | WSS: ${mode.w} | XTLS: ${mode.r} | MTU: ${mtu}`); - + showToast(`Testing: ${mode.t} | MTU: ${mtu}`); + rawConfig.ostp = rawConfig.ostp || {}; rawConfig.ostp.mtu = mtu; rawConfig.transport = rawConfig.transport || {}; rawConfig.transport.mode = mode.t; - rawConfig.transport.wss = mode.w; - + await invoke('save_config', { jsonContent: JSON.stringify(rawConfig, null, 2) }); diff --git a/ostp/Cargo.toml b/ostp/Cargo.toml index 9334264..139d29e 100644 --- a/ostp/Cargo.toml +++ b/ostp/Cargo.toml @@ -18,5 +18,5 @@ rand.workspace = true url = "2.5" tracing.workspace = true tracing-subscriber = { version = "0.3", features = ["env-filter"] } -ostp-core = { version = "0.2.68", path = "../ostp-core" } +ostp-core = { path = "../ostp-core" } colored = "2.1" diff --git a/ostp/src/main.rs b/ostp/src/main.rs index f5e268d..91a3511 100644 --- a/ostp/src/main.rs +++ b/ostp/src/main.rs @@ -85,7 +85,6 @@ fn parse_ostp_link(link: &str) -> Result { let mut transport_mode = String::from("udp"); let mut tun_enabled = false; let mut tun_dns = None; - let mut wss_enabled = false; for (k, v) in parsed.query_pairs() { match &*k { @@ -93,7 +92,6 @@ fn parse_ostp_link(link: &str) -> Result { "type" => transport_mode = v.into_owned(), "tun" => tun_enabled = v == "true", "dns" => tun_dns = Some(v.into_owned()), - "wss" => wss_enabled = v == "true", _ => {} } } @@ -105,7 +103,6 @@ fn parse_ostp_link(link: &str) -> Result { transport: Some(TransportConfigRaw { mode: Some(transport_mode), stealth_sni: Some(sni.clone()), - wss: Some(wss_enabled), }), socks5_bind: Some("127.0.0.1:1088".to_string()), tun: Some(TunConfig { @@ -320,7 +317,6 @@ struct ClientConfig { struct TransportConfigRaw { mode: Option, stealth_sni: Option, - wss: Option, } #[derive(Debug, Deserialize, Serialize, Clone)] @@ -699,8 +695,7 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> { }, "transport": { "mode": transport_mode, - "stealth_sni": "www.microsoft.com", - "wss": false + "stealth_sni": "www.microsoft.com" }, "mux": { "enabled": mux_enable, @@ -846,12 +841,8 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> { let digest: [u8; 32] = { use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; - // simple SHA-256 via sha2 would be ideal; we reuse existing pattern from the old script - // fallback: store plaintext-keyed sha256 if sha2 crate not available - // The ostp binary already uses sha256 for reality keys — let's do it properly via python fallback - // Actually: ostp-core likely has sha2 in tree. Let's use hex output. - // We'll use std's hash as placeholder and document; sha2 is not in ostp/Cargo.toml directly. - // Use sha2 via ostp_core if available, else hex of std hasher. + // Panel password hashing. sha2 is not a direct dep of ostp/Cargo.toml, + // so we use std's hasher as a placeholder digest here. let mut h = DefaultHasher::new(); password.hash(&mut h); let v = h.finish(); @@ -1348,8 +1339,7 @@ async fn run_app() -> Result<()> { // Transport Mode: "udp" (default WebRTC masquerade) or "uot" (TCP UoT) "transport": {{ "mode": "udp", - "stealth_sni": "www.microsoft.com", - "wss": false + "stealth_sni": "www.microsoft.com" }}, "mux": {{ @@ -1633,7 +1623,6 @@ async fn run_client_directly(client_cfg: ClientConfig) -> Result<()> { transport: ostp_client::config::TransportConfig { mode: client_cfg.transport.as_ref().and_then(|t| t.mode.clone()).unwrap_or_else(|| "udp".to_string()), stealth_sni: client_cfg.transport.as_ref().and_then(|t| t.stealth_sni.clone()).unwrap_or_else(|| "microsoft.com".to_string()), - wss: client_cfg.transport.as_ref().and_then(|t| t.wss).unwrap_or(false), }, dns_server: client_cfg.tun.as_ref().and_then(|t| t.dns.clone()), kill_switch: client_cfg.tun.as_ref().and_then(|t| t.kill_switch).unwrap_or(false),