Fix STUN bug, improve DNS in TUN, fix config gen, add GHA for clients

This commit is contained in:
ospab 2026-05-28 14:39:42 +03:00
parent 543e36e60e
commit 19f2c36400
8 changed files with 231 additions and 71 deletions

110
.github/workflows/clients.yml vendored Normal file
View File

@ -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

1
.ostp_public_ip Normal file
View File

@ -0,0 +1 @@
127.0.0.1

View File

@ -39,21 +39,8 @@ pub struct BridgeMetrics {
pub rtt_ms: portable_atomic::AtomicU32, pub rtt_ms: portable_atomic::AtomicU32,
} }
async fn send_datagram(socket: &crate::transport::Transport, frame: &Bytes, webrtc_masquerade: bool) -> std::io::Result<usize> { async fn send_datagram(socket: &crate::transport::Transport, frame: &Bytes, _webrtc_masquerade: bool) -> std::io::Result<usize> {
if webrtc_masquerade { socket.send(frame).await
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
}
} }
struct SessionState { struct SessionState {
@ -276,17 +263,13 @@ impl Bridge {
let session_index = sessions.len(); let session_index = sessions.len();
let socket_clone = sock.clone(); let socket_clone = sock.clone();
let udp_tx_clone = udp_tx.clone(); let udp_tx_clone = udp_tx.clone();
let is_webrtc = self.transport_mode == "udp" ;
tokio::spawn(async move { tokio::spawn(async move {
let mut buf = vec![0_u8; 65535]; let mut buf = vec![0_u8; 65535];
loop { loop {
match socket_clone.recv(&mut buf).await { match socket_clone.recv(&mut buf).await {
Ok(n) => { Ok(n) => {
let inbound = if is_webrtc && n >= 12 && buf[0] == 0x80 { let inbound = Bytes::copy_from_slice(&buf[..n]);
Bytes::copy_from_slice(&buf[12..n])
} else {
Bytes::copy_from_slice(&buf[..n])
};
if udp_tx_clone.send((session_index, inbound)).await.is_err() { if udp_tx_clone.send((session_index, inbound)).await.is_err() {
break; break;
} }
@ -366,17 +349,13 @@ impl Bridge {
let session_index = new_sessions.len(); let session_index = new_sessions.len();
let socket_clone = sock.clone(); let socket_clone = sock.clone();
let udp_tx_clone = udp_tx.clone(); let udp_tx_clone = udp_tx.clone();
let is_webrtc = self.transport_mode == "udp" ;
tokio::spawn(async move { tokio::spawn(async move {
let mut buf = vec![0_u8; 65535]; let mut buf = vec![0_u8; 65535];
loop { loop {
match socket_clone.recv(&mut buf).await { match socket_clone.recv(&mut buf).await {
Ok(n) => { Ok(n) => {
let inbound = if is_webrtc && n >= 12 && buf[0] == 0x80 { let inbound = Bytes::copy_from_slice(&buf[..n]);
Bytes::copy_from_slice(&buf[12..n])
} else {
Bytes::copy_from_slice(&buf[..n])
};
if udp_tx_clone.send((session_index, inbound)).await.is_err() { break; } if udp_tx_clone.send((session_index, inbound)).await.is_err() { break; }
} }
Err(e) => { Err(e) => {
@ -477,17 +456,13 @@ impl Bridge {
let session_index = new_sessions.len(); let session_index = new_sessions.len();
let socket_clone = sock.clone(); let socket_clone = sock.clone();
let udp_tx_clone = udp_tx.clone(); let udp_tx_clone = udp_tx.clone();
let is_webrtc = self.transport_mode == "udp" ;
tokio::spawn(async move { tokio::spawn(async move {
let mut buf = vec![0_u8; 65535]; let mut buf = vec![0_u8; 65535];
loop { loop {
match socket_clone.recv(&mut buf).await { match socket_clone.recv(&mut buf).await {
Ok(n) => { Ok(n) => {
let inbound = if is_webrtc && n >= 12 && buf[0] == 0x80 { let inbound = Bytes::copy_from_slice(&buf[..n]);
Bytes::copy_from_slice(&buf[12..n])
} else {
Bytes::copy_from_slice(&buf[..n])
};
if udp_tx_clone.send((session_index, inbound)).await.is_err() { if udp_tx_clone.send((session_index, inbound)).await.is_err() {
break; break;
} }
@ -885,11 +860,7 @@ impl Bridge {
self.metrics.bytes_recv.fetch_add(size as u64, Ordering::Relaxed); self.metrics.bytes_recv.fetch_add(size as u64, Ordering::Relaxed);
tracing::info!("Handshake response received: {} bytes", size); tracing::info!("Handshake response received: {} bytes", size);
let inbound = if (self.transport_mode == "udp") && size >= 12 && buf[0] == 0x80 { let inbound = Bytes::copy_from_slice(&buf[..size]);
Bytes::copy_from_slice(&buf[12..size])
} else {
Bytes::copy_from_slice(&buf[..size])
};
machine.on_event(OstpEvent::Inbound(inbound))?; machine.on_event(OstpEvent::Inbound(inbound))?;
let rtt_ms = start.elapsed().as_secs_f64() * 1000.0; let rtt_ms = start.elapsed().as_secs_f64() * 1000.0;
tracing::info!("Handshake complete: session={:#010x} rtt={:.1}ms", session_id, rtt_ms); 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.transport_mode = cfg.transport.mode.clone();
self.stealth_sni = cfg.transport.stealth_sni.clone(); self.stealth_sni = cfg.transport.stealth_sni.clone();
self.stealth_port = cfg.transport.stealth_port; 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( async fn try_connect_transport(

View File

@ -97,6 +97,8 @@ impl Default for TransportConfig {
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RealityConfig { pub struct RealityConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)] #[serde(default)]
pub sni: String, pub sni: String,
#[serde(default)] #[serde(default)]
@ -207,6 +209,7 @@ struct RawMuxSection {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct RawRealitySection { struct RawRealitySection {
enabled: Option<bool>,
sni: Option<String>, sni: Option<String>,
fp: Option<String>, fp: Option<String>,
pbk: Option<String>, pbk: Option<String>,
@ -260,6 +263,7 @@ impl ClientConfig {
connect_timeout_ms: 15000, connect_timeout_ms: 15000,
}, },
reality: RealityConfig { 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(), 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(), 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(), pbk: raw.reality.as_ref().and_then(|t| t.pbk.clone()).unwrap_or_default(),

View File

@ -180,11 +180,18 @@ pub async fn run_native_tunnel(
let mut rep = [0u8; 10]; let mut rep = [0u8; 10];
if socks.read_exact(&mut rep).await.is_err() || rep[1] != 0 { return; } if socks.read_exact(&mut rep).await.is_err() || rep[1] != 0 { return; }
if socks.write_all(&payload).await.is_ok() { let len = payload.len() as u16;
let mut response_buf = [0u8; 4096]; let mut dns_req = Vec::with_capacity(2 + payload.len());
if let Ok(n) = socks.read(&mut response_buf).await { dns_req.extend_from_slice(&len.to_be_bytes());
if n > 0 { dns_req.extend_from_slice(&payload);
let _ = tx_clone.lock().await.send((response_buf[..n].to_vec(), dst, src)).await;
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]; let mut rep = [0u8; 10];
if socks.read_exact(&mut rep).await.is_err() || rep[1] != 0 { return; } if socks.read_exact(&mut rep).await.is_err() || rep[1] != 0 { return; }
if socks.write_all(&payload).await.is_ok() { let len = payload.len() as u16;
let mut response_buf = [0u8; 4096]; let mut dns_req = Vec::with_capacity(2 + payload.len());
if let Ok(n) = socks.read(&mut response_buf).await { dns_req.extend_from_slice(&len.to_be_bytes());
if n > 0 { dns_req.extend_from_slice(&payload);
let _ = tx_clone.lock().await.send((response_buf[..n].to_vec(), dst, src)).await;
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;
} }
} }
} }

View File

@ -375,11 +375,7 @@ async fn run_server_loop(
loop { loop {
match sock_clone.recv_from(&mut buf).await { match sock_clone.recv_from(&mut buf).await {
Ok((size, peer)) => { Ok((size, peer)) => {
let packet = if size >= 12 && buf[0] == 0x80 { let packet = Bytes::copy_from_slice(&buf[..size]);
Bytes::copy_from_slice(&buf[12..size])
} else {
Bytes::copy_from_slice(&buf[..size])
};
if tx.send((packet, peer)).await.is_err() { if tx.send((packet, peer)).await.is_err() {
break; break;
} }
@ -529,10 +525,7 @@ async fn run_server_loop(
} }
} }
if !sent_tcp { if !sent_tcp {
let mut out = bytes::BytesMut::with_capacity(12 + resp.len()); let _ = socket.send_to(&resp, peer_addr).await?;
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 _ = ui_event_tx.send(UiEvent::Tx { peer: peer_ip, bytes: resp_len }); 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 { if !sent_tcp {
let mut out = bytes::BytesMut::with_capacity(12 + frame.len()); let _ = socket.send_to(&frame, peer_addr).await?;
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?;
} }
} }
for sid in dropped_sessions { for sid in dropped_sessions {

View File

@ -70,6 +70,8 @@ fn parse_ostp_link(link: &str) -> Result<ClientConfig> {
let mut sid = String::new(); let mut sid = String::new();
let mut spx = String::new(); let mut spx = String::new();
let mut transport_mode = String::from("udp"); let mut transport_mode = String::from("udp");
let mut tun_enabled = false;
let mut tun_dns = None;
for (k, v) in parsed.query_pairs() { for (k, v) in parsed.query_pairs() {
match k.as_ref() { match k.as_ref() {
@ -79,6 +81,8 @@ fn parse_ostp_link(link: &str) -> Result<ClientConfig> {
"sid" => sid = v.into_owned(), "sid" => sid = v.into_owned(),
"spx" => spx = v.into_owned(), "spx" => spx = v.into_owned(),
"type" => transport_mode = 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<ClientConfig> {
}), }),
socks5_bind: Some("127.0.0.1:1088".to_string()), socks5_bind: Some("127.0.0.1:1088".to_string()),
tun: Some(TunConfig { tun: Some(TunConfig {
enable: false, enable: tun_enabled,
wintun_path: Some("./wintun.dll".to_string()), wintun_path: Some("./wintun.dll".to_string()),
ipv4_address: Some("10.1.0.2/24".to_string()), ipv4_address: Some("10.1.0.2/24".to_string()),
dns: None, dns: tun_dns,
}), }),
reality: Some(RealityConfigRaw { reality: Some(RealityConfigRaw {
enabled: true,
sni, sni,
fp, fp,
pbk, pbk,
@ -338,6 +343,8 @@ struct TunConfig {
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
struct RealityConfigRaw { struct RealityConfigRaw {
#[serde(default)]
enabled: bool,
sni: String, sni: String,
fp: String, fp: String,
pbk: String, pbk: String,
@ -601,8 +608,8 @@ async fn run_app() -> Result<()> {
if let Some(ref mode_str) = args.init { if let Some(ref mode_str) = args.init {
let is_server = mode_str == "server"; let is_server = mode_str == "server";
let key = generate_secure_key("hex"); let key = generate_secure_key("hex");
let (priv_key, pub_key, sid) = generate_reality_keys();
let content = if is_server { let content = if is_server {
let (priv_key, pub_key, sid) = generate_reality_keys();
format!(r#"{{ format!(r#"{{
// OSTP Server Configuration // OSTP Server Configuration
"mode": "server", "mode": "server",
@ -708,11 +715,12 @@ async fn run_app() -> Result<()> {
// Reality (XTLS) / WebRTC Masquerade parameters // Reality (XTLS) / WebRTC Masquerade parameters
"reality": {{ "reality": {{
"dest": "www.microsoft.com:443", "enabled": false,
"private_key": "", "sni": "www.microsoft.com",
"pbk": "", "fp": "chrome",
"sid": "", "pbk": "{}",
"sni_list": ["www.microsoft.com"] "sid": "{}",
"spx": "/"
}}, }},
// Transport Mode: "udp" (default WebRTC masquerade) or "uot" (TCP XTLS-Reality) // Transport Mode: "udp" (default WebRTC masquerade) or "uot" (TCP XTLS-Reality)
@ -727,7 +735,7 @@ async fn run_app() -> Result<()> {
"sessions": 1 "sessions": 1
}}, }},
"debug": false "debug": false
}}"#, key) }}"#, key, pub_key, sid)
}; };
if let Some(parent) = args.config.parent() { if let Some(parent) = args.config.parent() {
if !parent.as_os_str().is_empty() { if !parent.as_os_str().is_empty() {
@ -1070,6 +1078,7 @@ async fn run_client_directly(client_cfg: ClientConfig) -> Result<()> {
connect_timeout_ms: 5000, connect_timeout_ms: 5000,
}, },
reality: ostp_client::config::RealityConfig { 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(), sni: reality_cfg.map(|t| t.sni.clone()).unwrap_or_default(),
fp: reality_cfg.map(|t| t.fp.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(), pbk: reality_cfg.map(|t| t.pbk.clone()).unwrap_or_default(),

61
server.json Normal file
View File

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