Compare commits

..

1 Commits

Author SHA1 Message Date
rustdesk
44abb4a99d webrtc: encrypt the signalling legs to the rendezvous server, or send no WebRTC signalling
Three TCP connections carry WebRTC signalling to hbbs in the clear: the
controller's punch connection, which carries the offer up and the answer and
both sides' ICE candidates through it, and on the controlled side the
short-lived connection that returns the answer and the one that trickles its
candidates. Candidates are every interface address of both machines, and the
controller is the side most often on a network it does not trust.

`secure_tcp` is fail-open by design: a server that answers the first message
with anything but a key exchange, or with nothing, leaves the stream in the
clear and the call returns Ok, which the paths from before such servers rely
on. That is not a channel WebRTC signalling may go out on.

So the three legs use `secure_tcp_required`: Ok only once the server's key
exchange has encrypted the stream, an error otherwise. WebSocket is treated
as `secure_tcp` treats it, as a transport encrypted already. On the controller an
error drops the offer, closes its peer connection through the guard and
reconnects, then punches without WebRTC on the fresh socket, with the legacy
condition applied to it as before; the failed exchange may have consumed a
message on the old one. On the controlled side an error abandons that WebRTC
attempt: the answer is not sent, or the candidates are not, and the
controller falls back to its other transports. Degrade to no WebRTC, never
to WebRTC signalling in the clear. `secure_tcp` itself is unchanged; the
exchange moves into `key_exchange`, which reports whether it happened.

A punch without an offer is unchanged: the legacy secure condition and the
UDP probe wait keep their shape, and an exchange still stands in for that
wait, being a server round trip of the same length.

Tests run a loopback stand-in for hbbs: a server that answers with another
message, or closes, is refused where `secure_tcp` would carry on in the
clear; a completed exchange is accepted and the stub decodes the reply with
its ephemeral key.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns
2026-09-15 17:41:55 +08:00
5 changed files with 178 additions and 337 deletions

View File

@@ -32,7 +32,7 @@ use crate::{
common::input::{MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP},
create_symmetric_key_msg, decode_id_pk, decode_id_pk_dtls, get_rs_pk, is_keyboard_mode_supported,
kcp_stream::KcpStream,
secure_tcp,
secure_tcp, secure_tcp_required,
ui_interface::{get_builtin_option, resolve_avatar_url, use_texture_render},
ui_session_interface::{InvokeUiSession, Session},
};
@@ -809,7 +809,7 @@ impl Client {
}
log::info!("rendezvous server: {}", rendezvous_server);
let mut socket = socket?;
let my_addr = socket.local_addr();
let mut my_addr = socket.local_addr();
let mut signed_id_pk = Vec::new();
let mut relay_server = "".to_owned();
let mut peer_addr = Config::get_any_listen_addr(true);
@@ -825,10 +825,41 @@ impl Client {
};
let switch_code = interface.get_switch_code();
if !key.is_empty() && (!token.is_empty() || !switch_code.is_empty()) {
let legacy_secure = !key.is_empty() && (!token.is_empty() || !switch_code.is_empty());
let carries_offer = webrtc_offerer
.as_ref()
.and_then(|g| g.stream())
.is_some();
let mut exchanged = false;
if carries_offer {
// An offer puts both sides' ICE candidates, every interface address of both
// machines, on this socket, so it goes out only once the server's key exchange has
// encrypted it. When the server does not complete one, an hbbs from before the
// exchange, the offer is dropped and this becomes a punch without WebRTC, on a fresh
// socket since the failed exchange may have consumed a message on this one. Degrade
// to no WebRTC, never to WebRTC signalling in the clear.
match secure_tcp_required(&mut socket, &key).await {
Ok(()) => exchanged = true,
Err(err) => {
log::warn!(
"WebRTC signalling to {} cannot be encrypted, punching without WebRTC: {}",
rendezvous_server,
err
);
webrtc_offerer = None;
socket = connect_tcp(&*rendezvous_server, CONNECT_TIMEOUT).await?;
my_addr = socket.local_addr();
}
}
}
if !exchanged && legacy_secure {
secure_tcp(&mut socket, &key)
.await
.map_err(|e| anyhow!("Failed to secure tcp: {}", e))?;
exchanged = true;
}
if exchanged {
// The exchange is a server round trip, the same time the wait below would have spent.
} else if let Some(udp) = udp.1.as_ref() {
let tm = Instant::now();
// rtt is the TCP connect time. When it is too short to be a real WAN round trip it

View File

@@ -2074,6 +2074,13 @@ async fn secure_tcp_impl(conn: &mut Stream, key: &str, log_on_success: bool) ->
if use_ws() {
return Ok(());
}
key_exchange(conn, key, log_on_success).await.map(|_| ())
}
/// The server's key exchange on `conn`. `Ok(true)` once the stream is encrypted. `Ok(false)`
/// when the server sent something else first, nothing parseable, or closed: `secure_tcp`
/// tolerates that for servers from before the exchange, `secure_tcp_required` does not.
async fn key_exchange(conn: &mut Stream, key: &str, log_on_success: bool) -> ResultType<bool> {
let rs_pk = get_rs_pk(key);
let Some(rs_pk) = rs_pk else {
bail!("Handshake failed: invalid public key from rendezvous server");
@@ -2102,6 +2109,7 @@ async fn secure_tcp_impl(conn: &mut Stream, key: &str, log_on_success: bool) ->
if log_on_success {
log::info!("Connection secured");
}
return Ok(true);
}
_ => {}
}
@@ -2109,7 +2117,7 @@ async fn secure_tcp_impl(conn: &mut Stream, key: &str, log_on_success: bool) ->
}
_ => {}
}
Ok(())
Ok(false)
}
pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
@@ -2120,6 +2128,22 @@ async fn secure_tcp_silent(conn: &mut Stream, key: &str) -> ResultType<()> {
secure_tcp_impl(conn, key, false).await
}
/// Like [`secure_tcp`], but returns only once the server's key exchange has actually encrypted
/// the stream; a server that answers with anything else, or with nothing, is an error, so the
/// caller can withhold what it was about to send instead of sending it in the clear.
/// `secure_tcp` keeps tolerating such a server, which the paths from before the exchange depend
/// on. WebSocket is treated as `secure_tcp` treats it, as a transport that is encrypted already.
pub async fn secure_tcp_required(conn: &mut Stream, key: &str) -> ResultType<()> {
if use_ws() {
return Ok(());
}
if key_exchange(conn, key, true).await? {
Ok(())
} else {
bail!("the rendezvous server did not complete the key exchange");
}
}
#[inline]
fn get_pk(pk: &[u8]) -> Option<[u8; 32]> {
if pk.len() == 32 {
@@ -3258,4 +3282,88 @@ mod tests {
assert_eq!(combined_mask & MOUSE_TYPE_MASK, MOUSE_TYPE_DOWN);
assert_eq!(combined_mask >> 3, MOUSE_BUTTON_LEFT | MOUSE_BUTTON_RIGHT);
}
/// A stand-in rendezvous server on loopback: accepts one connection and hands it to `serve`.
async fn rendezvous_stub<F, Fut>(serve: F) -> String
where
F: FnOnce(hbb_common::tcp::FramedStream) -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + Send + 'static,
{
let listener = hbb_common::tcp::new_listener("127.0.0.1:0", false)
.await
.unwrap();
let host = listener.local_addr().unwrap().to_string();
tokio::spawn(async move {
if let Ok((stream, addr)) = listener.accept().await {
serve(hbb_common::tcp::FramedStream::from(stream, addr)).await;
}
});
host
}
fn server_key() -> (String, sign::SecretKey) {
let (pk, sk) = sign::gen_keypair();
(encode64(pk.0), sk)
}
async fn connect(host: &str) -> Stream {
hbb_common::socket_client::connect_tcp(host.to_owned(), 3000)
.await
.unwrap()
}
#[tokio::test]
async fn test_secure_tcp_required_refuses_a_server_without_the_exchange() {
let (key, _) = server_key();
// A server from before the exchange answers the first message with something else.
let serve = |mut s: hbb_common::tcp::FramedStream| async move {
let mut msg = RendezvousMessage::new();
msg.set_register_peer_response(RegisterPeerResponse::new());
s.send(&msg).await.unwrap();
sleep(Duration::from_secs(2)).await;
};
let host = rendezvous_stub(serve).await;
let mut conn = connect(&host).await;
assert!(secure_tcp_required(&mut conn, &key).await.is_err());
assert!(!conn.is_secured());
// The legacy call tolerates the same server, and the stream stays in the clear.
let host = rendezvous_stub(serve).await;
let mut conn = connect(&host).await;
secure_tcp(&mut conn, &key).await.unwrap();
assert!(!conn.is_secured());
}
#[tokio::test]
async fn test_secure_tcp_required_refuses_a_closed_connection() {
let (key, _) = server_key();
let host = rendezvous_stub(|s| async move { drop(s) }).await;
let mut conn = connect(&host).await;
assert!(secure_tcp_required(&mut conn, &key).await.is_err());
assert!(!conn.is_secured());
}
#[tokio::test]
async fn test_secure_tcp_required_accepts_a_completed_exchange() {
let (key, sk) = server_key();
let host = rendezvous_stub(move |mut s| async move {
let (eph_pk, eph_sk) = box_::gen_keypair();
let mut msg = RendezvousMessage::new();
msg.set_key_exchange(KeyExchange {
keys: vec![sign::sign(&eph_pk.0, &sk).into()],
..Default::default()
});
s.send(&msg).await.unwrap();
// The client's reply must decode to a key with the ephemeral secret half.
let reply = s.next_timeout(3000).await.unwrap().unwrap();
let reply = RendezvousMessage::parse_from_bytes(&reply).unwrap();
let Some(rendezvous_message::Union::KeyExchange(ex)) = reply.union else {
panic!("expected the client's key exchange");
};
hbb_common::tcp::Encrypt::decode(&ex.keys[1], &ex.keys[0], &eph_sk).unwrap();
})
.await;
let mut conn = connect(&host).await;
secure_tcp_required(&mut conn, &key).await.unwrap();
assert!(conn.is_secured());
}
}

View File

@@ -806,6 +806,7 @@ impl RendezvousMediator {
// trickle, and TCP reliability replaces the old 400ms duplicate re-send
// (the controller keeps its own re-send for the server->peer UDP downlink).
let mut conn = None;
let key = crate::get_key(true).await;
while let Some(candidate) = local_ice_rx.recv().await {
let mut msg = Message::new();
msg.set_ice_candidate(IceCandidate {
@@ -819,7 +820,20 @@ impl RendezvousMediator {
for _ in 0..2 {
if conn.is_none() {
match connect_tcp(&*host, CONNECT_TIMEOUT).await {
Ok(s) => conn = Some(s),
Ok(mut s) => {
// Candidates are every interface address of this machine:
// sent only on a channel that is actually encrypted, else
// this WebRTC attempt goes without them.
if let Err(err) = crate::secure_tcp_required(&mut s, &key).await
{
log::warn!(
"failed to secure the WebRTC ICE candidate connection: {}",
err
);
break;
}
conn = Some(s);
}
Err(err) => {
log::warn!(
"failed to connect for WebRTC ICE candidate: {}",
@@ -993,6 +1007,9 @@ impl RendezvousMediator {
let mut msg_out = Message::new();
msg_out.set_punch_hole_sent(msg_punch);
let mut socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?;
// The answer goes out only on a channel that is actually encrypted; otherwise this
// WebRTC attempt is abandoned and the controller falls back to its other transports.
crate::secure_tcp_required(&mut socket, &crate::get_key(true).await).await?;
socket.send(&msg_out).await?;
return Ok(());
}

View File

@@ -118,15 +118,6 @@ pub struct Server {
pub type ServerPtr = Arc<RwLock<Server>>;
pub type ServerPtrWeak = Weak<RwLock<Server>>;
#[cfg(test)]
pub fn new_for_test() -> ServerPtr {
Arc::new(RwLock::new(Server {
connections: HashMap::new(),
services: HashMap::new(),
id_count: 1000,
}))
}
pub fn new() -> ServerPtr {
let mut server = Server {
connections: HashMap::new(),
@@ -213,40 +204,7 @@ pub async fn create_tcp_connection(
meta: ConnectionMeta,
) -> ResultType<()> {
let mut stream = stream;
// The address the connection layer keys on, whitelist and admission alike.
let addr = hbb_common::try_into_v4(addr);
let id = server.write().unwrap().get_new_id();
// Admitted before the identity handshake, so a peer that stalls in it, or after it without
// logging in, holds its place the whole time; an address over its share is turned away.
let Some(unauthorized) = admit_unauthorized(id, addr.ip()) else {
bail!("too many unauthenticated connections from {}", addr.ip());
};
tokio::select! {
handshake = identity_handshake(&mut stream, secure) => handshake?,
_ = unauthorized.evicted() => {
bail!("evicted to make room for a newer unauthenticated connection");
}
}
#[cfg(target_os = "macos")]
{
use std::process::Command;
if let Ok(task) = Command::new("/usr/bin/caffeinate")
.arg("-u")
.arg("-t 5")
.spawn()
{
super::CHILD_PROCESS.lock().unwrap().push(task);
}
log::info!("wake up macos");
}
Connection::start(addr, stream, id, Arc::downgrade(&server), meta, unauthorized).await;
Ok(())
}
/// Our signed identity goes out and, when `secure`, the controller's reply keys `stream`.
/// Separate so it can be raced against the connection's eviction.
async fn identity_handshake(stream: &mut Stream, secure: bool) -> ResultType<()> {
let (sk, pk) = Config::get_key_pair();
if secure && pk.len() == sign::PUBLICKEYBYTES && sk.len() == sign::SECRETKEYBYTES {
let mut sk_ = [0u8; sign::SECRETKEYBYTES];
@@ -309,6 +267,19 @@ async fn identity_handshake(stream: &mut Stream, secure: bool) -> ResultType<()>
}
}
#[cfg(target_os = "macos")]
{
use std::process::Command;
if let Ok(task) = Command::new("/usr/bin/caffeinate")
.arg("-u")
.arg("-t 5")
.spawn()
{
super::CHILD_PROCESS.lock().unwrap().push(task);
}
log::info!("wake up macos");
}
Connection::start(addr, stream, id, Arc::downgrade(&server), meta).await;
Ok(())
}

View File

@@ -55,16 +55,15 @@ use scrap::android::{call_main_service_key_event, call_main_service_pointer_inpu
use scrap::camera;
use serde_derive::Serialize;
use serde_json::{json, value::Value};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use std::sync::atomic::Ordering;
use std::{
collections::HashSet,
net::{IpAddr, Ipv6Addr},
net::Ipv6Addr,
num::NonZeroI64,
path::PathBuf,
str::FromStr,
sync::{
atomic::{AtomicBool, AtomicI64, Ordering},
mpsc as std_mpsc,
},
sync::{atomic::AtomicI64, mpsc as std_mpsc},
};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use system_shutdown;
@@ -80,111 +79,7 @@ const FAILURE_IDX_ID_WHITELIST: usize = 2;
// throttles enumeration harder; shorter limits collateral on whitelisted neighbours.
const ID_WHITELIST_FAILURE_DECAY_MINUTES: i32 = 10;
/// A connection that has not sent its login request within this long is closed. A controller
/// sends one as soon as the handshake is done, so nothing legitimate waits; a peer that holds
/// the connection open and says nothing is not a controller.
const LOGIN_REQUEST_GRACE: Duration = Duration::from_secs(20);
/// A connection that has sent a login request but not authorized within this long is closed,
/// however alive it keeps itself: a wrong password, a pending 2FA or an accept prompt still
/// unanswered. The controller reconnects on its own and the prompt comes back.
const LOGIN_GRACE: Duration = Duration::from_secs(180);
/// Connections between accept and authorization, across every transport. Beyond this many the
/// oldest is closed to make room, so a flood of them holds a legitimate controller off only
/// while newer ones keep arriving faster than a password is typed.
const MAX_UNAUTHORIZED_CONNS: usize = 64;
/// Of those, how many one address may hold at once; a further connection from that address is
/// refused. Filling the global limit takes this many times more addresses.
const MAX_UNAUTHORIZED_CONNS_PER_ADDR: usize = 4;
/// A place among the unauthorized connections, taken before the identity handshake and given
/// back on drop: at authorization, or when the connection ends first. The count of live
/// guards is the bound; an evicted one is told to go and keeps its place until it has.
pub struct UnauthorizedID {
id: i32,
shared: Arc<UnauthorizedShared>,
}
struct UnauthorizedShared {
evicted: AtomicBool,
notify: hbb_common::tokio::sync::Notify,
}
/// Admit a connection from `ip` among the unauthorized ones. `None` when that address already
/// holds its share, or when the global limit is reached: then the oldest connection is told to
/// go, unless one is on its way out already, and this one is refused rather than let in on a
/// place that is still occupied. At most one connection is ever on its way out, so a burst of
/// refused arrivals clears no more room than a single one. The controller retries on its own.
pub fn admit_unauthorized(id: i32, ip: IpAddr) -> Option<UnauthorizedID> {
let mut conns = UNAUTHORIZED_CONNS.lock().unwrap();
if conns.iter().filter(|(_, held, _)| *held == ip).count() >= MAX_UNAUTHORIZED_CONNS_PER_ADDR {
return None;
}
if conns.len() >= MAX_UNAUTHORIZED_CONNS {
if let Some((_, _, oldest)) = conns.first() {
if !oldest.evicted.swap(true, Ordering::AcqRel) {
oldest.notify.notify_one();
}
}
return None;
}
let shared = Arc::new(UnauthorizedShared {
evicted: AtomicBool::new(false),
notify: hbb_common::tokio::sync::Notify::new(),
});
conns.push((id, ip, shared.clone()));
Some(UnauthorizedID { id, shared })
}
impl UnauthorizedID {
/// Whether this connection was told to go to make room for a newer one.
pub fn is_evicted(&self) -> bool {
self.shared.evicted.load(Ordering::Acquire)
}
/// Resolves once this connection is told to go; at once if it already was.
pub async fn evicted(&self) {
if self.is_evicted() {
return;
}
self.shared.notify.notified().await;
}
}
impl Drop for UnauthorizedID {
fn drop(&mut self) {
UNAUTHORIZED_CONNS
.lock()
.unwrap()
.retain(|(id, _, _)| *id != self.id);
}
}
/// Resolves when the connection holding `unauthorized` is evicted; never once it has
/// authorized and given its place back.
async fn unauthorized_evicted(unauthorized: &Option<UnauthorizedID>) {
match unauthorized {
Some(u) => u.evicted().await,
None => std::future::pending().await,
}
}
/// Resolves at the login deadline of a connection started at `started`; never once it has
/// authorized. Until the login request arrives the shorter grace applies.
async fn login_deadline(authorized: bool, login_request_seen: bool, started: Instant) {
if authorized {
return std::future::pending().await;
}
let grace = if login_request_seen {
LOGIN_GRACE
} else {
LOGIN_REQUEST_GRACE
};
time::sleep_until(started + grace).await
}
lazy_static::lazy_static! {
// Connections between accept and authorization, oldest first; see admit_unauthorized.
static ref UNAUTHORIZED_CONNS: Mutex<Vec<(i32, IpAddr, Arc<UnauthorizedShared>)>> = Default::default();
// [0] password, [1] 2FA, [2] ID whitelist.
// Bucket 2 is separate so its rejections do not touch the password / 2FA budgets. It is
// decayed in `check_id_whitelist` and cleared on auth, never on a bare id match.
@@ -369,9 +264,6 @@ pub struct Connection {
port_forward_address: String,
tx_to_cm: mpsc::UnboundedSender<ipc::Data>,
authorized: bool,
// The place among the unauthorized connections; given back at authorization.
unauthorized_id: Option<UnauthorizedID>,
login_request_seen: bool,
require_2fa: Option<totp_rs::TOTP>,
awaiting_2fa: bool,
keyboard: bool,
@@ -533,7 +425,6 @@ impl Connection {
id: i32,
server: super::ServerPtrWeak,
meta: super::ConnectionMeta,
unauthorized: UnauthorizedID,
) {
let super::ConnectionMeta {
control_permissions,
@@ -592,8 +483,6 @@ impl Connection {
port_forward_address: "".to_owned(),
tx_to_cm,
authorized: false,
unauthorized_id: Some(unauthorized),
login_request_seen: false,
keyboard: Self::permission(keys::OPTION_ENABLE_KEYBOARD, &control_permissions),
clipboard: Self::permission(keys::OPTION_ENABLE_CLIPBOARD, &control_permissions),
audio: Self::permission(keys::OPTION_ENABLE_AUDIO, &control_permissions),
@@ -709,7 +598,6 @@ impl Connection {
let mut test_delay_timer =
crate::rustdesk_interval(time::interval_at(Instant::now(), TEST_DELAY_TIMEOUT));
let mut last_recv_time = Instant::now();
let started = Instant::now();
// The connection type is not known until the login request arrives;
// `on_message` picks the type-specific timeout then.
@@ -745,17 +633,6 @@ impl Connection {
tokio::select! {
// biased; // video has higher priority // causing test_delay_timer failed while transferring big file
// Both end an unauthorized connection at once, not on the next timer tick:
// told to go to make room, or past the grace for its login request or its
// authorization. Neither fires once the connection has authorized.
_ = unauthorized_evicted(&conn.unauthorized_id) => {
conn.on_close("Timeout", true).await;
break;
}
_ = login_deadline(conn.authorized, conn.login_request_seen, started) => {
conn.on_close("Timeout", true).await;
break;
}
Some(data) = rx_from_cm.recv() => {
match data {
ipc::Data::Authorize => {
@@ -1915,7 +1792,6 @@ impl Connection {
return false;
}
self.authorized = true;
self.unauthorized_id = None;
// Releases the budget `check_id_whitelist` charges against this address: only a peer
// that got this far proved more than a self-reported id.
self.clear_id_whitelist_failures();
@@ -2882,7 +2758,6 @@ impl Connection {
}
// After handling CloseReason messages, proceed to process other message types
if let Some(message::Union::LoginRequest(lr)) = msg.union {
self.login_request_seen = true;
if !self.check_login_scope(&lr).await {
return false;
}
@@ -7119,167 +6994,6 @@ mod test {
#[allow(unused)]
use super::*;
// The registry is process-global and the harness runs tests in parallel threads, so every
// test that admits connections holds this first; a poisoned lock is still a lock.
static UNAUTHORIZED_TESTS: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn unauthorized_count() -> usize {
UNAUTHORIZED_CONNS.lock().unwrap().len()
}
#[test]
fn test_unauthorized_admission_is_per_address() {
let _serial = UNAUTHORIZED_TESTS.lock().unwrap_or_else(|e| e.into_inner());
let a: IpAddr = "203.0.113.1".parse().unwrap();
let b: IpAddr = "203.0.113.2".parse().unwrap();
let held: Vec<_> = (0..MAX_UNAUTHORIZED_CONNS_PER_ADDR as i32)
.map(|i| admit_unauthorized(1_000_000 + i, a).unwrap())
.collect();
assert!(admit_unauthorized(1_000_100, a).is_none());
assert!(
held.iter().all(|u| !u.is_evicted()),
"a refusal evicts nobody"
);
let other = admit_unauthorized(1_000_101, b).unwrap();
assert!(!other.is_evicted());
drop(held);
assert!(admit_unauthorized(1_000_102, a).is_some());
}
#[tokio::test]
async fn test_unauthorized_full_tells_the_oldest_to_go_and_frees_its_place_only_when_it_has() {
let _serial = UNAUTHORIZED_TESTS.lock().unwrap_or_else(|e| e.into_inner());
let mut held: Vec<_> = (0..MAX_UNAUTHORIZED_CONNS as i32)
.map(|i| {
let ip: IpAddr = format!("198.51.100.{}", i + 1).parse().unwrap();
admit_unauthorized(2_000_000 + i, ip).unwrap()
})
.collect();
// At the limit the newcomer is refused, the oldest is told to go, and the count does
// not move: the place is still occupied.
assert!(admit_unauthorized(2_000_999, "198.51.100.250".parse().unwrap()).is_none());
assert!(held[0].is_evicted());
assert!(held[1..].iter().all(|u| !u.is_evicted()));
assert_eq!(unauthorized_count(), MAX_UNAUTHORIZED_CONNS);
hbb_common::timeout(1000, held[0].evicted()).await.unwrap();
// A further arrival while that one is still on its way out tells nobody else to go: a
// burst of refused arrivals clears no more room than a single one.
assert!(admit_unauthorized(2_001_000, "198.51.100.251".parse().unwrap()).is_none());
assert!(held[1..].iter().all(|u| !u.is_evicted()));
// Only once an evicted connection has gone is there a place for a newcomer.
drop(held.remove(0));
assert_eq!(unauthorized_count(), MAX_UNAUTHORIZED_CONNS - 1);
let newcomer = admit_unauthorized(2_001_001, "198.51.100.252".parse().unwrap()).unwrap();
assert!(!newcomer.is_evicted());
assert_eq!(unauthorized_count(), MAX_UNAUTHORIZED_CONNS);
// Full again, the next arrival tells the connection now oldest to go.
assert!(admit_unauthorized(2_001_002, "198.51.100.253".parse().unwrap()).is_none());
assert!(held[0].is_evicted());
assert!(held[1..].iter().all(|u| !u.is_evicted()));
assert!(!newcomer.is_evicted());
}
// The per-address share holds at the limit too: an address can turn out at most that many
// connections, one per place it then takes, and is refused before any eviction from then on.
#[test]
fn test_unauthorized_full_one_address_turns_out_at_most_its_share() {
let _serial = UNAUTHORIZED_TESTS.lock().unwrap_or_else(|e| e.into_inner());
let mut held: Vec<_> = (0..MAX_UNAUTHORIZED_CONNS as i32)
.map(|i| {
let ip: IpAddr = format!("198.51.100.{}", i + 1).parse().unwrap();
admit_unauthorized(3_000_000 + i, ip).unwrap()
})
.collect();
let flooder: IpAddr = "203.0.113.9".parse().unwrap();
let mut taken = Vec::new();
for i in 0..MAX_UNAUTHORIZED_CONNS_PER_ADDR as i32 {
assert!(admit_unauthorized(3_001_000 + i, flooder).is_none());
assert!(held[0].is_evicted());
drop(held.remove(0));
taken.push(admit_unauthorized(3_002_000 + i, flooder).unwrap());
}
assert_eq!(unauthorized_count(), MAX_UNAUTHORIZED_CONNS);
assert!(admit_unauthorized(3_003_000, flooder).is_none());
assert!(held.iter().all(|u| !u.is_evicted()));
assert!(taken.iter().all(|u| !u.is_evicted()));
}
/// A loopback TCP connection as create_tcp_connection sees it, and the controller's end,
/// which never speaks: the connection stalls in the identity handshake.
async fn stalled_incoming() -> (Stream, Stream, SocketAddr) {
let listener = hbb_common::tcp::new_listener("127.0.0.1:0", false)
.await
.unwrap();
let host = listener.local_addr().unwrap().to_string();
let controller = hbb_common::socket_client::connect_tcp(host, 3000)
.await
.unwrap();
let (accepted, addr) = listener.accept().await.unwrap();
let served = Stream::Tcp(hbb_common::tcp::FramedStream::from(accepted, addr));
(served, controller, addr)
}
// Live connections, not bookkeeping: with the limit reached by connections stalled in the
// handshake, one more arrival is refused and the oldest handshake is ended at once, not on
// a timer tick, so the live count never exceeds the limit and a place opens only then.
#[tokio::test]
async fn test_unauthorized_limit_bounds_live_handshakes() {
let _serial = UNAUTHORIZED_TESTS.lock().unwrap_or_else(|e| e.into_inner());
let server = crate::server::new_for_test();
let mut controllers = Vec::new();
let mut handshakes = Vec::new();
for i in 0..MAX_UNAUTHORIZED_CONNS {
let (served, controller, _) = stalled_incoming().await;
controllers.push(controller);
// Each from an address of its own, so only the global limit is in play.
let addr: SocketAddr = format!("192.0.2.{}:1", i + 1).parse().unwrap();
let server = server.clone();
handshakes.push(tokio::spawn(async move {
crate::server::create_tcp_connection(server, served, addr, true, Default::default())
.await
}));
}
for _ in 0..200 {
if unauthorized_count() == MAX_UNAUTHORIZED_CONNS {
break;
}
hbb_common::sleep(0.02).await;
}
assert_eq!(unauthorized_count(), MAX_UNAUTHORIZED_CONNS);
assert!(handshakes.iter().all(|h| !h.is_finished()));
let (served, _controller, _) = stalled_incoming().await;
let addr: SocketAddr = "192.0.2.200:1".parse().unwrap();
let refused = crate::server::create_tcp_connection(
server.clone(),
served,
addr,
true,
Default::default(),
)
.await;
assert!(refused.is_err());
let ended = hbb_common::timeout(2000, handshakes.remove(0)).await;
assert!(
matches!(ended, Ok(Ok(Err(_)))),
"the oldest handshake ends on eviction"
);
assert_eq!(unauthorized_count(), MAX_UNAUTHORIZED_CONNS - 1);
assert!(
handshakes.iter().all(|h| !h.is_finished()),
"only the oldest was ended"
);
drop(controllers);
for h in handshakes {
assert!(
matches!(hbb_common::timeout(3000, h).await, Ok(Ok(Err(_)))),
"a stalled handshake ends when its controller goes"
);
}
assert_eq!(unauthorized_count(), 0, "no handshake outlives the test");
}
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[test]