mirror of https://github.com/ospab/ostp.git
Compare commits
No commits in common. "e7a4f2b4a4251eac5403238e7b825f87a3b19178" and "9a891310f9ce17661870201a818003072dede91b" have entirely different histories.
e7a4f2b4a4
...
9a891310f9
|
|
@ -2,5 +2,5 @@
|
||||||
"target_version": "0.4.2",
|
"target_version": "0.4.2",
|
||||||
"branch": "beta",
|
"branch": "beta",
|
||||||
"alpha_iteration": 0,
|
"alpha_iteration": 0,
|
||||||
"beta_iteration": 4
|
"beta_iteration": 2
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1400,7 +1400,6 @@ dependencies = [
|
||||||
"rlimit",
|
"rlimit",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,7 @@ impl Bridge {
|
||||||
self.handle_inbound_udp(udp_msg, &mut sessions_opt, &mut udp_rx_opt, &mut proxy_guard, &mut stream_map, &tx, &proxy_tx).await;
|
self.handle_inbound_udp(udp_msg, &mut sessions_opt, &mut udp_rx_opt, &mut proxy_guard, &mut stream_map, &tx, &proxy_tx).await;
|
||||||
}
|
}
|
||||||
cmd = bridge_rx.recv() => {
|
cmd = bridge_rx.recv() => {
|
||||||
if !self.handle_bridge_cmd(cmd, &mut bridge_rx, &mut sessions_opt, &mut udp_rx_opt, &mut proxy_guard, &mut stream_map, &tx, &proxy_tx).await {
|
if !self.handle_bridge_cmd(cmd, &mut sessions_opt, &mut udp_rx_opt, &mut proxy_guard, &mut stream_map, &tx, &proxy_tx).await {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -374,7 +374,6 @@ impl Bridge {
|
||||||
async fn handle_bridge_cmd(
|
async fn handle_bridge_cmd(
|
||||||
&mut self,
|
&mut self,
|
||||||
cmd: Option<BridgeCommand>,
|
cmd: Option<BridgeCommand>,
|
||||||
bridge_rx: &mut mpsc::Receiver<BridgeCommand>,
|
|
||||||
sessions_opt: &mut Option<Vec<SessionState>>,
|
sessions_opt: &mut Option<Vec<SessionState>>,
|
||||||
udp_rx_opt: &mut Option<mpsc::Receiver<(usize, Bytes)>>,
|
udp_rx_opt: &mut Option<mpsc::Receiver<(usize, Bytes)>>,
|
||||||
proxy_guard: &mut Option<crate::sysproxy::SystemProxyGuard>,
|
proxy_guard: &mut Option<crate::sysproxy::SystemProxyGuard>,
|
||||||
|
|
@ -466,32 +465,6 @@ impl Bridge {
|
||||||
tx.send(UiEvent::Log(format!("Obfuscation profile switched to {:?}", self.profile))).await.ok();
|
tx.send(UiEvent::Log(format!("Obfuscation profile switched to {:?}", self.profile))).await.ok();
|
||||||
}
|
}
|
||||||
Some(BridgeCommand::NetworkChanged) => {
|
Some(BridgeCommand::NetworkChanged) => {
|
||||||
// A real network handoff (Wi-Fi <-> cellular) commonly fires
|
|
||||||
// onLost + onAvailable within milliseconds of each other on
|
|
||||||
// Android, queuing several NetworkChanged commands back to
|
|
||||||
// back. Each reconnect below is a full sequential handshake
|
|
||||||
// (up to ~1.2s x 4 attempts x mux_sessions) run synchronously
|
|
||||||
// in this select-loop iteration, so without coalescing, the
|
|
||||||
// first attempt often races the OS's own network switch and
|
|
||||||
// fails on the now-dead interface, then the SECOND queued
|
|
||||||
// NetworkChanged only starts its own full reconnect after
|
|
||||||
// that first one finishes - multiplying a sub-second handoff
|
|
||||||
// into many seconds of extra outage. Drain same-kind repeats
|
|
||||||
// so a burst collapses into one reconnect on the freshest
|
|
||||||
// signal; a different command found while draining is
|
|
||||||
// handled immediately rather than dropped.
|
|
||||||
while let Ok(next) = bridge_rx.try_recv() {
|
|
||||||
if !matches!(next, BridgeCommand::NetworkChanged) {
|
|
||||||
let more = Box::pin(self.handle_bridge_cmd(
|
|
||||||
Some(next), bridge_rx, sessions_opt, udp_rx_opt, proxy_guard, stream_map, tx, proxy_tx,
|
|
||||||
)).await;
|
|
||||||
if !more {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.running {
|
if self.running {
|
||||||
let _ = tx.send(UiEvent::Log("Network changed — starting immediate reconnect".to_string())).await;
|
let _ = tx.send(UiEvent::Log("Network changed — starting immediate reconnect".to_string())).await;
|
||||||
self.metrics.connection_state.store(1, Ordering::Relaxed);
|
self.metrics.connection_state.store(1, Ordering::Relaxed);
|
||||||
|
|
|
||||||
|
|
@ -361,10 +361,6 @@ async fn handle_udp_associate(
|
||||||
|
|
||||||
let mut direct_udp_v4: Option<Arc<UdpSocket>> = None;
|
let mut direct_udp_v4: Option<Arc<UdpSocket>> = None;
|
||||||
let mut direct_udp_v6: Option<Arc<UdpSocket>> = None;
|
let mut direct_udp_v6: Option<Arc<UdpSocket>> = None;
|
||||||
// Held only to keep the direct-UDP readers' cancellation senders alive;
|
|
||||||
// dropping this (on every return path from this function) is what tells
|
|
||||||
// spawn_direct_udp_reader's tasks to stop. See its doc comment.
|
|
||||||
let mut direct_udp_cancel_txs: Vec<tokio::sync::oneshot::Sender<()>> = Vec::new();
|
|
||||||
|
|
||||||
let mut tcp_buf = [0u8; 1];
|
let mut tcp_buf = [0u8; 1];
|
||||||
loop {
|
loop {
|
||||||
|
|
@ -436,9 +432,7 @@ async fn handle_udp_associate(
|
||||||
match create_udp_socket_bypassing_tun(true, matcher.physical_if_index, &matcher.physical_if_name).await {
|
match create_udp_socket_bypassing_tun(true, matcher.physical_if_index, &matcher.physical_if_name).await {
|
||||||
Ok(s) => {
|
Ok(s) => {
|
||||||
let s_arc = Arc::new(s);
|
let s_arc = Arc::new(s);
|
||||||
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel();
|
spawn_direct_udp_reader(s_arc.clone(), sock_tx.clone(), client_udp_addr.clone(), debug);
|
||||||
spawn_direct_udp_reader(s_arc.clone(), sock_tx.clone(), client_udp_addr.clone(), debug, cancel_rx);
|
|
||||||
direct_udp_cancel_txs.push(cancel_tx);
|
|
||||||
direct_udp_v6 = Some(s_arc);
|
direct_udp_v6 = Some(s_arc);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -452,9 +446,7 @@ async fn handle_udp_associate(
|
||||||
match create_udp_socket_bypassing_tun(false, matcher.physical_if_index, &matcher.physical_if_name).await {
|
match create_udp_socket_bypassing_tun(false, matcher.physical_if_index, &matcher.physical_if_name).await {
|
||||||
Ok(s) => {
|
Ok(s) => {
|
||||||
let s_arc = Arc::new(s);
|
let s_arc = Arc::new(s);
|
||||||
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel();
|
spawn_direct_udp_reader(s_arc.clone(), sock_tx.clone(), client_udp_addr.clone(), debug);
|
||||||
spawn_direct_udp_reader(s_arc.clone(), sock_tx.clone(), client_udp_addr.clone(), debug, cancel_rx);
|
|
||||||
direct_udp_cancel_txs.push(cancel_tx);
|
|
||||||
direct_udp_v4 = Some(s_arc);
|
direct_udp_v4 = Some(s_arc);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -528,24 +520,11 @@ fn spawn_direct_udp_reader(
|
||||||
sock_tx: Arc<UdpSocket>,
|
sock_tx: Arc<UdpSocket>,
|
||||||
client_udp_addr: Arc<std::sync::Mutex<Option<std::net::SocketAddr>>>,
|
client_udp_addr: Arc<std::sync::Mutex<Option<std::net::SocketAddr>>>,
|
||||||
_debug: bool,
|
_debug: bool,
|
||||||
mut cancel_rx: tokio::sync::oneshot::Receiver<()>,
|
|
||||||
) {
|
) {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut buf = vec![0u8; 65536];
|
let mut buf = vec![0u8; 65536];
|
||||||
loop {
|
loop {
|
||||||
let recv_result = tokio::select! {
|
match direct_socket.recv_from(&mut buf).await {
|
||||||
// Fires as soon as the sender half (held by handle_udp_associate
|
|
||||||
// for exactly this reason) is dropped - which happens the
|
|
||||||
// instant that function returns, on every exit path, with no
|
|
||||||
// explicit signaling needed. Without this, a UDP-associate
|
|
||||||
// session that ever bypassed traffic direct (excluded IP/
|
|
||||||
// domain) leaked this socket + task for the rest of the
|
|
||||||
// process's life once the session ended: nothing else ever
|
|
||||||
// stopped this loop.
|
|
||||||
_ = &mut cancel_rx => break,
|
|
||||||
res = direct_socket.recv_from(&mut buf) => res,
|
|
||||||
};
|
|
||||||
match recv_result {
|
|
||||||
Ok((len, target_addr)) => {
|
Ok((len, target_addr)) => {
|
||||||
let client_addr = {
|
let client_addr = {
|
||||||
let guard = client_udp_addr.lock().unwrap();
|
let guard = client_udp_addr.lock().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -43,11 +43,6 @@ pub struct CongestionController {
|
||||||
mtu: u64,
|
mtu: u64,
|
||||||
/// Min RTT expiry: re-probe after 10 seconds
|
/// Min RTT expiry: re-probe after 10 seconds
|
||||||
min_rtt_stamp: Instant,
|
min_rtt_stamp: Instant,
|
||||||
/// Loss events counted toward SLOW_START_LOSS_TOLERANCE within the
|
|
||||||
/// current SLOW_START_LOSS_WINDOW (see on_loss's SlowStart arm).
|
|
||||||
slow_start_losses: u32,
|
|
||||||
/// Start of the current loss-tolerance window.
|
|
||||||
slow_start_loss_window_start: Instant,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|
@ -72,24 +67,6 @@ const RTO_MAX: Duration = Duration::from_secs(16);
|
||||||
/// Will be replaced by first real measurement within milliseconds.
|
/// Will be replaced by first real measurement within milliseconds.
|
||||||
const INITIAL_RTT: Duration = Duration::from_millis(30);
|
const INITIAL_RTT: Duration = Duration::from_millis(30);
|
||||||
|
|
||||||
/// Isolated packet loss during slow start (a single dropped frame from
|
|
||||||
/// wireless noise, a brief LTE handover blip, etc.) is normal on real
|
|
||||||
/// mobile/Wi-Fi links and does NOT mean the link is congested. The previous
|
|
||||||
/// behavior exited slow start and halved cwnd on the very FIRST loss, which
|
|
||||||
/// on any link with a non-zero background loss rate permanently downgrades
|
|
||||||
/// the session from exponential growth to linear (+1 MTU/RTT) ProbeBandwidth
|
|
||||||
/// growth within the first few RTTs - turning what should be a sub-second
|
|
||||||
/// ramp-up into tens of seconds to minutes before throughput opens up
|
|
||||||
/// (observed as: a trickle of KB/s, then a sudden jump once cwnd finally
|
|
||||||
/// claws back up). Only treat loss as a real congestion signal - and pay
|
|
||||||
/// the full slow-start-exit + halving cost - once this many losses land
|
|
||||||
/// within SLOW_START_LOSS_WINDOW.
|
|
||||||
const SLOW_START_LOSS_TOLERANCE: u32 = 3;
|
|
||||||
/// Window within which SLOW_START_LOSS_TOLERANCE losses must land to count
|
|
||||||
/// as sustained (rather than isolated) loss. Roughly a few RTTs on a
|
|
||||||
/// well-connected link, generous on a slow one.
|
|
||||||
const SLOW_START_LOSS_WINDOW: Duration = Duration::from_millis(500);
|
|
||||||
|
|
||||||
impl CongestionController {
|
impl CongestionController {
|
||||||
pub fn new(mtu: u64) -> Self {
|
pub fn new(mtu: u64) -> Self {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
|
|
@ -111,8 +88,6 @@ impl CongestionController {
|
||||||
pacing_rate: initial_pacing,
|
pacing_rate: initial_pacing,
|
||||||
mtu,
|
mtu,
|
||||||
min_rtt_stamp: now,
|
min_rtt_stamp: now,
|
||||||
slow_start_losses: 0,
|
|
||||||
slow_start_loss_window_start: now,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,28 +197,11 @@ impl CongestionController {
|
||||||
|
|
||||||
match self.phase {
|
match self.phase {
|
||||||
Phase::SlowStart => {
|
Phase::SlowStart => {
|
||||||
let now = Instant::now();
|
// Exit slow start, set ssthresh to half of cwnd
|
||||||
if now.duration_since(self.slow_start_loss_window_start) > SLOW_START_LOSS_WINDOW {
|
self.ssthresh = self.cwnd / 2;
|
||||||
// Previous window's losses have aged out - this loss starts a fresh count.
|
self.cwnd = self.ssthresh.max(MIN_CWND_PACKETS * self.mtu);
|
||||||
self.slow_start_losses = 0;
|
self.phase = Phase::ProbeBandwidth;
|
||||||
self.slow_start_loss_window_start = now;
|
tracing::debug!(cwnd = self.cwnd, ssthresh = self.ssthresh, "congestion: loss during slow start");
|
||||||
}
|
|
||||||
self.slow_start_losses += 1;
|
|
||||||
|
|
||||||
if self.slow_start_losses >= SLOW_START_LOSS_TOLERANCE {
|
|
||||||
// Sustained loss within the window: treat as real congestion.
|
|
||||||
// Exit slow start, set ssthresh to half of cwnd.
|
|
||||||
self.ssthresh = self.cwnd / 2;
|
|
||||||
self.cwnd = self.ssthresh.max(MIN_CWND_PACKETS * self.mtu);
|
|
||||||
self.phase = Phase::ProbeBandwidth;
|
|
||||||
tracing::debug!(cwnd = self.cwnd, ssthresh = self.ssthresh, "congestion: sustained loss during slow start, exiting");
|
|
||||||
} else {
|
|
||||||
// Isolated loss: likely non-congestive noise. Take a mild,
|
|
||||||
// temporary haircut but keep exponential growth going -
|
|
||||||
// don't throw away slow start over a single dropped frame.
|
|
||||||
self.cwnd = (self.cwnd * 8 / 10).max(MIN_CWND_PACKETS * self.mtu);
|
|
||||||
tracing::debug!(cwnd = self.cwnd, count = self.slow_start_losses, "congestion: isolated loss during slow start, staying in slow start");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Phase::ProbeBandwidth => {
|
Phase::ProbeBandwidth => {
|
||||||
// Multiplicative decrease: cwnd *= 0.7 (BBR-style, less aggressive than Cubic's 0.5)
|
// Multiplicative decrease: cwnd *= 0.7 (BBR-style, less aggressive than Cubic's 0.5)
|
||||||
|
|
@ -332,50 +290,6 @@ mod tests {
|
||||||
assert!(cc.cwnd() < initial);
|
assert!(cc.cwnd() < initial);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_isolated_slow_start_loss_does_not_exit_slow_start() {
|
|
||||||
// A single dropped packet (wireless noise, a brief handover blip) is
|
|
||||||
// normal on real links and must not permanently downgrade the
|
|
||||||
// session from exponential to linear growth.
|
|
||||||
let mut cc = CongestionController::new(1200);
|
|
||||||
cc.on_loss(1200);
|
|
||||||
assert_eq!(cc.phase, Phase::SlowStart, "one isolated loss must not exit slow start");
|
|
||||||
|
|
||||||
// It should still shrink the window somewhat (not ignored entirely),
|
|
||||||
// just far less punishing than the sustained-congestion case.
|
|
||||||
let after_one = cc.cwnd();
|
|
||||||
assert!(after_one < INITIAL_CWND_PACKETS * 1200);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sustained_slow_start_loss_exits_slow_start() {
|
|
||||||
// Losses landing close together (within SLOW_START_LOSS_WINDOW) are
|
|
||||||
// a real congestion signal and must still trigger the harsher
|
|
||||||
// exit-slow-start + halve response.
|
|
||||||
let mut cc = CongestionController::new(1200);
|
|
||||||
for _ in 0..SLOW_START_LOSS_TOLERANCE {
|
|
||||||
cc.on_loss(1200);
|
|
||||||
}
|
|
||||||
assert_eq!(cc.phase, Phase::ProbeBandwidth, "sustained loss must exit slow start");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_slow_start_loss_window_resets_after_expiry() {
|
|
||||||
// Two losses far enough apart (window expired between them) must
|
|
||||||
// each be treated as isolated, not accumulated toward the sustained-
|
|
||||||
// loss threshold.
|
|
||||||
let mut cc = CongestionController::new(1200);
|
|
||||||
cc.on_loss(1200);
|
|
||||||
assert_eq!(cc.phase, Phase::SlowStart);
|
|
||||||
|
|
||||||
// Simulate the window having expired by resetting its start
|
|
||||||
// directly (std::thread::sleep in a unit test would be flaky/slow).
|
|
||||||
cc.slow_start_loss_window_start = Instant::now() - SLOW_START_LOSS_WINDOW - Duration::from_millis(1);
|
|
||||||
cc.on_loss(1200);
|
|
||||||
assert_eq!(cc.phase, Phase::SlowStart, "a loss after the window expired must restart the count, not accumulate");
|
|
||||||
assert_eq!(cc.slow_start_losses, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_can_send_limits() {
|
fn test_can_send_limits() {
|
||||||
let mut cc = CongestionController::new(1200);
|
let mut cc = CongestionController::new(1200);
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 0.4.2+23
|
version: 0.4.2+21
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.4
|
sdk: ^3.11.4
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ use clap::Parser;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use sha2::Digest;
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(author, version, about = "OSTP Core - Ospab Stealth Transport Protocol", long_about = None)]
|
#[command(author, version, about = "OSTP Core - Ospab Stealth Transport Protocol", long_about = None)]
|
||||||
|
|
|
||||||
|
|
@ -115,18 +115,12 @@ if [ -n "$TARGET_VERSION" ]; then
|
||||||
fi
|
fi
|
||||||
echo "Fetching requested release $LATEST_RELEASE..."
|
echo "Fetching requested release $LATEST_RELEASE..."
|
||||||
else
|
else
|
||||||
if [ "$TARGET_BRANCH" == "alpha" ] || [ "$TARGET_BRANCH" == "beta" ]; then
|
if [ "$TARGET_BRANCH" == "alpha" ]; then
|
||||||
# There is no floating "alpha"/"beta" GitHub Release - gha.ps1 cuts a
|
echo "Fetching alpha release..."
|
||||||
# fresh versioned tag every time (v0.4.2-beta.4, v0.4.2-alpha.7, ...).
|
LATEST_RELEASE="alpha"
|
||||||
# /releases/latest only ever returns the newest NON-prerelease
|
elif [ "$TARGET_BRANCH" == "beta" ]; then
|
||||||
# (stable) tag, so it can't find these. Query the full releases list
|
echo "Fetching beta release..."
|
||||||
# (newest first) and take the first tag_name containing "-$TARGET_BRANCH".
|
LATEST_RELEASE="beta"
|
||||||
echo "Fetching latest ${TARGET_BRANCH} release..."
|
|
||||||
LATEST_RELEASE=$(curl -s "https://api.github.com/repos/${GITHUB_REPO}/releases" \
|
|
||||||
| grep '"tag_name":' \
|
|
||||||
| grep -- "-${TARGET_BRANCH}" \
|
|
||||||
| head -1 \
|
|
||||||
| sed -E 's/.*"tag_name": *"([^"]+)".*/\1/')
|
|
||||||
else
|
else
|
||||||
echo "Fetching latest stable release..."
|
echo "Fetching latest stable release..."
|
||||||
LATEST_RELEASE=$(curl -s "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
LATEST_RELEASE=$(curl -s "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue