diff --git a/Cargo.lock b/Cargo.lock index 52011f23f..09e13e45d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -755,9 +755,9 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.71.1" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ "bitflags 2.9.1", "cexpr", @@ -4276,11 +4276,11 @@ dependencies = [ [[package]] name = "kcp-sys" version = "0.1.0" -source = "git+https://github.com/rustdesk-org/kcp-sys#32a6c09fc6223f54aea83981a6aa8995931d29be" +source = "git+https://github.com/rustdesk-org/kcp-sys?branch=rustdesk-patches#acd13bad5248c7ea0f4c0595a627c308af210fb1" dependencies = [ "anyhow", "auto_impl", - "bindgen 0.71.1", + "bindgen 0.72.1", "bitflags 2.9.1", "bytes", "cc", diff --git a/Cargo.toml b/Cargo.toml index 3e6f6b6e9..6aad7be33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,7 +83,7 @@ fon = "0.6" shutdown_hooks = "0.1" totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] } stunclient = "0.4" -kcp-sys= { git = "https://github.com/rustdesk-org/kcp-sys"} +kcp-sys= { git = "https://github.com/rustdesk-org/kcp-sys", branch = "rustdesk-patches" } reqwest = { version = "0.12", features = ["blocking", "socks", "json", "native-tls", "rustls-tls", "rustls-tls-native-roots", "gzip", "zstd"], default-features=false } [target.'cfg(not(target_os = "linux"))'.dependencies] diff --git a/src/common.rs b/src/common.rs index 6e1b961b3..4d2a533f2 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,7 +1,7 @@ use std::{ collections::HashMap, future::Future, - net::{SocketAddr, ToSocketAddrs}, + net::SocketAddr, sync::{Arc, Mutex, RwLock}, task::Poll, }; @@ -2387,16 +2387,27 @@ pub fn is_udp_disabled() -> bool { Config::get_option(keys::OPTION_DISABLE_UDP) == "Y" } +pub const OPTION_ENABLE_KCP_CC: &str = "enable-kcp-congestion-control"; + +// Default ON ("enable-" option2bool semantics); set "N" to fall back to the pure +// turbo profile (nc=1, no congestion window). +#[inline] +pub fn get_kcp_cc_enabled() -> bool { + config::option2bool( + OPTION_ENABLE_KCP_CC, + &Config::get_option(OPTION_ENABLE_KCP_CC), + ) +} + // this crate https://github.com/yoshd/stun-client supports nat type async fn stun_ipv6_test(stun_server: &str) -> ResultType<(SocketAddr, String)> { - use std::net::ToSocketAddrs; use stunclient::StunClient; let local_addr = SocketAddr::from(([0u16; 8], 0)); // [::]:0 let socket = UdpSocket::bind(&local_addr).await?; - let Some(stun_addr) = stun_server - .to_socket_addrs()? - .filter(|x| x.is_ipv6()) - .next() + // Resolve via tokio so DNS never blocks the async runtime worker. + let Some(stun_addr) = tokio::net::lookup_host(stun_server) + .await? + .find(|x| x.is_ipv6()) else { bail!( "Failed to resolve STUN ipv6 server address: {}", @@ -2413,14 +2424,13 @@ async fn stun_ipv6_test(stun_server: &str) -> ResultType<(SocketAddr, String)> { } async fn stun_ipv4_test(stun_server: &str) -> ResultType<(SocketAddr, String)> { - use std::net::ToSocketAddrs; use stunclient::StunClient; let local_addr = SocketAddr::from(([0u8; 4], 0)); let socket = UdpSocket::bind(&local_addr).await?; - let Some(stun_addr) = stun_server - .to_socket_addrs()? - .filter(|x| x.is_ipv4()) - .next() + // Resolve via tokio so DNS never blocks the async runtime worker. + let Some(stun_addr) = tokio::net::lookup_host(stun_server) + .await? + .find(|x| x.is_ipv4()) else { bail!( "Failed to resolve STUN ipv4 server address: {}", @@ -2432,7 +2442,7 @@ async fn stun_ipv4_test(stun_server: &str) -> ResultType<(SocketAddr, String)> { Ok(if addr.ip().is_ipv4() { (addr, stun_server.to_owned()) } else { - bail!("STUN server returned non-IPv6 address: {}", addr) + bail!("STUN server returned non-IPv4 address: {}", addr) }) } @@ -2471,10 +2481,9 @@ pub async fn test_nat_ipv4() -> ResultType<(SocketAddr, String)> { async fn test_bind_ipv6() -> ResultType { let local_addr = SocketAddr::from(([0u16; 8], 0)); // [::]:0 let socket = UdpSocket::bind(local_addr).await?; - let addr = STUNS_V6[0] - .to_socket_addrs()? - .filter(|x| x.is_ipv6()) - .next() + let addr = tokio::net::lookup_host(STUNS_V6[0]) + .await? + .find(|x| x.is_ipv6()) .ok_or_else(|| { anyhow!( "Failed to resolve STUN ipv6 server address: {}", @@ -2605,7 +2614,14 @@ pub async fn punch_udp( } } res = socket.recv(&mut data) => match res { - Err(e) => bail!("UDP punch failed, {packets_sent} packets sent: {e}"), + Err(e) => { + // While the hole is still forming, ICMP unreachable from the peer's NAT + // is expected and surfaces as ConnectionReset/Refused on a connected + // socket (notably 10054 on Windows). Treat it as loss and keep punching; + // MAX_TIME above still bounds the whole attempt. + log::debug!("UDP punch recv error (treated as loss): {e}"); + hbb_common::sleep(0.01).await; + } Ok(n) => { // log::debug!("UDP punch succeeded after sending {} packets after {:?}", packets_sent, tm.elapsed()); if listen { diff --git a/src/kcp_stream.rs b/src/kcp_stream.rs index 74bd84130..fac98b89c 100644 --- a/src/kcp_stream.rs +++ b/src/kcp_stream.rs @@ -20,6 +20,22 @@ pub struct KcpStream { } impl KcpStream { + // Engage KCP's built-in congestion control (nc=0) unless disabled by option: pure turbo + // (nc=1) keeps blasting a full 1024-segment window through loss, which on constrained + // links amplifies brief loss into a spiral users experience as stalls or drops. This is + // sender-side only, so no wire negotiation is needed and either peer may run either + // profile. Requires kcp-sys from the `rustdesk-patches` branch, which wires the config + // factory into connection setup (on older revs the factory was stored but never consulted). + fn apply_kcp_config(endpoint: &mut KcpEndpoint) { + if crate::get_kcp_cc_enabled() { + endpoint.set_kcp_config_factory(Box::new(|conv| { + let mut config = kcp_sys::ffi_safe::KcpConfig::new_turbo(conv); + config.nc = Some(0); + config + })); + } + } + fn create_framed(stream: stream::KcpStream, local_addr: Option) -> Stream { Stream::Tcp(FramedStream( tokio_util::codec::Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), @@ -35,6 +51,7 @@ impl KcpStream { init_packet: Option, ) -> ResultType<(Self, Stream)> { let mut endpoint = KcpEndpoint::new(); + Self::apply_kcp_config(&mut endpoint); endpoint.run().await; let (input, output) = ( @@ -70,6 +87,7 @@ impl KcpStream { timeout: std::time::Duration, ) -> ResultType<(Self, Stream)> { let mut endpoint = KcpEndpoint::new(); + Self::apply_kcp_config(&mut endpoint); endpoint.run().await; let (input, output) = ( @@ -104,6 +122,13 @@ impl KcpStream { let udp = udp_socket.clone(); tokio::spawn(async move { let mut buf = vec![0; 1500]; + // A connected UDP socket surfaces ICMP port-unreachable as an error on + // send/recv (WSAECONNRESET 10054 on Windows, ECONNREFUSED on Linux). For UDP + // these are advisory: a stray ICMP from a NAT rebind glitch or a momentary + // peer hiccup does not mean the path is dead, and KCP retransmits through it. + // Treat socket errors as packet loss instead of tearing the session down; + // a truly dead link is reaped by the KCP pong timeout / app-level timeouts. + // The short sleep prevents a persistently failing socket from busy-spinning. loop { tokio::select! { _ = &mut stop_receiver => { @@ -112,8 +137,8 @@ impl KcpStream { } Some(data) = output.recv() => { if let Err(e) = udp.send(&data.inner()).await { - log::debug!("KCP send error: {:?}", e); - break; + log::debug!("KCP send error (treated as loss): {:?}", e); + tokio::time::sleep(std::time::Duration::from_millis(10)).await; } } result = udp.recv_from(&mut buf) => { @@ -127,8 +152,8 @@ impl KcpStream { .await.ok(); } Err(e) => { - log::debug!("KCP recv_from error: {:?}", e); - break; + log::debug!("KCP recv_from error (treated as loss): {:?}", e); + tokio::time::sleep(std::time::Duration::from_millis(10)).await; } } }