diff --git a/.github/workflows/clients.yml b/.github/workflows/clients.yml new file mode 100644 index 0000000..6a913f0 --- /dev/null +++ b/.github/workflows/clients.yml @@ -0,0 +1,110 @@ +name: Build GUI Clients + +on: + workflow_dispatch: + push: + branches: [ "main" ] + paths: + - "ostp-gui/**" + - "ostp-flutter/**" + +permissions: + contents: write + +jobs: + build-windows-gui: + name: Build Windows Client (Tauri) + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-pc-windows-msvc + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: cargo-windows-gui-${{ hashFiles('**/Cargo.lock') }} + + - name: Download wintun and tun2socks + shell: pwsh + run: | + $ProgressPreference = 'SilentlyContinue' + + # Download tun2socks + New-Item -ItemType Directory -Force -Path "t2s_tmp" + Invoke-WebRequest -Uri "https://github.com/xjasonlyu/tun2socks/releases/download/v2.6.0/tun2socks-windows-amd64.zip" -OutFile "t2s_tmp/t2s.zip" + Expand-Archive "t2s_tmp/t2s.zip" -DestinationPath "t2s_tmp/ext" -Force + Get-ChildItem "t2s_tmp/ext" -Filter "*.exe" -Recurse | Select-Object -First 1 | Copy-Item -Destination "t2s_tmp/tun2socks-windows-amd64.exe" -Force + + # Download wintun + New-Item -ItemType Directory -Force -Path "target/release" + Invoke-WebRequest -Uri "https://www.wintun.net/builds/wintun-0.14.1.zip" -OutFile "target/release/wt.zip" + Expand-Archive "target/release/wt.zip" -DestinationPath "target/release/wt_tmp" -Force + Get-ChildItem "target/release/wt_tmp" -Filter "wintun.dll" -Recurse | Where-Object { $_.FullName -match 'bin[\\/]amd64[\\/]' } | Copy-Item -Destination "target/release/wintun.dll" -Force + + - name: Build Tauri App + working-directory: ostp-gui + run: | + npm install + npm run build:installer + + - name: Upload Windows Installer + uses: actions/upload-artifact@v4 + with: + name: ostp-windows-installer + path: ostp-gui/src-tauri/target/release/bundle/nsis/*.exe + + build-android: + name: Build Android Client (Flutter) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '17' + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.19.0' + channel: 'stable' + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-linux-android + + - name: Setup Android NDK + uses: nttld/setup-ndk@v1 + with: + ndk-version: r26b + + - name: Install cargo-ndk + run: cargo install cargo-ndk + + - name: Build Android APK + shell: pwsh + working-directory: ostp-flutter + run: ./build.ps1 + + - name: Upload Android APK + uses: actions/upload-artifact@v4 + with: + name: ostp-android-apk + path: ostp-flutter/ostp-client-release.apk diff --git a/.ostp_public_ip b/.ostp_public_ip new file mode 100644 index 0000000..e56ea71 --- /dev/null +++ b/.ostp_public_ip @@ -0,0 +1 @@ +127.0.0.1 \ No newline at end of file diff --git a/ostp-client/src/bridge.rs b/ostp-client/src/bridge.rs index e2b3ffd..a29b799 100644 --- a/ostp-client/src/bridge.rs +++ b/ostp-client/src/bridge.rs @@ -39,21 +39,8 @@ pub struct BridgeMetrics { pub rtt_ms: portable_atomic::AtomicU32, } -async fn send_datagram(socket: &crate::transport::Transport, frame: &Bytes, webrtc_masquerade: bool) -> std::io::Result { - if webrtc_masquerade { - let mut out = bytes::BytesMut::with_capacity(12 + frame.len()); - // Fake SRTP Header: - // [0] 0x80 (Version 2) - // [1] 0x60 (Payload Type 96 - dynamic video) - // [2..3] Sequence number (dummy 0x1234) - // [4..7] Timestamp (dummy) - // [8..11] SSRC (dummy) - out.extend_from_slice(&[0x80, 0x60, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44]); - out.extend_from_slice(frame); - socket.send(&out.freeze()).await - } else { - socket.send(frame).await - } +async fn send_datagram(socket: &crate::transport::Transport, frame: &Bytes, _webrtc_masquerade: bool) -> std::io::Result { + socket.send(frame).await } struct SessionState { @@ -276,17 +263,13 @@ impl Bridge { let session_index = sessions.len(); let socket_clone = sock.clone(); let udp_tx_clone = udp_tx.clone(); - let is_webrtc = self.transport_mode == "udp" ; + tokio::spawn(async move { let mut buf = vec![0_u8; 65535]; loop { match socket_clone.recv(&mut buf).await { Ok(n) => { - let inbound = if is_webrtc && n >= 12 && buf[0] == 0x80 { - Bytes::copy_from_slice(&buf[12..n]) - } else { - Bytes::copy_from_slice(&buf[..n]) - }; + let inbound = Bytes::copy_from_slice(&buf[..n]); if udp_tx_clone.send((session_index, inbound)).await.is_err() { break; } @@ -366,17 +349,13 @@ impl Bridge { let session_index = new_sessions.len(); let socket_clone = sock.clone(); let udp_tx_clone = udp_tx.clone(); - let is_webrtc = self.transport_mode == "udp" ; + tokio::spawn(async move { let mut buf = vec![0_u8; 65535]; loop { match socket_clone.recv(&mut buf).await { Ok(n) => { - let inbound = if is_webrtc && n >= 12 && buf[0] == 0x80 { - Bytes::copy_from_slice(&buf[12..n]) - } else { - Bytes::copy_from_slice(&buf[..n]) - }; + let inbound = Bytes::copy_from_slice(&buf[..n]); if udp_tx_clone.send((session_index, inbound)).await.is_err() { break; } } Err(e) => { @@ -477,17 +456,13 @@ impl Bridge { let session_index = new_sessions.len(); let socket_clone = sock.clone(); let udp_tx_clone = udp_tx.clone(); - let is_webrtc = self.transport_mode == "udp" ; + tokio::spawn(async move { let mut buf = vec![0_u8; 65535]; loop { match socket_clone.recv(&mut buf).await { Ok(n) => { - let inbound = if is_webrtc && n >= 12 && buf[0] == 0x80 { - Bytes::copy_from_slice(&buf[12..n]) - } else { - Bytes::copy_from_slice(&buf[..n]) - }; + let inbound = Bytes::copy_from_slice(&buf[..n]); if udp_tx_clone.send((session_index, inbound)).await.is_err() { break; } @@ -885,11 +860,7 @@ impl Bridge { self.metrics.bytes_recv.fetch_add(size as u64, Ordering::Relaxed); tracing::info!("Handshake response received: {} bytes", size); - let inbound = if (self.transport_mode == "udp") && size >= 12 && buf[0] == 0x80 { - Bytes::copy_from_slice(&buf[12..size]) - } else { - Bytes::copy_from_slice(&buf[..size]) - }; + let inbound = Bytes::copy_from_slice(&buf[..size]); machine.on_event(OstpEvent::Inbound(inbound))?; let rtt_ms = start.elapsed().as_secs_f64() * 1000.0; tracing::info!("Handshake complete: session={:#010x} rtt={:.1}ms", session_id, rtt_ms); @@ -910,7 +881,7 @@ impl Bridge { self.transport_mode = cfg.transport.mode.clone(); self.stealth_sni = cfg.transport.stealth_sni.clone(); self.stealth_port = cfg.transport.stealth_port; - self.reality_enabled = !cfg.reality.pbk.is_empty(); + self.reality_enabled = cfg.reality.enabled; } async fn try_connect_transport( diff --git a/ostp-client/src/config.rs b/ostp-client/src/config.rs index 4677998..599005b 100644 --- a/ostp-client/src/config.rs +++ b/ostp-client/src/config.rs @@ -97,6 +97,8 @@ impl Default for TransportConfig { #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct RealityConfig { + #[serde(default)] + pub enabled: bool, #[serde(default)] pub sni: String, #[serde(default)] @@ -207,6 +209,7 @@ struct RawMuxSection { #[derive(Debug, Deserialize)] struct RawRealitySection { + enabled: Option, sni: Option, fp: Option, pbk: Option, @@ -260,6 +263,7 @@ impl ClientConfig { connect_timeout_ms: 15000, }, reality: RealityConfig { + enabled: raw.reality.as_ref().and_then(|t| t.enabled).unwrap_or(false), sni: raw.reality.as_ref().and_then(|t| t.sni.clone()).unwrap_or_default(), fp: raw.reality.as_ref().and_then(|t| t.fp.clone()).unwrap_or_default(), pbk: raw.reality.as_ref().and_then(|t| t.pbk.clone()).unwrap_or_default(), diff --git a/ostp-client/src/tunnel/native_handler.rs b/ostp-client/src/tunnel/native_handler.rs index 1e378e4..6ab677b 100644 --- a/ostp-client/src/tunnel/native_handler.rs +++ b/ostp-client/src/tunnel/native_handler.rs @@ -180,11 +180,18 @@ pub async fn run_native_tunnel( let mut rep = [0u8; 10]; if socks.read_exact(&mut rep).await.is_err() || rep[1] != 0 { return; } - if socks.write_all(&payload).await.is_ok() { - let mut response_buf = [0u8; 4096]; - if let Ok(n) = socks.read(&mut response_buf).await { - if n > 0 { - let _ = tx_clone.lock().await.send((response_buf[..n].to_vec(), dst, src)).await; + let len = payload.len() as u16; + let mut dns_req = Vec::with_capacity(2 + payload.len()); + dns_req.extend_from_slice(&len.to_be_bytes()); + dns_req.extend_from_slice(&payload); + + if socks.write_all(&dns_req).await.is_ok() { + let mut len_buf = [0u8; 2]; + if socks.read_exact(&mut len_buf).await.is_ok() { + let resp_len = u16::from_be_bytes(len_buf) as usize; + let mut response_buf = vec![0u8; resp_len]; + if socks.read_exact(&mut response_buf).await.is_ok() { + let _ = tx_clone.lock().await.send((response_buf, dst, src)).await; } } } @@ -428,11 +435,18 @@ pub async fn run_native_tunnel_from_fd( let mut rep = [0u8; 10]; if socks.read_exact(&mut rep).await.is_err() || rep[1] != 0 { return; } - if socks.write_all(&payload).await.is_ok() { - let mut response_buf = [0u8; 4096]; - if let Ok(n) = socks.read(&mut response_buf).await { - if n > 0 { - let _ = tx_clone.lock().await.send((response_buf[..n].to_vec(), dst, src)).await; + let len = payload.len() as u16; + let mut dns_req = Vec::with_capacity(2 + payload.len()); + dns_req.extend_from_slice(&len.to_be_bytes()); + dns_req.extend_from_slice(&payload); + + if socks.write_all(&dns_req).await.is_ok() { + let mut len_buf = [0u8; 2]; + if socks.read_exact(&mut len_buf).await.is_ok() { + let resp_len = u16::from_be_bytes(len_buf) as usize; + let mut response_buf = vec![0u8; resp_len]; + if socks.read_exact(&mut response_buf).await.is_ok() { + let _ = tx_clone.lock().await.send((response_buf, dst, src)).await; } } } diff --git a/ostp-server/src/lib.rs b/ostp-server/src/lib.rs index 2c57726..fc017f2 100644 --- a/ostp-server/src/lib.rs +++ b/ostp-server/src/lib.rs @@ -375,11 +375,7 @@ async fn run_server_loop( loop { match sock_clone.recv_from(&mut buf).await { Ok((size, peer)) => { - let packet = if size >= 12 && buf[0] == 0x80 { - Bytes::copy_from_slice(&buf[12..size]) - } else { - Bytes::copy_from_slice(&buf[..size]) - }; + let packet = Bytes::copy_from_slice(&buf[..size]); if tx.send((packet, peer)).await.is_err() { break; } @@ -529,10 +525,7 @@ async fn run_server_loop( } } if !sent_tcp { - let mut out = bytes::BytesMut::with_capacity(12 + resp.len()); - out.extend_from_slice(&[0x80, 0x60, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44]); - out.extend_from_slice(&resp); - let _ = socket.send_to(&out.freeze(), peer_addr).await?; + let _ = socket.send_to(&resp, peer_addr).await?; } let _ = ui_event_tx.send(UiEvent::Tx { peer: peer_ip, bytes: resp_len }); } @@ -617,10 +610,7 @@ async fn run_server_loop( } } if !sent_tcp { - let mut out = bytes::BytesMut::with_capacity(12 + frame.len()); - out.extend_from_slice(&[0x80, 0x60, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44]); - out.extend_from_slice(&frame); - let _ = socket.send_to(&out.freeze(), peer_addr).await?; + let _ = socket.send_to(&frame, peer_addr).await?; } } for sid in dropped_sessions { diff --git a/ostp/src/main.rs b/ostp/src/main.rs index 0a0cbe0..f673600 100644 --- a/ostp/src/main.rs +++ b/ostp/src/main.rs @@ -70,6 +70,8 @@ fn parse_ostp_link(link: &str) -> Result { let mut sid = String::new(); let mut spx = String::new(); let mut transport_mode = String::from("udp"); + let mut tun_enabled = false; + let mut tun_dns = None; for (k, v) in parsed.query_pairs() { match k.as_ref() { @@ -79,6 +81,8 @@ fn parse_ostp_link(link: &str) -> Result { "sid" => sid = v.into_owned(), "spx" => spx = v.into_owned(), "type" => transport_mode = v.into_owned(), + "tun" => tun_enabled = v == "true", + "dns" => tun_dns = Some(v.into_owned()), _ => {} } } @@ -94,12 +98,13 @@ fn parse_ostp_link(link: &str) -> Result { }), socks5_bind: Some("127.0.0.1:1088".to_string()), tun: Some(TunConfig { - enable: false, + enable: tun_enabled, wintun_path: Some("./wintun.dll".to_string()), ipv4_address: Some("10.1.0.2/24".to_string()), - dns: None, + dns: tun_dns, }), reality: Some(RealityConfigRaw { + enabled: true, sni, fp, pbk, @@ -338,6 +343,8 @@ struct TunConfig { #[derive(Debug, Deserialize, Serialize, Clone)] struct RealityConfigRaw { + #[serde(default)] + enabled: bool, sni: String, fp: String, pbk: String, @@ -601,8 +608,8 @@ async fn run_app() -> Result<()> { if let Some(ref mode_str) = args.init { let is_server = mode_str == "server"; let key = generate_secure_key("hex"); + let (priv_key, pub_key, sid) = generate_reality_keys(); let content = if is_server { - let (priv_key, pub_key, sid) = generate_reality_keys(); format!(r#"{{ // OSTP Server Configuration "mode": "server", @@ -708,11 +715,12 @@ async fn run_app() -> Result<()> { // Reality (XTLS) / WebRTC Masquerade parameters "reality": {{ - "dest": "www.microsoft.com:443", - "private_key": "", - "pbk": "", - "sid": "", - "sni_list": ["www.microsoft.com"] + "enabled": false, + "sni": "www.microsoft.com", + "fp": "chrome", + "pbk": "{}", + "sid": "{}", + "spx": "/" }}, // Transport Mode: "udp" (default WebRTC masquerade) or "uot" (TCP XTLS-Reality) @@ -727,7 +735,7 @@ async fn run_app() -> Result<()> { "sessions": 1 }}, "debug": false -}}"#, key) +}}"#, key, pub_key, sid) }; if let Some(parent) = args.config.parent() { if !parent.as_os_str().is_empty() { @@ -1070,6 +1078,7 @@ async fn run_client_directly(client_cfg: ClientConfig) -> Result<()> { connect_timeout_ms: 5000, }, reality: ostp_client::config::RealityConfig { + enabled: reality_cfg.map(|t| t.enabled).unwrap_or(false), sni: reality_cfg.map(|t| t.sni.clone()).unwrap_or_default(), fp: reality_cfg.map(|t| t.fp.clone()).unwrap_or_default(), pbk: reality_cfg.map(|t| t.pbk.clone()).unwrap_or_default(), diff --git a/server.json b/server.json new file mode 100644 index 0000000..7a6b8f3 --- /dev/null +++ b/server.json @@ -0,0 +1,61 @@ +{ + // OSTP Server Configuration + "mode": "server", + "log_level": "info", + + // The address and port the server listens on for incoming OSTP connections. + "listen": "0.0.0.0:50000", + + // List of valid keys. Clients must use one of these to connect. + "access_keys": [ + "a1d8795a93553c08b4e89b017a16ca52" + ], + + // Optional proxy for outbound traffic. + "outbound": { + "enabled": false, + "protocol": "socks5", + "address": "127.0.0.1", + "port": 9050, + // default_action: 'proxy' (all through proxy) or 'direct' (bypass proxy by default). + "default_action": "proxy", + "rules": [ + { + "domain_suffix": [".onion"], + "action": "proxy" + } + ] + }, + + // Web control panel & Management API + "api": { + "enabled": false, + "bind": "0.0.0.0:9090", + // Static API token for Relay servers (optional) + "token": "", + // Secret URL path to hide panel from scanners (e.g. "mySecret123") + "webpath": "", + // Login credentials for web panel (password stored as SHA256 hash) + "username": "", + "password_hash": "" + }, + + // Fallback TCP proxy: unrecognized connections are proxied to a web server (anti-DPI). + "fallback": { + "enabled": false, + "listen": "0.0.0.0:443", + // Target web server (e.g., local nginx or caddy) + "target": "127.0.0.1:8080" + }, + + // Reality (XTLS) / UoT Masquerade parameters + "reality": { + "enabled": false, + "dest": "www.microsoft.com:443", + "private_key": "6FVg53jUBTt-dJ52F1Zu1RBCcW1gr9K84WdynBb7i80", + "pbk": "c9QjERoaqFGoKBd-9ZpNzj51E8B93fcnEQT_cohEk2E", + "sid": "960223edfa174fc5", + "sni_list": ["www.microsoft.com"] + }, + "debug": false +} \ No newline at end of file