Compare commits

..

No commits in common. "5f9682663e5e1f14bc4da8e46d9a9b7d37278ffb" and "b31da29b2d29a5c707e49a4d138a7d2c46ce3e53" have entirely different histories.

27 changed files with 819 additions and 574 deletions

BIN
.gitignore vendored

Binary file not shown.

View File

@ -22,7 +22,6 @@ By contributing to this project, you agree to abide by our code of conduct and l
To build and test OSTP locally, you will need:
* **Rust Toolchain**: Install via [rustup](https://rustup.rs/) (stable channel).
* **Go 1.20+**: Required to compile the embedded `dnstt` tunnel binaries.
* **Node.js (18+) & npm**: Required to compile Tauri GUI resources.
* **Git**: For version control.

View File

@ -22,7 +22,6 @@
Для локальной сборки и тестирования OSTP вам понадобятся:
* **Rust Toolchain**: Установите через [rustup](https://rustup.rs/) (stable канал).
* **Go 1.20+**: Необходимо для сборки встроенного DNS-туннеля dnstt.
* **Node.js (18+) и npm**: Необходимы для сборки интерфейса Tauri.
* **Git**: Для контроля версий.

View File

@ -142,13 +142,8 @@ irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | ie
## Сборка из исходников
### Зависимости для сборки
- Rust 1.70+
- Go 1.20+ (необходимо для сборки встроенного DNS-туннеля dnstt)
> **Благодарности:** Этот проект использует [dnstt](https://www.bamsoftware.com/software/dnstt/) от Bamsoftware для обеспечения устойчивого туннелирования поверх DNS. Бинарники dnstt автоматически компилируются и встраиваются в ядро OSTP.
```bash
# Требования: Rust 1.75+
cargo build --release
# Кросс-компиляция для Linux

View File

@ -94,7 +94,7 @@ OSTP executes a Noise Protocol Framework exchange utilizing the `Noise_NNpsk0_25
2. The PSK is integrated into the state at pattern position zero, authorizing and encrypting the very first handshaking datagram.
3. Ephemeral Curve25519 key exchange is evaluated to synthesize autonomous symmetric keys for subsequent read/write channels.
The initial handshake payload includes a Unix timestamp to mitigate replay attacks. The server enforces a ±300-second synchronization window to accommodate clock drift and mobile roaming scenarios.
The initial handshake payload includes a Unix timestamp to mitigate replay attacks. The server enforces a strict ±30-second synchronization window.
---

View File

@ -94,7 +94,7 @@ OSTP использует Noise Protocol Framework с паттерном `Noise_
2. PSK применяется на нулевой позиции паттерна, обеспечивая авторизацию и шифрование самой первой датаграммы рукопожатия (Zero-RTT авторизация).
3. Выполняется эфемерный обмен ключами Curve25519 для создания симметричных ключей передачи данных.
Первичная полезная нагрузка рукопожатия содержит Unix-отметку времени для защиты от атак повторного воспроизведения (Replay Attacks). Сервер контролирует окно синхронизации (±300 секунд) с учётом дрейфа часов и смены сети при роуминге.
Первичная полезная нагрузка рукопожатия содержит Unix-отметку времени для защиты от атак повторного воспроизведения (Replay Attacks). Сервер строго контролирует окно синхронизации (±30 секунд).
---

View File

@ -9,7 +9,7 @@ anyhow.workspace = true
bytes.workspace = true
tokio.workspace = true
tracing.workspace = true
tracing-subscriber = { version = "0.3", features = ["env-filter", "time"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-appender = "0.2"
ostp-core = { path = "../ostp-core" }
ostp-tun = { path = "../ostp-tun" }

View File

@ -1,41 +0,0 @@
use anyhow::{anyhow, Result};
use chacha20poly1305::{ChaCha20Poly1305, Nonce};
use chacha20poly1305::aead::{Aead, KeyInit};
use sha2::{Sha256, Digest};
/// Symmetric IPC channel encryption for the tun-helper ↔ GUI pipe.
///
/// Both sides derive the same key from the per-launch random token, so no
/// secret is ever passed on the command line. The zero nonce is safe here
/// because each session uses a fresh random token, making key reuse impossible.
#[derive(Clone)]
pub struct IpcCrypto {
cipher: ChaCha20Poly1305,
}
impl IpcCrypto {
pub fn new(key: &[u8; 32]) -> Self {
let cipher = ChaCha20Poly1305::new_from_slice(key)
.expect("32-byte key is always valid for ChaCha20Poly1305");
Self { cipher }
}
pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
let nonce = Nonce::from_slice(&[0u8; 12]);
self.cipher.encrypt(nonce, plaintext)
.map_err(|e| anyhow!("IPC encrypt: {}", e))
}
pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
let nonce = Nonce::from_slice(&[0u8; 12]);
self.cipher.decrypt(nonce, ciphertext)
.map_err(|e| anyhow!("IPC decrypt: {}", e))
}
}
/// Derive a 32-byte key from the per-session random token.
pub fn derive_key(token: &str) -> [u8; 32] {
let mut key = [0u8; 32];
key.copy_from_slice(&Sha256::digest(token.as_bytes()));
key
}

View File

@ -9,4 +9,3 @@ pub mod tunnel;
pub mod runner;
pub mod logging;
pub mod ipc_crypto;

View File

@ -73,21 +73,17 @@ pub fn init_tracing(level: &str, app_name: &str, version: &str) -> Option<tracin
if let Ok(file) = OpenOptions::new().create(true).append(true).open(&path) {
let (file_writer, guard) = tracing_appender::non_blocking(file);
let timer = tracing_subscriber::fmt::time::UtcTime::rfc_3339();
let fmt_layer = tracing_subscriber::fmt::layer()
.with_target(true)
.with_line_number(false)
.with_line_number(true)
.with_thread_ids(false)
.with_thread_names(false)
.with_ansi(false)
.with_timer(timer.clone())
.with_writer(file_writer);
let stderr_layer = tracing_subscriber::fmt::layer()
.with_target(true)
.with_timer(timer)
.with_writer(std::io::stderr);
let _ = tracing_subscriber::registry()
@ -111,7 +107,6 @@ pub fn init_tracing(level: &str, app_name: &str, version: &str) -> Option<tracin
// Fallback: stderr only
let stderr_layer = tracing_subscriber::fmt::layer()
.with_target(true)
.with_timer(tracing_subscriber::fmt::time::UtcTime::rfc_3339())
.with_writer(std::io::stderr);
let _ = tracing_subscriber::registry()
.with(EnvFilter::new(level))

View File

@ -13,7 +13,7 @@ pub async fn run_client_core(
mut shutdown_rx_ext: watch::Receiver<bool>,
_config_rx: Option<watch::Receiver<ClientConfig>>,
) -> Result<()> {
tracing::info!("starting client core");
println!("[ostp] Starting run_client_core with multi-server architecture");
let router = Arc::new(Router::new(config.routing.clone()));
let balancer = Arc::new(Balancer::new(&config));

View File

@ -1 +1,230 @@
// Left empty by request
/// DNS tunnel transport — dnstt-style implementation.
///
/// Protocol (client → server, embedded in DNS query domain name):
/// Base32([client_id: 8][msg_id: 2 BE][total_frags: 1][frag_idx: 1][payload: ≤MAX_CHUNK])
/// Split into DNS labels of max 63 chars, suffixed with base_domain.
///
/// Poll query: payload is empty (total_frags=1, frag_idx=0, len=0).
///
/// Protocol (server → client, in TXT rdata):
/// Concatenated length-prefixed OSTP packets: [len: 2 BE][data ...]...
///
/// Polling: adaptive 500ms → 10s, like dnstt. Resets to 500ms on real data.
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use bytes::Bytes;
use rand::Rng;
use tokio::net::UdpSocket;
use tokio::sync::{mpsc, Mutex};
use crate::transport::Transport;
use rand::RngCore;
use ostp_core::dns::{base32_encode, DnsPacket, DnsRecordType};
/// Max raw payload bytes we put into one DNS query.
/// Calculation: FQDN ≤ 253 chars. Domain suffix ~30 chars max.
/// Remaining: ~220 chars for base32 labels. 220/8*5 = 137 bytes raw.
/// Header = 12 bytes → payload ≤ 120 bytes (conservative, works for any domain ≤ 40 chars).
const MAX_CHUNK_PAYLOAD: usize = 120;
const CLIENT_ID_LEN: usize = 8;
const INIT_POLL_DELAY: Duration = Duration::from_millis(500);
const MAX_POLL_DELAY: Duration = Duration::from_secs(10);
const POLL_DELAY_MULTIPLIER: f64 = 2.0;
pub async fn start_dns_transport(
domain: String,
resolver: String,
_pubkey: Option<String>,
) -> std::io::Result<Transport> {
let (app_tx, transport_rx) = mpsc::channel::<Bytes>(256);
let (transport_tx, app_rx) = mpsc::channel::<Bytes>(256);
let resolver_addr = if resolver.contains(':') {
resolver.clone()
} else {
format!("{}:53", resolver)
};
let socket = UdpSocket::bind("0.0.0.0:0").await?;
socket.connect(&resolver_addr).await?;
let socket = Arc::new(socket);
// Generate random ClientID for this tunnel session
let mut client_id = [0u8; CLIENT_ID_LEN];
rand::thread_rng().fill_bytes(&mut client_id);
let client_id = Arc::new(client_id);
tracing::info!("DNS transport: domain={} resolver={} client_id={}",
domain, resolver_addr,
hex::encode(client_id.as_slice()));
// ── Send task ─────────────────────────────────────────────────────────────
let sock_send = socket.clone();
let cid_send = client_id.clone();
let domain_send = domain.clone();
tokio::spawn(async move {
let mut rx = transport_rx;
let mut msg_id: u16 = 0;
let mut poll_delay = INIT_POLL_DELAY;
loop {
let data: Option<Bytes> = tokio::select! {
data = rx.recv() => data,
_ = tokio::time::sleep(poll_delay) => {
poll_delay = Duration::from_secs_f64(
(poll_delay.as_secs_f64() * POLL_DELAY_MULTIPLIER)
.min(MAX_POLL_DELAY.as_secs_f64())
);
// Send poll (empty payload)
Some(Bytes::new())
}
};
let data = match data {
Some(d) => d,
None => {
tracing::debug!("DNS send task: channel closed, exiting");
break;
}
};
if data.is_empty() {
// Poll query — one empty chunk
if let Err(e) = send_chunk(&sock_send, &cid_send, msg_id, 1, 0, &[], &domain_send).await {
tracing::warn!("DNS poll send error: {}", e);
}
} else {
// Real OSTP packet — fragment into chunks
poll_delay = INIT_POLL_DELAY; // reset on real data
let data_slice = data.as_ref();
let total_chunks = data_slice.chunks(MAX_CHUNK_PAYLOAD).count();
let total_u8 = total_chunks.min(255) as u8;
for (idx, chunk) in data_slice.chunks(MAX_CHUNK_PAYLOAD).enumerate() {
if let Err(e) = send_chunk(
&sock_send, &cid_send,
msg_id, total_u8, idx as u8,
chunk, &domain_send,
).await {
tracing::warn!("DNS chunk send error (idx={}): {}", idx, e);
break;
}
// Brief inter-fragment delay to avoid flooding the resolver
if total_chunks > 1 {
tokio::time::sleep(Duration::from_millis(20)).await;
}
}
msg_id = msg_id.wrapping_add(1);
}
}
});
// ── Receive task ──────────────────────────────────────────────────────────
let sock_recv = socket.clone();
let tx_recv = transport_tx.clone();
let domain_recv = domain.clone();
tokio::spawn(async move {
let mut buf = vec![0u8; 65535];
// Reassembly buffers: msg_id → (total, Vec<Option<chunk>>)
let reassembly: HashMap<u16, (u8, Vec<Option<Vec<u8>>>)> = HashMap::new();
loop {
match sock_recv.recv(&mut buf).await {
Ok(n) => {
let Some(pkt) = DnsPacket::decode(&buf[..n]) else { continue };
// Only process DNS responses
if pkt.flags & 0x8000 == 0 { continue; }
for answer in pkt.answers {
if answer.rtype != DnsRecordType::TXT && answer.rtype != DnsRecordType::NULL {
continue;
}
let rdata = answer.rdata;
// Parse length-prefixed OSTP packets packed in rdata:
// [len_hi: 1][len_lo: 1][data: len]...
let mut pos = 0;
while pos + 2 <= rdata.len() {
let pkt_len = u16::from_be_bytes([rdata[pos], rdata[pos + 1]]) as usize;
pos += 2;
if pkt_len == 0 { continue; }
if pos + pkt_len > rdata.len() {
tracing::debug!("DNS recv: truncated packet in rdata");
break;
}
let payload = Bytes::copy_from_slice(&rdata[pos..pos + pkt_len]);
pos += pkt_len;
if tx_recv.send(payload).await.is_err() {
return; // app closed
}
}
}
// Also check for responses packed in the server's extra DNS answer rdata
// that use our fragmentation scheme (server→client fragments)
// This is handled above via the length-prefix protocol.
let _ = &reassembly; // Keep for future upstream fragmentation support
let _ = &domain_recv;
}
Err(e) => {
tracing::warn!("DNS transport recv error: {}", e);
break;
}
}
}
});
Ok(Transport::Dns {
tx: app_tx,
rx: Arc::new(Mutex::new(app_rx)),
})
}
/// Build and send one DNS TXT query with a framed chunk.
///
/// Frame format (before base32 encoding):
/// [client_id: 8][msg_id: 2 BE][total_frags: 1][frag_idx: 1][payload: 0120]
async fn send_chunk(
socket: &UdpSocket,
client_id: &[u8; CLIENT_ID_LEN],
msg_id: u16,
total_frags: u8,
frag_idx: u8,
payload: &[u8],
base_domain: &str,
) -> std::io::Result<()> {
// Build frame
let mut frame = Vec::with_capacity(CLIENT_ID_LEN + 4 + payload.len());
frame.extend_from_slice(client_id);
frame.extend_from_slice(&msg_id.to_be_bytes());
frame.push(total_frags);
frame.push(frag_idx);
frame.extend_from_slice(payload);
// Base32-encode
let encoded = base32_encode(&frame);
// Split into 63-char labels and append domain
let mut fqdn = String::with_capacity(encoded.len() + base_domain.len() + 10);
let mut start = 0;
while start < encoded.len() {
let end = (start + 63).min(encoded.len());
fqdn.push_str(&encoded[start..end]);
fqdn.push('.');
start = end;
}
fqdn.push_str(base_domain);
// Build DNS TXT query with random ID
let id: u16 = rand::thread_rng().gen();
let pkt = DnsPacket::new_query(id, &fqdn, DnsRecordType::TXT);
let wire = pkt.encode();
tracing::trace!("DNS send chunk: msg_id={} frag={}/{} payload={}B fqdn_len={}",
msg_id, frag_idx + 1, total_frags, payload.len(), fqdn.len());
socket.send(&wire).await?;
Ok(())
}

View File

@ -1,3 +1,4 @@
pub mod dns;
use std::sync::Arc;
use tokio::net::UdpSocket;
use bytes::Bytes;
@ -9,10 +10,9 @@ pub enum Transport {
tx: tokio::sync::mpsc::Sender<Bytes>,
rx: Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<Bytes>>>,
},
Dnstt {
Dns {
tx: tokio::sync::mpsc::Sender<Bytes>,
rx: Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<Bytes>>>,
_guard: Arc<tokio::sync::Mutex<ostp_core::dnstt::DnsttProcess>>,
}
}
@ -20,7 +20,7 @@ impl Transport {
pub async fn send(&self, frame: &Bytes) -> std::io::Result<usize> {
match self {
Self::Udp(sock) => sock.send(frame).await,
Self::Uot { tx, .. } | Self::Dnstt { tx, .. } => {
Self::Uot { tx, .. } | Self::Dns { tx, .. } => {
tx.send(frame.clone()).await.map_err(|_| std::io::Error::new(std::io::ErrorKind::BrokenPipe, "channel closed"))?;
Ok(frame.len())
}
@ -30,40 +30,31 @@ impl Transport {
pub async fn send_to(&self, frame: &Bytes, target: std::net::SocketAddr) -> std::io::Result<usize> {
match self {
Self::Udp(sock) => sock.send_to(frame, target).await,
Self::Uot { .. } | Self::Dnstt { .. } => self.send(frame).await,
Self::Uot { .. } | Self::Dns { .. } => self.send(frame).await,
}
}
pub async fn recv(&self, buf: &mut [u8]) -> std::io::Result<usize> {
match self {
Self::Udp(sock) => sock.recv(buf).await,
Self::Uot { rx, .. } | Self::Dnstt { rx, .. } => {
Self::Uot { rx, .. } | Self::Dns { rx, .. } => {
let mut rx = rx.lock().await;
if let Some(frame) = rx.recv().await {
let len = frame.len().min(buf.len());
buf[..len].copy_from_slice(&frame[..len]);
Ok(len)
} else {
Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "channel closed"))
match rx.recv().await {
Some(bytes) => {
let len = bytes.len().min(buf.len());
buf[..len].copy_from_slice(&bytes[..len]);
Ok(len)
}
None => Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "channel closed")),
}
}
}
}
pub async fn recv_from(&self, buf: &mut [u8]) -> std::io::Result<(usize, std::net::SocketAddr)> {
match self {
Self::Udp(sock) => sock.recv_from(buf).await,
Self::Uot { .. } | Self::Dnstt { .. } => {
let n = self.recv(buf).await?;
Ok((n, "127.0.0.1:0".parse().unwrap()))
}
}
}
pub fn local_addr(&self) -> std::io::Result<std::net::SocketAddr> {
match self {
Self::Udp(sock) => sock.local_addr(),
Self::Uot { .. } | Self::Dnstt { .. } => Ok("0.0.0.0:0".parse().unwrap()),
Self::Uot { .. } | Self::Dns { .. } => Ok("0.0.0.0:0".parse().unwrap()),
}
}
}

View File

@ -32,13 +32,6 @@ fn make_initiator_config(
"dns" => 1100,
_ => 1350,
};
// For DNS transport: use larger ack_delay and rto to match DNS round-trip latency
// (each DNS query + reply takes 300-800ms end-to-end through Cloudflare).
// For UDP: minimize ack_delay to 1ms (ACK asap) and let CC drive the RTO.
let (ack_delay_ms, rto_ms) = match transport_cfg.r#type.as_str() {
"dns" => (50, 1500),
_ => (1, 200),
};
ProtocolConfig {
role: ostp_core::NoiseRole::Initiator,
@ -50,8 +43,8 @@ fn make_initiator_config(
obfuscation_key: secrets.obfuscation_key,
max_reorder: 16384,
max_reorder_buffer: 8192,
ack_delay_ms,
rto_ms,
ack_delay_ms: 5,
rto_ms: 100,
max_retries: 8,
max_sent_history: 32768,
handshake_pad_min: secrets.handshake_pad_min,
@ -187,25 +180,12 @@ pub async fn dial_tcp(
}
// ── Main bidirectional data forwarding loop ───────────────────────
// Backpressure: we track how many frames are in-flight vs the congestion
// window. When the window is full we stop reading from the TCP stream
// (the kernel buffers it) until the remote ACKs enough frames.
// This prevents overrunning the sender's sent_history and collapsing cwnd.
let mut buf = [0u8; 65535];
let mut udp_buf = [0u8; 65535];
loop {
// Compute adaptive tick interval:
// - If there is a pending ACK: tick = ack_delay (flush it quickly)
// - Otherwise: tick = rto/4 (check retransmits without busy-spinning)
// Floor at 1ms, ceiling at 50ms.
let tick_ms = (machine.rto().as_millis() / 4).clamp(1, 50) as u64;
let can_send = machine.in_flight_count() < machine.cwnd_packets().max(4);
tokio::select! {
// Only read from the application TCP stream when cwnd allows
Ok(n) = server_stream.read(&mut buf), if can_send => {
Ok(n) = server_stream.read(&mut buf) => {
if n == 0 { break; }
let data_msg = ostp_core::relay::RelayMessage::Data(buf[..n].to_vec());
let encoded = data_msg.encode();
@ -218,7 +198,7 @@ pub async fn dial_tcp(
handle_action(action, &transport, &mut server_stream).await;
}
}
_ = tokio::time::sleep(std::time::Duration::from_millis(tick_ms)) => {
_ = tokio::time::sleep(std::time::Duration::from_millis(10)) => {
if let Ok(action) = machine.on_event(OstpEvent::Tick) {
handle_action(action, &transport, &mut server_stream).await;
}
@ -319,62 +299,15 @@ async fn make_transport(
server: &str,
port: u16,
) -> Result<crate::transport::Transport> {
let debug = tracing::enabled!(tracing::Level::DEBUG);
match transport_cfg.r#type.as_str() {
"dns" => {
let domain = transport_cfg.domain.clone()
.unwrap_or_else(|| "tunnel.example.com".to_string());
let pubkey = transport_cfg.pubkey.clone()
.unwrap_or_else(|| "".to_string());
let resolver = transport_cfg.resolver.clone()
.unwrap_or_else(|| server.to_string());
let resolver_with_port = if resolver.contains(':') {
resolver.clone()
} else {
format!("{}:53", resolver)
};
let (local_port, process) = ostp_core::dnstt::spawn_client(&pubkey, &domain, &resolver_with_port, debug)?;
// Wait for dnstt-client to start its local TCP listener
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
// Connect TCP to the local dnstt-client port
let stream = tokio::net::TcpStream::connect(("127.0.0.1", local_port)).await?;
let (mut rh, mut wh) = stream.into_split();
let (tx_send, mut tx_recv) = tokio::sync::mpsc::channel::<bytes::Bytes>(1024);
let (rx_send, rx_recv) = tokio::sync::mpsc::channel::<bytes::Bytes>(1024);
// Writer task
tokio::spawn(async move {
use tokio::io::AsyncWriteExt;
while let Some(data) = tx_recv.recv().await {
let len = data.len() as u16;
if wh.write_u16(len).await.is_err() { break; }
if wh.write_all(&data).await.is_err() { break; }
}
});
// Reader task
tokio::spawn(async move {
use tokio::io::AsyncReadExt;
loop {
let len = match rh.read_u16().await {
Ok(l) => l,
Err(_) => break,
};
let mut buf = vec![0u8; len as usize];
if rh.read_exact(&mut buf).await.is_err() { break; }
if rx_send.send(bytes::Bytes::from(buf)).await.is_err() { break; }
}
});
Ok(crate::transport::Transport::Dnstt {
tx: tx_send,
rx: std::sync::Arc::new(tokio::sync::Mutex::new(rx_recv)),
_guard: std::sync::Arc::new(tokio::sync::Mutex::new(process)),
})
let transport = crate::transport::dns::start_dns_transport(domain, resolver, transport_cfg.pubkey.clone()).await
.map_err(|e| anyhow::anyhow!(e))?;
Ok(transport)
}
_ => {
let udp = tokio::net::UdpSocket::bind("0.0.0.0:0").await?;

View File

@ -4,12 +4,6 @@
//! bandwidth and minimum RTT to determine the optimal sending rate.
//! This replaces the fixed `retransmit_budget = 8` with an adaptive
//! congestion window that responds to network conditions.
//!
//! RTO calculation follows RFC 6298:
//! SRTT = (1 - α) * SRTT + α * RTT (α = 1/8)
//! RTTVAR = (1 - β) * RTTVAR + β * |SRTT - RTT| (β = 1/4)
//! RTO = SRTT + 4 * RTTVAR
//! clamped to [RTO_MIN, RTO_MAX]
use std::time::{Duration, Instant};
@ -21,14 +15,8 @@ pub struct CongestionController {
ssthresh: u64,
/// Current phase
phase: Phase,
/// Minimum RTT observed (for BBR-style bandwidth estimation)
/// Minimum RTT observed
min_rtt: Duration,
/// Smoothed RTT (RFC 6298 SRTT)
srtt: Duration,
/// RTT variance (RFC 6298 RTTVAR)
rttvar: Duration,
/// Whether we have received a first RTT sample
rtt_initialized: bool,
/// Bytes currently in flight (unacknowledged)
bytes_in_flight: u64,
/// Total bytes acknowledged (for bandwidth estimation)
@ -49,43 +37,31 @@ pub struct CongestionController {
enum Phase {
/// Exponential growth until loss or ssthresh
SlowStart,
/// Probe bandwidth: additive increase
/// Probe bandwidth: cycle through pacing gains
ProbeBandwidth,
}
/// Initial congestion window: 32 packets × MTU (IW10 is too conservative for modern links)
const INITIAL_CWND_PACKETS: u64 = 32;
/// Initial congestion window: 10 packets × MTU
const INITIAL_CWND_PACKETS: u64 = 10;
/// Minimum cwnd: 2 packets
const MIN_CWND_PACKETS: u64 = 2;
/// Min RTT expiry window (after which we re-probe)
const MIN_RTT_EXPIRY: Duration = Duration::from_secs(10);
/// Minimum RTO (RFC 6298: 1s in TCP; we use 50ms since we own the protocol)
const RTO_MIN: Duration = Duration::from_millis(50);
/// Maximum RTO
const RTO_MAX: Duration = Duration::from_secs(16);
/// Initial RTT estimate — 30 ms is reasonable for a well-connected VPN server.
/// Will be replaced by first real measurement within milliseconds.
const INITIAL_RTT: Duration = Duration::from_millis(30);
impl CongestionController {
pub fn new(mtu: u64) -> Self {
let now = Instant::now();
let initial_cwnd = INITIAL_CWND_PACKETS * mtu;
// Initial pacing: deliver cwnd in ~2 RTTs to fill the pipe quickly
let initial_pacing = initial_cwnd * 1_000_000 / INITIAL_RTT.as_micros().max(1) as u64;
Self {
cwnd: initial_cwnd,
ssthresh: u64::MAX,
phase: Phase::SlowStart,
min_rtt: INITIAL_RTT,
srtt: INITIAL_RTT,
rttvar: INITIAL_RTT / 2,
rtt_initialized: false,
min_rtt: Duration::from_millis(100), // Conservative initial estimate
bytes_in_flight: 0,
total_acked: 0,
last_ack_time: now,
loss_count: 0,
pacing_rate: initial_pacing,
pacing_rate: initial_cwnd * 10, // initial: ~10 windows/sec
mtu,
min_rtt_stamp: now,
}
@ -106,20 +82,9 @@ impl CongestionController {
self.pacing_rate
}
/// Returns the smoothed RTT estimate (SRTT).
/// Returns the smoothed RTT estimate.
pub fn smoothed_rtt(&self) -> Duration {
self.srtt
}
/// Returns the adaptive RTO computed per RFC 6298:
/// RTO = SRTT + 4 * RTTVAR, clamped to [RTO_MIN, RTO_MAX].
///
/// This replaces the static `rto_ms` field in ProtocolMachine so that
/// retransmit timers automatically track changing network conditions.
pub fn rto(&self) -> Duration {
let rttvar4 = self.rttvar.saturating_mul(4);
let rto = self.srtt.saturating_add(rttvar4);
rto.clamp(RTO_MIN, RTO_MAX)
self.min_rtt
}
/// Returns how many bytes can still be sent.
@ -150,13 +115,16 @@ impl CongestionController {
self.bytes_in_flight = self.bytes_in_flight.saturating_sub(bytes);
self.total_acked = self.total_acked.saturating_add(bytes);
// Update RTT measurements
// Update RTT
self.update_rtt(rtt, now);
// Update bandwidth estimate
self.update_bandwidth(bytes, now);
// State machine
match self.phase {
Phase::SlowStart => {
// Exponential growth: increase cwnd by acked bytes (doubles per RTT)
// Exponential growth: increase cwnd by acked bytes
self.cwnd = self.cwnd.saturating_add(bytes);
if self.cwnd >= self.ssthresh {
self.phase = Phase::ProbeBandwidth;
@ -196,49 +164,32 @@ impl CongestionController {
self.update_pacing_rate();
}
/// Called periodically to update state.
pub fn on_tick(&mut self) {
// Nothing special needed per-tick -- state updates happen on ACK/loss
}
// ── Private ──────────────────────────────────────────────────────────────
fn update_rtt(&mut self, rtt: Duration, now: Instant) {
// Update windowed minimum RTT (for pacing)
// Track windowed minimum RTT
if rtt < self.min_rtt || now.duration_since(self.min_rtt_stamp) >= MIN_RTT_EXPIRY {
self.min_rtt = rtt;
self.min_rtt_stamp = now;
}
// Update SRTT and RTTVAR per RFC 6298
if !self.rtt_initialized {
// First measurement: initialize directly
self.srtt = rtt;
self.rttvar = rtt / 2;
self.rtt_initialized = true;
} else {
// RTTVAR = (3/4) * RTTVAR + (1/4) * |SRTT - R|
let diff = if rtt > self.srtt {
rtt - self.srtt
} else {
self.srtt - rtt
};
// Integer-safe: RTTVAR = RTTVAR - RTTVAR/4 + diff/4
self.rttvar = self.rttvar
.saturating_sub(self.rttvar / 4)
.saturating_add(diff / 4);
// SRTT = (7/8) * SRTT + (1/8) * R
self.srtt = self.srtt
.saturating_sub(self.srtt / 8)
.saturating_add(rtt / 8);
}
tracing::trace!(
srtt_ms = self.srtt.as_millis(),
rttvar_ms = self.rttvar.as_millis(),
rto_ms = self.rto().as_millis(),
"congestion: RTT updated"
);
}
fn update_bandwidth(&mut self, _acked_bytes: u64, now: Instant) {
let elapsed = now.duration_since(self.last_ack_time);
if elapsed.as_micros() > 0 {
// Removed bw_samples tracking
}
}
fn update_pacing_rate(&mut self) {
// Pacing rate = cwnd / min_rtt (delivery rate target)
// Pacing rate = cwnd / min_rtt (with gain)
let rtt_us = self.min_rtt.as_micros().max(1) as u64;
self.pacing_rate = self.cwnd * 1_000_000 / rtt_us;
}
@ -251,18 +202,19 @@ mod tests {
#[test]
fn test_initial_state() {
let cc = CongestionController::new(1200);
assert_eq!(cc.cwnd(), 32 * 1200); // 32 * 1200
assert_eq!(cc.cwnd(), 12000); // 10 * 1200
assert!(cc.can_send());
assert_eq!(cc.cwnd_packets(), 32);
assert_eq!(cc.cwnd_packets(), 10);
}
#[test]
fn test_slow_start_growth() {
let mut cc = CongestionController::new(1200);
let initial = cc.cwnd();
// Simulate sending and ACKing
cc.on_send(1200);
cc.on_ack(1200, Duration::from_millis(50));
assert!(cc.cwnd() > initial);
// cwnd should grow
assert!(cc.cwnd() > 12000);
}
#[test]
@ -277,7 +229,7 @@ mod tests {
fn test_can_send_limits() {
let mut cc = CongestionController::new(1200);
// Send until cwnd is exhausted
for _ in 0..32 {
for _ in 0..10 {
cc.on_send(1200);
}
assert!(!cc.can_send()); // cwnd exhausted
@ -292,46 +244,10 @@ mod tests {
}
#[test]
fn test_rtt_tracking_first_sample() {
fn test_rtt_tracking() {
let mut cc = CongestionController::new(1200);
cc.on_send(1200);
cc.on_ack(1200, Duration::from_millis(25));
// After first sample: SRTT = 25ms, RTTVAR = 12ms
assert_eq!(cc.smoothed_rtt(), Duration::from_millis(25));
}
#[test]
fn test_rto_rfc6298() {
let mut cc = CongestionController::new(1200);
// After first sample with RTT=50ms: SRTT=50ms, RTTVAR=25ms, RTO=150ms
cc.on_send(1200);
cc.on_ack(1200, Duration::from_millis(50));
let rto = cc.rto();
// RTO = 50 + 4*25 = 150ms; clamped to [50ms, 16s]
assert!(rto >= RTO_MIN);
assert!(rto <= RTO_MAX);
assert_eq!(rto, Duration::from_millis(150));
}
#[test]
fn test_rto_clamp_min() {
let cc = CongestionController::new(1200);
// Even with no RTT samples, RTO should not go below RTO_MIN
assert!(cc.rto() >= RTO_MIN);
}
#[test]
fn test_rto_adapts_after_multiple_samples() {
let mut cc = CongestionController::new(1200);
// Feed several consistent RTT samples
for _ in 0..8 {
cc.on_send(1200);
cc.on_ack(1200, Duration::from_millis(20));
}
// After convergence, RTTVAR should be small → RTO close to SRTT + small margin
let rto = cc.rto();
// Should be well below 100ms (the old hardcoded default)
assert!(rto < Duration::from_millis(200));
assert!(rto >= RTO_MIN);
}
}

View File

@ -6,7 +6,6 @@ pub mod relay;
pub mod resumption;
pub mod dns;
pub mod dns_prober;
pub mod dnstt;
pub use crypto::NoiseRole;
pub use framing::{TrafficProfile, PaddingStrategy};

View File

@ -2,7 +2,7 @@ use bytes::Bytes;
use rand::Rng;
use sha2::{Digest, Sha256};
use thiserror::Error;
use std::collections::BTreeMap;
use std::collections::{BTreeMap, VecDeque};
use std::time::{Duration, Instant};
use crate::congestion::CongestionController;
@ -75,7 +75,7 @@ pub struct ProtocolMachine {
send_nonce: u64,
expected_recv_nonce: u64,
reorder_buffer: BTreeMap<u64, ProtocolAction>,
sent_history: BTreeMap<u64, SentFrame>,
sent_history: VecDeque<SentFrame>,
session_id: u32,
handshake_payload: Vec<u8>,
padder: AdaptivePadder,
@ -83,8 +83,7 @@ pub struct ProtocolMachine {
max_reorder: u64,
max_reorder_buffer: usize,
ack_delay: Duration,
/// Initial/fallback RTO from config (overridden by cc.rto() after first RTT sample)
rto_initial: Duration,
rto: Duration,
max_retries: u8,
max_sent_history: usize,
ack_pending: bool,
@ -101,11 +100,11 @@ pub struct ProtocolMachine {
/// Key-derived handshake padding range
handshake_pad_min: usize,
handshake_pad_max: usize,
_mtu: usize,
}
#[derive(Debug, Clone)]
struct SentFrame {
#[allow(dead_code)] // mirrored in BTreeMap key; kept for Debug output
nonce: u64,
bytes: Bytes,
last_sent: Instant,
@ -129,7 +128,7 @@ impl ProtocolMachine {
send_nonce: 0,
expected_recv_nonce: 0,
reorder_buffer: BTreeMap::new(),
sent_history: BTreeMap::new(),
sent_history: VecDeque::with_capacity(config.max_sent_history.max(1)),
session_id: config.session_id,
handshake_payload: config.handshake_payload,
padder: AdaptivePadder::new(config.mtu, config.max_padding, config.padding_strategy),
@ -137,7 +136,7 @@ impl ProtocolMachine {
max_reorder: config.max_reorder.max(1),
max_reorder_buffer: config.max_reorder_buffer.max(1),
ack_delay: Duration::from_millis(config.ack_delay_ms.max(1)),
rto_initial: Duration::from_millis(config.rto_ms.max(1)),
rto: Duration::from_millis(config.rto_ms.max(1)),
max_retries: config.max_retries.max(1),
max_sent_history: config.max_sent_history.max(1),
ack_pending: false,
@ -147,25 +146,20 @@ impl ProtocolMachine {
cc: CongestionController::new(config.mtu as u64),
handshake_pad_min: config.handshake_pad_min.max(8),
handshake_pad_max: config.handshake_pad_max.max(config.handshake_pad_min + 16),
_mtu: config.mtu,
})
}
pub fn in_flight_count(&self) -> usize {
// COUNT ONLY retransmittable Data frames — control frames (Ack/Nack) must not
// contribute to this counter or they will trigger false backpressure.
self.sent_history.values().filter(|f| f.is_retransmittable).count()
self.sent_history.iter().filter(|f| f.is_retransmittable).count()
}
pub fn cwnd_packets(&self) -> usize {
self.cc.cwnd_packets() as usize
}
/// Returns the current adaptive RTO (from congestion controller after first RTT sample,
/// falls back to the config-specified initial value before any ACK is received).
pub fn rto(&self) -> Duration {
self.cc.rto()
}
pub fn on_send(&mut self, bytes: u64) {
self.cc.on_send(bytes);
}
@ -213,12 +207,13 @@ impl ProtocolMachine {
.map(ProtocolAction::SendDatagram)
}
(OstpState::Closing, OstpEvent::Inbound(raw)) => {
// The remote may still have data or ACKs in transit.
// handle_inbound transitions to Closed when it receives a Close frame.
self.handle_inbound(raw)
// Process final in-flight packets to prevent data loss during teardown.
// The remote may still have data or ACKs in transit when we initiated Close.
let result = self.handle_inbound(raw);
self.state = OstpState::Closed;
result
}
(OstpState::Established, OstpEvent::Tick) => self.handle_tick(),
(OstpState::Closing, OstpEvent::Tick) => self.handle_tick(),
(OstpState::Closed, _) => Ok(ProtocolAction::Noop),
(_, OstpEvent::Close) => {
self.state = OstpState::Closed;
@ -413,10 +408,10 @@ impl ProtocolMachine {
tracing::debug!("Frame nonce={} arrived too late after gap recovery, dropping", nonce);
}
// Rate-limited NACK: send at most once per (rto/2) to prevent retransmit storms.
// Using rto/2 means we send a NACK before the sender's timer fires, prompting
// fast retransmit without flooding. Floor at 10ms to handle very low-RTT links.
let nack_cooldown = (self.cc.rto() / 2).max(Duration::from_millis(10));
// Rate-limited NACK: send at most once per 30ms to prevent retransmit storms.
// Under high load with natural UDP reordering, sending a NACK per packet
// causes exponential retransmit explosion that saturates the channel.
let nack_cooldown = Duration::from_millis(30);
if self.last_nack_sent.elapsed() >= nack_cooldown {
self.last_nack_sent = Instant::now();
let nack_payload = self.expected_recv_nonce.to_be_bytes();
@ -530,39 +525,36 @@ impl ProtocolMachine {
}
let now = Instant::now();
// Use the adaptive RTO from the congestion controller (RFC 6298 SRTT + 4*RTTVAR).
// Falls back to rto_initial before the first ACK is received.
let base_rto = self.cc.rto().max(self.rto_initial);
let base_rto_ms = base_rto.as_millis().max(1) as u64;
let base_rto_ms = self.rto.as_millis().max(1) as u64;
// ── Zombie frame eviction ────────────────────────────────────
// Evict frames that exceeded max_retries + 2 grace retries.
// Shorter grace period than before (was +4) to free memory faster
// after high-throughput bursts.
let grace = self.max_retries.saturating_add(2);
let before = self.sent_history.len();
self.sent_history.retain(|_, f| !f.is_retransmittable || f.retries <= grace);
self.sent_history.retain(|f| !f.is_retransmittable || f.retries <= grace);
let evicted = before - self.sent_history.len();
if evicted > 0 {
tracing::debug!("Evicted {} zombie frames from sent_history (remaining={})", evicted, self.sent_history.len());
}
// ── Retransmit expired frames ────────────────────────────────
// Backoff starts from retry #0 (immediately effective):
// effective_rto = base_rto * 2^retries, capped at 2^6 = 64×
// This ensures we do not flood with retransmits on the first few losses
// while still recovering quickly on a transient single loss.
// Limit retransmits per tick to prevent bandwidth saturation
let mut retransmit_budget: usize = self.cc.retransmit_budget();
for frame in self.sent_history.values_mut() {
for frame in self.sent_history.iter_mut() {
if !frame.is_retransmittable {
continue;
}
let backoff_factor = 1u64 << (frame.retries as u64).min(6);
let retry_over = frame.retries.saturating_sub(self.max_retries);
let backoff_factor = 1u64 << retry_over.min(6);
let effective_rto = Duration::from_millis(base_rto_ms.saturating_mul(backoff_factor));
if now.duration_since(frame.last_sent) >= effective_rto {
frame.last_sent = now;
frame.retries = frame.retries.saturating_add(1);
if retransmit_budget > 0 {
actions.push(ProtocolAction::SendDatagram(frame.bytes.clone()));
retransmit_budget -= 1;
@ -662,7 +654,7 @@ impl ProtocolMachine {
}
fn lookup_sent_frame(&mut self, nonce: u64) -> Option<Bytes> {
if let Some(frame) = self.sent_history.get_mut(&nonce) {
if let Some(frame) = self.sent_history.iter_mut().rev().find(|f| f.nonce == nonce) {
frame.last_sent = Instant::now();
frame.retries = frame.retries.saturating_add(1);
return Some(frame.bytes.clone());
@ -674,7 +666,7 @@ impl ProtocolMachine {
if is_retransmittable {
self.cc.on_send(bytes.len() as u64);
}
self.sent_history.insert(nonce, SentFrame {
self.sent_history.push_back(SentFrame {
nonce,
bytes,
last_sent: Instant::now(),
@ -687,7 +679,7 @@ impl ProtocolMachine {
overflow, self.max_sent_history
);
while self.sent_history.len() > self.max_sent_history {
self.sent_history.pop_first();
self.sent_history.pop_front();
}
}
}
@ -698,8 +690,8 @@ impl ProtocolMachine {
let mut min_rtt = Duration::from_secs(60);
// Compute RTT from the oldest acked frame's send timestamp
for (&nonce, frame) in &self.sent_history {
if nonce_in_ranges(nonce, ranges) {
for frame in self.sent_history.iter() {
if nonce_in_ranges(frame.nonce, ranges) {
acked_bytes += frame.bytes.len() as u64;
let rtt = now.duration_since(frame.last_sent);
if rtt < min_rtt {
@ -708,7 +700,7 @@ impl ProtocolMachine {
}
}
self.sent_history.retain(|&nonce, _| !nonce_in_ranges(nonce, ranges));
self.sent_history.retain(|frame| !nonce_in_ranges(frame.nonce, ranges));
// Notify congestion controller
if acked_bytes > 0 {

View File

@ -1,3 +1,41 @@
// Re-export the shared IPC crypto from ostp-client so that GUI and tun-helper
// always use identical encrypt/decrypt logic.
pub use ostp_client::ipc_crypto::{derive_key, IpcCrypto};
use anyhow::{anyhow, Result};
use chacha20poly1305::{ChaCha20Poly1305, Nonce};
use chacha20poly1305::aead::{Aead, KeyInit};
use sha2::{Sha256, Digest};
pub struct IpcCrypto {
cipher: ChaCha20Poly1305,
nonce: [u8; 12],
}
impl IpcCrypto {
pub fn new(key: &[u8; 32]) -> Self {
let cipher = ChaCha20Poly1305::new_from_slice(key)
.expect("valid key size");
let nonce = [0u8; 12];
Self { cipher, nonce }
}
pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
let nonce = Nonce::from_slice(&self.nonce);
let ciphertext = self.cipher.encrypt(nonce, plaintext)
.map_err(|e| anyhow!("Encryption failed: {}", e))?;
Ok(ciphertext)
}
pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
let nonce = Nonce::from_slice(&self.nonce);
let plaintext = self.cipher.decrypt(nonce, ciphertext)
.map_err(|e| anyhow!("Decryption failed: {}", e))?;
Ok(plaintext)
}
}
pub fn derive_key(token: &str) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(token.as_bytes());
let result = hasher.finalize();
let mut key = [0u8; 32];
key.copy_from_slice(&result);
key
}

View File

@ -40,7 +40,7 @@ struct UIMetrics {
#[serde(tag = "type", rename_all = "lowercase")]
enum HelperMsg {
Status { value: u8 },
Log { #[allow(dead_code)] message: String },
Log { message: String },
Metrics { bytes_sent: u64, bytes_recv: u64, rtt_ms: u32 },
Error { message: String },
}
@ -59,7 +59,6 @@ struct HelperState {
pipe_state: Arc<Mutex<HelperPipeState>>,
cmd_tx: tokio::sync::mpsc::Sender<String>,
token: String,
#[allow(dead_code)]
port: u16,
}

View File

@ -12,13 +12,12 @@ use portable_atomic::AtomicU64;
// const MAX_SESSIONS removed because dynamic limit is used
pub enum DispatchOutcome {
Unauthorized(String),
Accepted {
responses: Vec<Bytes>,
app_payloads: Vec<(u32, u16, Bytes)>, // session_id, stream_id, payload
peer_addr: SocketAddr,
},
Unauthorized(String),
Ignored,
}
/// Per-user traffic statistics.
@ -84,6 +83,7 @@ pub struct Dispatcher {
last_token_regen: std::time::Instant,
}
#[allow(dead_code)]
impl Dispatcher {
pub fn new(machine_config: ProtocolConfig, access_keys: Arc<RwLock<HashMap<String, crate::api::UserMeta>>>) -> Self {
let mut initial_stats = HashMap::new();
@ -108,7 +108,6 @@ impl Dispatcher {
}
/// Snapshot all user stats for API responses.
#[allow(dead_code)]
pub fn snapshot_all_users(&self) -> Vec<UserStatsSnapshot> {
let stats = self.user_stats.read().unwrap_or_else(|e| e.into_inner());
let mut online_keys: HashMap<String, std::time::Instant> = HashMap::new();
@ -162,7 +161,6 @@ impl Dispatcher {
}
/// Set traffic limit for a user.
#[allow(dead_code)]
pub fn set_user_limit(&self, key: &str, limit: Option<u64>) {
let mut stats = self.user_stats.write().unwrap_or_else(|e| e.into_inner());
let entry = stats.entry(key.to_string())
@ -178,7 +176,6 @@ impl Dispatcher {
}
/// Active session count.
#[allow(dead_code)]
pub fn active_sessions(&self) -> usize {
self.peer_machines.len()
}
@ -379,19 +376,15 @@ impl Dispatcher {
continue;
}
if self.replay_cache.contains_key(&payload.to_vec()) {
tracing::debug!("Replay detected from {}, ignoring", peer);
return Ok(DispatchOutcome::Ignored);
}
if !self.replay_cache.contains_key(&payload.to_vec()) {
if self.replay_cache.len() >= 50_000 {
tracing::warn!("Replay cache full (100000 entries), rejecting handshake from {}", peer);
return Ok(DispatchOutcome::Unauthorized("replay cache full".to_string()));
}
if self.replay_cache.len() >= 50_000 {
tracing::warn!("Replay cache full (50000 entries), rejecting handshake from {}", peer);
return Ok(DispatchOutcome::Unauthorized("replay cache full".to_string()));
}
self.replay_cache.insert(payload.to_vec(), ts);
self.replay_cache.insert(payload.to_vec(), ts);
machine.set_session_keys(candidate_session_id, secrets.obfuscation_key);
machine.set_session_keys(candidate_session_id, secrets.obfuscation_key);
// Track per-user connection count
let user_stats = self.get_or_create_user_stats(&candidate_key);
@ -421,6 +414,7 @@ impl Dispatcher {
app_payloads: Vec::new(),
peer_addr: peer,
});
}
}
}
}
@ -435,35 +429,23 @@ impl Dispatcher {
Ok(DispatchOutcome::Unauthorized(reason))
}
pub fn outbound_to_session(&mut self, session_id: u32, stream_id: u16, payload: Bytes) -> Result<Vec<(Bytes, SocketAddr)>> {
pub fn outbound_to_session(&mut self, session_id: u32, stream_id: u16, payload: Bytes) -> Result<Option<(Bytes, SocketAddr)>> {
let peer_state = if let Some(existing) = self.peer_machines.get_mut(&session_id) {
existing
} else {
return Ok(Vec::new());
return Ok(None);
};
let addr = peer_state.last_addr;
let key = peer_state.access_key.clone();
let action = peer_state.machine.on_event(OstpEvent::Outbound(stream_id, payload))?;
let mut frames = Vec::new();
let mut queue = vec![action];
while let Some(current) = queue.pop() {
match current {
ProtocolAction::Multiple(list) => {
for item in list {
queue.push(item);
}
}
ProtocolAction::SendDatagram(frame) => {
track_user_bytes_down(&self.user_stats, &self.access_keys, &key, frame.len() as u64);
frames.push((frame, addr));
}
_ => {}
match peer_state.machine.on_event(OstpEvent::Outbound(stream_id, payload))? {
ProtocolAction::SendDatagram(frame) => {
// Track outbound bytes per user
track_user_bytes_down(&self.user_stats, &self.access_keys, &key, frame.len() as u64);
Ok(Some((frame, addr)))
}
_ => Ok(None),
}
Ok(frames)
}
pub fn on_tick(&mut self) -> (Vec<(Bytes, SocketAddr)>, Vec<u32>) {
@ -477,7 +459,7 @@ impl Dispatcher {
let mut frames = Vec::new();
let mut expired = Vec::new();
let now = std::time::Instant::now();
let timeout_dur = std::time::Duration::from_secs(600); // 10-minute session timeout (mobile NAT mappings can live 510 min)
let timeout_dur = std::time::Duration::from_secs(600); // 10 minute session timeout (mobile NAT can be up to 5-10min)
// Gather expired or invalid sessions
for (&sid, peer_state) in &self.peer_machines {
@ -495,7 +477,7 @@ impl Dispatcher {
let key_valid = self.access_keys.read().unwrap_or_else(|e| e.into_inner()).contains_key(&ps.access_key);
let user_stats = self.get_or_create_user_stats(&ps.access_key);
if now.duration_since(ps.last_seen) > timeout_dur {
"inactive >10min"
"inactive >5min"
} else if !key_valid {
"key deleted"
} else if user_stats.is_over_limit() {

View File

@ -1,23 +1,15 @@
use anyhow::Result;
use bytes::Bytes;
use std::collections::{HashMap, VecDeque};
use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use std::collections::HashMap;
use std::net::IpAddr;
use dispatcher::{DispatchOutcome, Dispatcher};
use ostp_core::relay::RelayMessage;
use signal::wait_for_shutdown_signal;
use tokio::net::UdpSocket;
use tokio::sync::{mpsc, RwLock};
use tokio::sync::mpsc;
use tokio::time::{interval, Duration, Instant};
use std::sync::OnceLock;
pub fn dns_queue() -> &'static Arc<RwLock<HashMap<SocketAddr, VecDeque<Bytes>>>> {
static DNS_QUEUE: OnceLock<Arc<RwLock<HashMap<SocketAddr, VecDeque<Bytes>>>>> = OnceLock::new();
DNS_QUEUE.get_or_init(|| Arc::new(RwLock::new(HashMap::new())))
}
mod dispatcher;
pub mod outbound;
pub mod fallback;
@ -128,29 +120,6 @@ pub async fn run_server(
let dispatcher = Dispatcher::new(protocol_config, shared_keys.clone());
// Launch dnstt-server if configured
let _dnstt_guard = if let Some(dns) = &dns_transport {
let pub_ip = server_public_ip.clone().unwrap_or_else(|| {
let p = config_path.as_ref()
.and_then(|p| p.parent())
.unwrap_or_else(|| std::path::Path::new("."))
.join(".ostp_public_ip");
std::fs::read_to_string(p).unwrap_or_else(|_| "127.0.0.1".to_string()).trim().to_string()
});
match ostp_core::dnstt::spawn_server(&pub_ip, 50000, &dns.privkey, debug) {
Ok(guard) => {
tracing::info!("dnstt-server initialized on {}:53 with pubkey: {}", pub_ip, dns.pubkey);
Some(guard)
}
Err(e) => {
tracing::error!("Failed to initialize dnstt-server: {}", e);
None
}
}
} else {
None
};
// Background config hot-reloader for access keys
let shared_keys_clone = shared_keys.clone();
let user_stats_clone = dispatcher.user_stats_ref();
@ -486,9 +455,17 @@ async fn run_server_loop(
if let Some(dns_cfg) = dns_transport {
if dns_cfg.enabled {
// DNS transport is now handled entirely by dnstt-server launched at startup.
// We just trace it here.
tracing::info!("DNS Transport via dnstt is enabled");
let dns_udp_tx = udp_tx.clone();
let dns_tcp_map = tcp_map.clone();
let dns_ui_tx = ui_event_tx.clone();
tokio::spawn(async move {
crate::transport::dns::start_dns_transport_server(
dns_cfg,
dns_udp_tx,
dns_tcp_map,
dns_ui_tx,
).await;
});
}
}
@ -608,11 +585,7 @@ async fn handle_udp_packet(
if !peer_available.get(&peer_ip).copied().unwrap_or(false) {
peer_available.insert(peer_ip, true);
let is_tcp = tcp_map.read().await.contains_key(&peer_addr);
let is_dns = match peer_ip {
std::net::IpAddr::V4(v4) => v4.octets()[0] == 10 && v4.octets()[1] == 255,
_ => false,
};
let proto = if is_dns { "DNS-tunnel" } else if is_tcp { "TCP (UoT)" } else { "UDP" };
let proto = if is_tcp { "TCP (UoT)" } else { "UDP" };
let _ = ui_event_tx.send(UiEvent::Log(format!("Client {peer_ip} connected via {proto}")));
}
@ -636,21 +609,7 @@ async fn handle_udp_packet(
}
}
if !sent_tcp {
// Check if this is a DNS tunnel virtual IP (10.255.x.x)
let is_dns_ip = match peer_addr.ip() {
std::net::IpAddr::V4(v4) => v4.octets()[0] == 10 && v4.octets()[1] == 255,
_ => false,
};
if is_dns_ip {
// Queue the packet for the next DNS poll query
let mut dq = crate::dns_queue().write().await;
let queue = dq.entry(peer_addr).or_insert_with(std::collections::VecDeque::new);
if queue.len() < 256 {
queue.push_back(resp);
}
} else {
let _ = socket.send_to(&resp, peer_addr).await?;
}
let _ = socket.send_to(&resp, peer_addr).await?;
}
let _ = ui_event_tx.send(UiEvent::Tx { peer: peer_ip, bytes: resp_len });
}
@ -677,9 +636,6 @@ async fn handle_udp_packet(
).await?;
}
}
Ok(DispatchOutcome::Ignored) => {
// Handshake replay, safely ignored
}
Err(err) => {
let _ = ui_event_tx.send(UiEvent::Log(format!("Protocol error for {peer}: {err}")));
}
@ -716,19 +672,7 @@ async fn handle_tick(
}
}
if !sent_tcp {
let is_dns_ip = match peer_addr.ip() {
std::net::IpAddr::V4(v4) => v4.octets()[0] == 10 && v4.octets()[1] == 255,
_ => false,
};
if is_dns_ip {
let mut dq = crate::dns_queue().write().await;
let queue = dq.entry(peer_addr).or_insert_with(std::collections::VecDeque::new);
if queue.len() < 256 {
queue.push_back(frame);
}
} else {
let _ = socket.send_to(&frame, peer_addr).await;
}
let _ = socket.send_to(&frame, peer_addr).await?;
}
}
for sid in dropped_sessions {

View File

@ -247,58 +247,18 @@ pub async fn send_relay_to_stream(
tcp_map: &std::sync::Arc<tokio::sync::RwLock<HashMap<std::net::SocketAddr, tokio::sync::mpsc::Sender<Bytes>>>>,
) -> Result<()> {
let payload = Bytes::from(msg.encode());
for (frame, peer_addr) in dispatcher.outbound_to_session(session_id, stream_id, payload)? {
if let Some((frame, peer_addr)) = dispatcher.outbound_to_session(session_id, stream_id, payload)? {
let response_len = frame.len();
let mut sent_tcp = false;
{
let map = tcp_map.read().await;
if let Some(tx) = map.get(&peer_addr) {
// Use a bounded async send with a generous timeout instead of try_send.
// try_send silently drops frames when the channel is full (common with
// bursty traffic), causing spurious retransmits and throughput collapse.
// 200ms matches roughly one RTO — if we can't deliver in that window
// the receiver is definitely stalled and we should log it.
let tx = tx.clone();
let frame_clone = frame.clone();
match tokio::time::timeout(
std::time::Duration::from_millis(200),
tx.send(frame_clone),
).await {
Ok(Ok(())) => { sent_tcp = true; }
Ok(Err(_)) => {
tracing::warn!(
"relay: TCP channel closed for peer={}, frame dropped (session={}, stream={})",
peer_addr, session_id, stream_id
);
sent_tcp = true; // channel gone, don't fall through to UDP
}
Err(_timeout) => {
tracing::warn!(
"relay: TCP channel full / timeout for peer={}, falling back to UDP (session={}, stream={})",
peer_addr, session_id, stream_id
);
// sent_tcp stays false → will fall through to UDP send below
}
}
let _ = tx.try_send(frame.clone());
sent_tcp = true;
}
}
if !sent_tcp {
let is_dns_ip = match peer_addr.ip() {
std::net::IpAddr::V4(v4) => v4.octets()[0] == 10 && v4.octets()[1] == 255,
_ => false,
};
if is_dns_ip {
// DNS virtual IP — queue for next poll
let mut dq = crate::dns_queue().write().await;
let queue = dq.entry(peer_addr).or_insert_with(std::collections::VecDeque::new);
if queue.len() < 256 {
queue.push_back(frame);
} else {
tracing::warn!("relay: dns_queue full for peer={}, frame dropped", peer_addr);
}
} else {
let _ = socket.send_to(&frame, peer_addr).await;
}
let _ = socket.send_to(&frame, peer_addr).await?;
}
let _ = ui_event_tx.send(UiEvent::Tx {
peer: peer_addr.ip(),
@ -307,4 +267,3 @@ pub async fn send_relay_to_stream(
}
Ok(())
}

View File

@ -1 +1,346 @@
// Left empty by request
/// DNS tunnel transport — dnstt-style server implementation.
///
/// Each DNS TXT query from client contains a framed chunk:
/// Base32([client_id: 8][msg_id: 2 BE][total_frags: 1][frag_idx: 1][payload: ≤120])
///
/// Server:
/// 1. Decodes ClientID + fragment from query name
/// 2. Reassembles fragments per (client_id, msg_id)
/// 3. Forwards complete OSTP packet to dispatcher (udp_tx)
/// 4. Waits up to MAX_RESPONSE_DELAY for responses
/// 5. Bundles responses as length-prefixed packets in DNS TXT answer
///
/// Server → client data in TXT rdata: [len_hi][len_lo][data...]...
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use bytes::Bytes;
use tokio::net::UdpSocket;
use tokio::sync::{mpsc, RwLock};
use tokio::time::Duration;
use ostp_core::dns::{base32_decode, DnsPacket, DnsRecordType};
use crate::config::DnsTransportConfig;
use crate::UiEvent;
const CLIENT_ID_LEN: usize = 8;
const HEADER_LEN: usize = CLIENT_ID_LEN + 4; // client_id + msg_id(2) + total(1) + idx(1)
/// How long to wait for downstream OSTP data before sending an empty response.
const MAX_RESPONSE_DELAY: Duration = Duration::from_millis(800);
/// Maximum number of response packets to bundle into one DNS answer.
const MAX_RESPONSE_PACKETS: usize = 8;
/// How long to keep per-client reassembly state without activity.
const CLIENT_EXPIRY: Duration = Duration::from_secs(30);
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
struct ClientId([u8; CLIENT_ID_LEN]);
struct ReassemblyState {
total: u8,
frags: Vec<Option<Vec<u8>>>,
received: u8,
}
impl ReassemblyState {
fn new(total: u8) -> Self {
Self {
total,
frags: vec![None; total as usize],
received: 0,
}
}
fn insert(&mut self, idx: u8, payload: Vec<u8>) -> bool {
let idx = idx as usize;
if idx >= self.frags.len() { return false; }
if self.frags[idx].is_none() {
self.frags[idx] = Some(payload);
self.received += 1;
}
self.received >= self.total
}
fn assemble(self) -> Option<Vec<u8>> {
let mut out = Vec::new();
for frag in self.frags {
out.extend_from_slice(&frag?);
}
Some(out)
}
}
struct ClientState {
/// msg_id → reassembly buffer
reassembly: HashMap<u16, ReassemblyState>,
/// Channel to push pending responses into; DNS handler polls this per-query
#[allow(dead_code)]
resp_tx: mpsc::Sender<Bytes>,
last_seen: std::time::Instant,
}
pub(crate) async fn start_dns_transport_server(
config: DnsTransportConfig,
udp_tx: mpsc::Sender<(Bytes, SocketAddr)>,
tcp_map: Arc<RwLock<HashMap<SocketAddr, mpsc::Sender<Bytes>>>>,
ui_event_tx: mpsc::UnboundedSender<UiEvent>,
) {
let listen_addr = if config.listen.contains(':') {
config.listen.clone()
} else {
format!("0.0.0.0:{}", config.listen)
};
let socket = match UdpSocket::bind(&listen_addr).await {
Ok(s) => Arc::new(s),
Err(e) => {
tracing::error!("DNS Transport failed to bind to {}: {}", listen_addr, e);
let _ = ui_event_tx.send(UiEvent::Log(format!("DNS Transport failed to bind: {}", e)));
return;
}
};
tracing::info!("DNS Transport listening on {}", listen_addr);
let _ = ui_event_tx.send(UiEvent::Log(format!("DNS Transport listening on {}", listen_addr)));
// Per-client state: ClientId → ClientState
// Access is serialised by a single Mutex so fragments from the same client
// are always reassembled atomically.
let clients: Arc<tokio::sync::Mutex<HashMap<ClientId, ClientState>>> =
Arc::new(tokio::sync::Mutex::new(HashMap::new()));
// Cleanup task: evict stale client state
{
let clients_gc = clients.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(15)).await;
let mut map = clients_gc.lock().await;
map.retain(|_, v| v.last_seen.elapsed() < CLIENT_EXPIRY);
}
});
}
let base_domain = config.domain.clone();
let mut buf = vec![0u8; 65535];
loop {
let (size, peer) = match socket.recv_from(&mut buf).await {
Ok(v) => v,
Err(e) => {
tracing::warn!("DNS Transport recv error: {}", e);
continue;
}
};
let packet_bytes = buf[..size].to_vec();
let udp_tx = udp_tx.clone();
let tcp_map = tcp_map.clone();
let socket = socket.clone();
let clients = clients.clone();
let base_domain = base_domain.clone();
tokio::spawn(async move {
handle_dns_query(
packet_bytes, peer,
udp_tx, tcp_map, socket, clients, base_domain,
).await;
});
}
}
async fn handle_dns_query(
packet_bytes: Vec<u8>,
peer: SocketAddr,
udp_tx: mpsc::Sender<(Bytes, SocketAddr)>,
tcp_map: Arc<RwLock<HashMap<SocketAddr, mpsc::Sender<Bytes>>>>,
socket: Arc<UdpSocket>,
clients: Arc<tokio::sync::Mutex<HashMap<ClientId, ClientState>>>,
base_domain: String,
) {
let dns_req = match DnsPacket::decode(&packet_bytes) {
Some(p) => p,
None => {
tracing::debug!("DNS: failed to decode packet from {}", peer);
return;
}
};
if dns_req.questions.is_empty() { return; }
let query = &dns_req.questions[0];
if query.qtype != DnsRecordType::TXT && query.qtype != DnsRecordType::NULL {
let resp = build_dns_response(&dns_req, &query.name, query.qtype.clone(), vec![]);
let _ = socket.send_to(&resp, peer).await;
return;
}
if !query.name.ends_with(&base_domain) {
let mut resp = DnsPacket::new_response(dns_req.id, &query.name, query.qtype.clone(), vec![]);
resp.flags = 0x8183; // NXDOMAIN
let _ = socket.send_to(&resp.encode(), peer).await;
return;
}
// Strip base domain and labels separator to get base32 subdomain
let subdomain = {
let name_lower = query.name.to_lowercase();
let suffix = format!(".{}", base_domain.to_lowercase());
let suffix_bare = base_domain.to_lowercase();
let stripped = if name_lower.ends_with(&suffix) {
&query.name[..name_lower.len() - suffix.len()]
} else if name_lower == suffix_bare {
""
} else {
return;
};
// Remove dots (label separators) to get contiguous base32
stripped.replace('.', "")
};
if subdomain.is_empty() {
// Pure poll — no payload
let resp = build_dns_response(&dns_req, &query.name, query.qtype.clone(), vec![]);
let _ = socket.send_to(&resp, peer).await;
return;
}
// Base32-decode
let raw = match base32_decode(&subdomain) {
Some(b) => b,
None => {
tracing::debug!("DNS: base32 decode failed from {}", peer);
return;
}
};
if raw.len() < HEADER_LEN {
tracing::debug!("DNS: frame too short ({} bytes) from {}", raw.len(), peer);
return;
}
// Parse header
let client_id = ClientId(raw[..CLIENT_ID_LEN].try_into().unwrap());
let msg_id = u16::from_be_bytes([raw[8], raw[9]]);
let total_frags = raw[10];
let frag_idx = raw[11];
let payload = raw[HEADER_LEN..].to_vec();
let fake_peer = client_id_to_fake_addr(&client_id);
tracing::trace!("DNS: client={} msg={} frag={}/{} payload={}B",
hex::encode(&client_id.0), msg_id, frag_idx + 1, total_frags, payload.len());
// ── Reassembly ────────────────────────────────────────────────────────────
let complete_packet: Option<Vec<u8>> = {
let mut map = clients.lock().await;
let state = map.entry(client_id).or_insert_with(|| {
let (resp_tx, _) = mpsc::channel(64); // placeholder, will be replaced below
ClientState {
reassembly: HashMap::new(),
resp_tx,
last_seen: std::time::Instant::now(),
}
});
state.last_seen = std::time::Instant::now();
if total_frags == 0 {
// Empty poll — no data
None
} else if total_frags == 1 && payload.is_empty() {
// Poll with empty payload
None
} else {
let asm = state.reassembly
.entry(msg_id)
.or_insert_with(|| ReassemblyState::new(total_frags));
if asm.insert(frag_idx, payload) {
// All fragments received — assemble and remove
let complete = state.reassembly.remove(&msg_id)
.and_then(|s| s.assemble());
complete
} else {
None
}
}
};
// ── Create per-query response channel ────────────────────────────────────
// We use the stable fake_peer as the routing key in tcp_map.
// For each query we create a fresh one-shot channel.
let (resp_tx, mut resp_rx) = mpsc::channel::<Bytes>(MAX_RESPONSE_PACKETS);
tcp_map.write().await.insert(fake_peer, resp_tx.clone());
// ── Forward complete OSTP packet to dispatcher ────────────────────────────
if let Some(ostp_pkt) = complete_packet {
tracing::debug!("DNS: forwarding {}B OSTP packet from client={} to dispatcher",
ostp_pkt.len(), hex::encode(&client_id.0));
let _ = udp_tx.send((Bytes::from(ostp_pkt), fake_peer)).await;
}
// ── Wait for OSTP response(s) ─────────────────────────────────────────────
let mut responses: Vec<Bytes> = Vec::new();
let deadline = tokio::time::sleep(MAX_RESPONSE_DELAY);
tokio::pin!(deadline);
loop {
tokio::select! {
_ = &mut deadline => break,
resp = resp_rx.recv() => {
match resp {
Some(r) => {
responses.push(r);
if responses.len() >= MAX_RESPONSE_PACKETS { break; }
}
None => break,
}
}
}
}
// Only remove if it's still our channel
{
let mut map = tcp_map.write().await;
if let Some(existing_tx) = map.get(&fake_peer) {
if existing_tx.same_channel(&resp_tx) {
map.remove(&fake_peer);
}
}
}
// ── Build DNS TXT response ────────────────────────────────────────────────
// Bundle all response packets as length-prefixed data in TXT rdata:
// [len_hi][len_lo][data...]...
let mut rdata: Vec<u8> = Vec::new();
for r in &responses {
let len = r.len() as u16;
rdata.push((len >> 8) as u8);
rdata.push((len & 0xFF) as u8);
rdata.extend_from_slice(r);
}
tracing::trace!("DNS: responding to {} with {} OSTP packets ({} bytes rdata)",
peer, responses.len(), rdata.len());
let resp = build_dns_response(&dns_req, &query.name, query.qtype.clone(), rdata);
let _ = socket.send_to(&resp, peer).await;
}
/// Build a DNS response packet with the given TXT rdata.
fn build_dns_response(
req: &DnsPacket,
name: &str,
rtype: DnsRecordType,
rdata: Vec<u8>,
) -> Vec<u8> {
let resp = DnsPacket::new_response(req.id, name, rtype, rdata);
resp.encode()
}
fn client_id_to_fake_addr(client_id: &ClientId) -> SocketAddr {
let mut ip_bytes = [10, 255, 0, 0];
ip_bytes[2] = client_id.0[0];
ip_bytes[3] = client_id.0[1];
let port = u16::from_be_bytes([client_id.0[2], client_id.0[3]]);
let port = if port == 0 { 1 } else { port };
SocketAddr::from((ip_bytes, port))
}

View File

@ -1 +1,2 @@
pub mod uot;
pub mod dns;

View File

@ -11,11 +11,10 @@ path = "src/main.rs"
ostp-client = { path = "../ostp-client" }
tokio = { workspace = true }
anyhow = { workspace = true }
tracing = { workspace = true }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
portable-atomic = { workspace = true }
hex = "0.4"
chrono = "0.4"
[build-dependencies]
# no extra build deps needed; manifest is embedded via build.rs

View File

@ -2,16 +2,30 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use anyhow::Result;
use hex;
use ostp_client::ipc_crypto::{derive_key, IpcCrypto};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::Duration;
use std::io::Write as _;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpListener;
use tokio::sync::{watch, Mutex};
use tokio::net::TcpListener;
use portable_atomic::Ordering;
fn log_to_file(msg: &str) {
let msg = msg.to_string();
tokio::task::spawn_blocking(move || {
let path = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|d| d.join("ostp-helper.log")))
.unwrap_or_else(|| std::path::PathBuf::from("ostp-helper.log"));
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
let _ = writeln!(file, "[{}] {}", chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), msg);
}
});
}
#[derive(Deserialize)]
#[serde(tag = "cmd", rename_all = "lowercase")]
enum GuiCmd {
@ -58,22 +72,22 @@ async fn main() -> Result<()> {
let path = &args[i + 1];
if let Ok(content) = std::fs::read_to_string(path) {
expected_token = content.trim().to_string();
let _ = std::fs::remove_file(path);
let _ = std::fs::remove_file(path); // securely delete after reading
}
}
}
tracing::info!("helper started (TCP mode)");
log_to_file("Helper started (TCP mode)");
if expected_token.is_empty() {
tracing::error!("auth token is required (--token-file or OSTP_TUN_TOKEN)");
return Err(anyhow::anyhow!("auth token is required"));
log_to_file("FATAL: Auth token is required for security (--token-file or OSTP_TUN_TOKEN).");
return Err(anyhow::anyhow!("Auth token is required"));
}
if let Err(e) = run_server(expected_token, port).await {
tracing::error!("fatal: {}", e);
log_to_file(&format!("Fatal error: {}", e));
}
tracing::info!("helper exiting");
log_to_file("Helper exiting");
Ok(())
}
@ -84,26 +98,24 @@ async fn run_server(expected_token: String, port: u16) -> Result<()> {
metrics: None,
}));
let ipc_key = derive_key(&expected_token);
let crypto = IpcCrypto::new(&ipc_key);
let bind_addr = format!("127.0.0.1:{}", port);
tracing::info!("binding to {}", bind_addr);
log_to_file(&format!("Attempting to bind to {}", bind_addr));
let listener = TcpListener::bind(&bind_addr).await.map_err(|e| {
tracing::error!("bind failed: {}", e);
log_to_file(&format!("Bind failed: {}", e));
e
})?;
tracing::info!("listening, waiting for GUI connection");
log_to_file("Listening successfully");
// Wait for GUI to connect (60 second timeout)
let (socket, _) = match tokio::time::timeout(Duration::from_secs(60), listener.accept()).await {
Ok(Ok(s)) => s,
_ => {
tracing::warn!("no connection from GUI within 60s, exiting");
log_to_file("No connection from GUI within 60s, exiting");
return Ok(());
}
};
tracing::info!("GUI connected");
log_to_file("GUI connected via TCP");
let (reader_half, writer_half) = tokio::io::split(socket);
let writer = Arc::new(Mutex::new(writer_half));
@ -111,20 +123,12 @@ async fn run_server(expected_token: String, port: u16) -> Result<()> {
let send_msg = {
let writer = writer.clone();
let crypto = crypto.clone();
move |msg: HelperMsg| {
let writer = writer.clone();
let crypto = crypto.clone();
let json = serde_json::to_string(&msg).unwrap_or_default();
tokio::spawn(async move {
match crypto.encrypt(json.as_bytes()) {
Ok(enc) => {
let line = format!("{}\n", hex::encode(&enc));
let mut w = writer.lock().await;
let _ = w.write_all(line.as_bytes()).await;
}
Err(e) => tracing::error!("send_msg encrypt failed: {}", e),
}
let mut w = writer.lock().await;
let _ = w.write_all(format!("{}\n", json).as_bytes()).await;
});
}
};
@ -134,7 +138,7 @@ async fn run_server(expected_token: String, port: u16) -> Result<()> {
line.clear();
let n = reader.read_line(&mut line).await.unwrap_or(0);
if n == 0 {
tracing::info!("GUI disconnected, stopping tunnel");
log_to_file("GUI disconnected, stopping tunnel");
let mut st = state.lock().await;
if let Some(tx) = st.shutdown_tx.take() {
let _ = tx.send(true);
@ -145,23 +149,10 @@ async fn run_server(expected_token: String, port: u16) -> Result<()> {
let trimmed = line.trim();
if trimmed.is_empty() { continue; }
// Decrypt the hex-encoded encrypted command from the GUI
let decrypted_json = match hex::decode(trimmed)
.ok()
.and_then(|enc| crypto.decrypt(&enc).ok())
.and_then(|dec| String::from_utf8(dec).ok())
{
Some(s) => s,
None => {
tracing::warn!("received undecodable command, ignoring");
continue;
}
};
let cmd: GuiCmd = match serde_json::from_str(&decrypted_json) {
let cmd: GuiCmd = match serde_json::from_str(trimmed) {
Ok(c) => c,
Err(e) => {
send_msg(HelperMsg::Error { message: format!("bad command: {}", e) });
send_msg(HelperMsg::Error { message: format!("Bad command: {}", e) });
continue;
}
};
@ -169,11 +160,11 @@ async fn run_server(expected_token: String, port: u16) -> Result<()> {
match cmd {
GuiCmd::Start { config, token } => {
if token != expected_token {
tracing::warn!("START command with invalid token");
send_msg(HelperMsg::Error { message: "invalid authorization token".to_string() });
log_to_file("Received START command with invalid token");
send_msg(HelperMsg::Error { message: "Invalid authorization token".to_string() });
continue;
}
tracing::info!("received START command");
log_to_file("Received START command");
{
let mut st = state.lock().await;
if let Some(tx) = st.shutdown_tx.take() {
@ -185,8 +176,8 @@ async fn run_server(expected_token: String, port: u16) -> Result<()> {
let cfg: ostp_client::config::ClientConfig = match serde_json::from_str(&config) {
Ok(c) => c,
Err(e) => {
tracing::error!("config parse error: {}", e);
send_msg(HelperMsg::Error { message: format!("config parse error: {}", e) });
log_to_file(&format!("Config parse error: {}", e));
send_msg(HelperMsg::Error { message: format!("Config parse error: {}", e) });
continue;
}
};
@ -210,26 +201,21 @@ async fn run_server(expected_token: String, port: u16) -> Result<()> {
let metrics_for_runner = metrics.clone();
let writer_for_err = writer.clone();
let crypto_for_err = crypto.clone();
let shutdown_rx_for_core = shutdown_rx.clone();
tokio::spawn(async move {
tracing::info!("starting tunnel core");
log_to_file("Starting tunnel core...");
match ostp_client::runner::run_client_core(cfg, metrics_for_runner, shutdown_rx_for_core, Some(config_rx)).await {
Ok(_) => tracing::info!("tunnel core stopped normally"),
Ok(_) => { log_to_file("Tunnel core stopped normally"); }
Err(e) => {
tracing::error!("tunnel core error: {}", e);
let json = serde_json::to_string(&HelperMsg::Error { message: e.to_string() })
.unwrap_or_default();
if let Ok(enc) = crypto_for_err.encrypt(json.as_bytes()) {
let mut w = writer_for_err.lock().await;
let _ = w.write_all(format!("{}\n", hex::encode(&enc)).as_bytes()).await;
}
log_to_file(&format!("Tunnel core error: {}", e));
let json = serde_json::to_string(&HelperMsg::Error { message: e.to_string() }).unwrap_or_default();
let mut w = writer_for_err.lock().await;
let _ = w.write_all(format!("{}\n", json).as_bytes()).await;
}
}
});
let writer_tick = writer.clone();
let crypto_tick = crypto.clone();
let metrics_tick = metrics.clone();
let mut shutdown_rx_tick = shutdown_rx.clone();
tokio::spawn(async move {
@ -241,28 +227,21 @@ async fn run_server(expected_token: String, port: u16) -> Result<()> {
if *shutdown_rx_tick.borrow() { break; }
}
}
let cs = metrics_tick.connection_state.load(Ordering::Relaxed);
let sent = metrics_tick.bytes_sent.load(Ordering::Relaxed);
let recv = metrics_tick.bytes_recv.load(Ordering::Relaxed);
let rtt = metrics_tick.rtt_ms.load(Ordering::Relaxed);
let mut msgs: Vec<HelperMsg> = Vec::new();
let mut w = writer_tick.lock().await;
if cs != last_state {
last_state = cs;
msgs.push(HelperMsg::Status { value: cs });
}
msgs.push(HelperMsg::Metrics { bytes_sent: sent, bytes_recv: recv, rtt_ms: rtt });
let mut w = writer_tick.lock().await;
for msg in msgs {
let json = serde_json::to_string(&msg).unwrap_or_default();
if let Ok(enc) = crypto_tick.encrypt(json.as_bytes()) {
if w.write_all(format!("{}\n", hex::encode(&enc)).as_bytes()).await.is_err() {
return;
}
}
let json = serde_json::to_string(&HelperMsg::Status { value: cs }).unwrap_or_default();
if w.write_all(format!("{}\n", json).as_bytes()).await.is_err() { break; }
}
let json = serde_json::to_string(&HelperMsg::Metrics { bytes_sent: sent, bytes_recv: recv, rtt_ms: rtt }).unwrap_or_default();
if w.write_all(format!("{}\n", json).as_bytes()).await.is_err() { break; }
drop(w);
}
});
@ -271,15 +250,15 @@ async fn run_server(expected_token: String, port: u16) -> Result<()> {
}
GuiCmd::Reload { config, token } => {
if token != expected_token {
send_msg(HelperMsg::Error { message: "invalid authorization token".to_string() });
send_msg(HelperMsg::Error { message: "Invalid authorization token".to_string() });
continue;
}
tracing::info!("received RELOAD command");
log_to_file("Received RELOAD command");
let cfg: ostp_client::config::ClientConfig = match serde_json::from_str(&config) {
Ok(c) => c,
Err(e) => {
send_msg(HelperMsg::Error { message: format!("config parse error during reload: {}", e) });
send_msg(HelperMsg::Error { message: format!("Config parse error during reload: {}", e) });
continue;
}
};
@ -288,7 +267,7 @@ async fn run_server(expected_token: String, port: u16) -> Result<()> {
let st = state.lock().await;
if let Some(tx) = &st.config_tx {
let _ = tx.send(cfg);
tracing::info!("config sent to running core for hot-reload");
log_to_file("Config sent to running core for seamless hot-reload");
}
}
@ -296,11 +275,11 @@ async fn run_server(expected_token: String, port: u16) -> Result<()> {
}
GuiCmd::Stop { token } => {
if token != expected_token {
tracing::warn!("STOP command with invalid token");
send_msg(HelperMsg::Error { message: "invalid authorization token".to_string() });
log_to_file("Received STOP command with invalid token");
send_msg(HelperMsg::Error { message: "Invalid authorization token".to_string() });
continue;
}
tracing::info!("received STOP command");
log_to_file("Received STOP command");
let mut st = state.lock().await;
if let Some(tx) = st.shutdown_tx.take() {
let _ = tx.send(true);

View File

@ -1464,15 +1464,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 (dns_priv, dns_pub) = if is_server {
ostp_core::dnstt::generate_keypair().unwrap_or_else(|e| {
tracing::warn!("Failed to generate dnstt keys: {}. Using placeholders.", e);
("YOUR_PRIVKEY".to_string(), "YOUR_PUBKEY".to_string())
})
} else {
("".to_string(), "".to_string())
};
let dns_pub = generate_secure_key("base64");
let dns_priv = generate_secure_key("base64");
let content = if is_server {
format!(r#"{{
// OSTP Server Configuration