mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-05 15:41:23 +03:00
port_forward_mux: window accounting and channel frame builders
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
This commit is contained in:
@@ -45,6 +45,7 @@ mod custom_server;
|
||||
mod lang;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
mod port_forward;
|
||||
mod port_forward_mux;
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
mod tray;
|
||||
|
||||
301
src/port_forward_mux.rs
Normal file
301
src/port_forward_mux.rs
Normal file
@@ -0,0 +1,301 @@
|
||||
use hbb_common::{
|
||||
bytes::Bytes,
|
||||
message_proto::*,
|
||||
tokio::{self, sync::Notify},
|
||||
};
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// On the wire and fixed forever: what the controller may have in flight on a
|
||||
/// channel before `opened` brings the peer's window.
|
||||
pub const INITIAL_WINDOW: u32 = 64 * 1024;
|
||||
/// Also on the wire and fixed forever: the window a `data` frame costs at
|
||||
/// minimum, whatever its length. It bounds the per-frame bookkeeping a peer
|
||||
/// can make us hold — 1-byte frames would otherwise cost it one byte and us
|
||||
/// a queue entry.
|
||||
pub const MIN_FRAME_CHARGE: u32 = 64;
|
||||
pub const CHANNEL_WINDOW: u32 = 256 * 1024;
|
||||
pub const MAX_FRAME: usize = 64 * 1024;
|
||||
pub const UPDATE_THRESHOLD: u32 = CHANNEL_WINDOW / 2;
|
||||
pub const MAX_CHANNELS: usize = 256;
|
||||
pub const DATA_QUEUE_FRAMES: usize = 128;
|
||||
|
||||
pub fn effective_window(advertised: u32) -> u32 {
|
||||
advertised.max(INITIAL_WINDOW)
|
||||
}
|
||||
|
||||
/// What a `data` frame of this length costs its channel's window.
|
||||
pub fn charge(len: usize) -> u32 {
|
||||
u32::try_from(len).unwrap_or(u32::MAX).max(MIN_FRAME_CHARGE)
|
||||
}
|
||||
|
||||
/// Receiver-side accounting: what we granted, what arrived, what we have
|
||||
/// drained to the local socket since the last `window_update`.
|
||||
pub struct RecvWindow {
|
||||
granted: u32,
|
||||
received: u32,
|
||||
drained_since_update: u32,
|
||||
}
|
||||
|
||||
impl RecvWindow {
|
||||
pub fn new(granted: u32) -> Self {
|
||||
Self {
|
||||
granted: effective_window(granted),
|
||||
received: 0,
|
||||
drained_since_update: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// False means the peer overran the window: a protocol violation.
|
||||
pub fn accept(&mut self, len: usize) -> bool {
|
||||
let len = charge(len);
|
||||
match self.received.checked_add(len) {
|
||||
Some(total) if total <= self.granted => {
|
||||
self.received = total;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the amount to advertise in a `window_update` once enough has
|
||||
/// been drained; the grant is extended by the same amount.
|
||||
pub fn drained(&mut self, n: usize) -> Option<u32> {
|
||||
let n = charge(n);
|
||||
self.drained_since_update = self.drained_since_update.saturating_add(n);
|
||||
if self.drained_since_update < UPDATE_THRESHOLD {
|
||||
return None;
|
||||
}
|
||||
let add = self.drained_since_update;
|
||||
self.drained_since_update = 0;
|
||||
self.granted = self.granted.saturating_add(add);
|
||||
Some(add)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sender-side credit. `take` parks until credit is available; the lock is
|
||||
/// never held across an await.
|
||||
pub struct SendCredit {
|
||||
credit: Mutex<u32>,
|
||||
notify: Notify,
|
||||
}
|
||||
|
||||
impl SendCredit {
|
||||
pub fn new(initial: u32) -> Self {
|
||||
Self {
|
||||
credit: Mutex::new(initial),
|
||||
notify: Notify::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `max` must be at least `MIN_FRAME_CHARGE` (`MAX_FRAME` is), and the
|
||||
/// caller pays `charge(bytes_read)` and refunds the rest — so this parks
|
||||
/// until a whole minimum charge is available rather than at zero.
|
||||
pub async fn take(&self, max: usize) -> usize {
|
||||
debug_assert!(max >= MIN_FRAME_CHARGE as usize);
|
||||
loop {
|
||||
{
|
||||
let mut credit = self.credit.lock().unwrap();
|
||||
if *credit >= MIN_FRAME_CHARGE {
|
||||
let n = (*credit as usize).min(max);
|
||||
*credit -= n as u32;
|
||||
return n;
|
||||
}
|
||||
}
|
||||
self.notify.notified().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&self, n: u32) {
|
||||
{
|
||||
let mut credit = self.credit.lock().unwrap();
|
||||
*credit = credit.saturating_add(n);
|
||||
}
|
||||
self.notify.notify_waiters();
|
||||
self.notify.notify_one();
|
||||
}
|
||||
|
||||
/// The controller starts a channel with `INITIAL_WINDOW` of credit; when
|
||||
/// `opened` advertises the peer's real window this re-bases to it.
|
||||
pub fn raise_initial(&self, total: u32) {
|
||||
let extra = effective_window(total) - INITIAL_WINDOW;
|
||||
if extra > 0 {
|
||||
self.add(extra);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_msg(union: port_forward_channel::Union) -> Message {
|
||||
let mut ch = PortForwardChannel::new();
|
||||
ch.union = Some(union);
|
||||
let mut msg = Message::new();
|
||||
msg.set_port_forward_channel(ch);
|
||||
msg
|
||||
}
|
||||
|
||||
pub fn open_msg(id: i32, host: &str, port: i32, window: u32) -> Message {
|
||||
channel_msg(port_forward_channel::Union::Open(PortForwardOpen {
|
||||
channel_id: id,
|
||||
host: host.to_owned(),
|
||||
port,
|
||||
window,
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn opened_msg(id: i32, success: bool, message: &str, window: u32) -> Message {
|
||||
channel_msg(port_forward_channel::Union::Opened(PortForwardOpened {
|
||||
channel_id: id,
|
||||
success,
|
||||
message: message.to_owned(),
|
||||
window,
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn data_msg(id: i32, data: Bytes) -> Message {
|
||||
channel_msg(port_forward_channel::Union::Data(PortForwardData {
|
||||
channel_id: id,
|
||||
data,
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn close_msg(id: i32) -> Message {
|
||||
channel_msg(port_forward_channel::Union::Close(PortForwardClose {
|
||||
channel_id: id,
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn window_update_msg(id: i32, add: u32) -> Message {
|
||||
channel_msg(port_forward_channel::Union::WindowUpdate(PortForwardWindowUpdate {
|
||||
channel_id: id,
|
||||
add,
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn rt() -> tokio::runtime::Runtime {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_window_clamps_to_initial() {
|
||||
assert_eq!(effective_window(0), INITIAL_WINDOW);
|
||||
assert_eq!(effective_window(INITIAL_WINDOW - 1), INITIAL_WINDOW);
|
||||
assert_eq!(effective_window(INITIAL_WINDOW), INITIAL_WINDOW);
|
||||
assert_eq!(effective_window(CHANNEL_WINDOW), CHANNEL_WINDOW);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recv_window_rejects_over_window_data() {
|
||||
// `new` clamps its grant to INITIAL_WINDOW, so the test must fill that.
|
||||
let mut w = RecvWindow::new(INITIAL_WINDOW);
|
||||
assert!(w.accept(INITIAL_WINDOW as usize - MIN_FRAME_CHARGE as usize));
|
||||
assert!(w.accept(MIN_FRAME_CHARGE as usize));
|
||||
assert!(!w.accept(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiny_frames_are_charged_at_the_minimum() {
|
||||
let mut w = RecvWindow::new(INITIAL_WINDOW);
|
||||
for _ in 0..(INITIAL_WINDOW / MIN_FRAME_CHARGE) {
|
||||
assert!(w.accept(1));
|
||||
}
|
||||
// A 64 KiB window holds 1024 one-byte frames, not 65536 of them.
|
||||
assert!(!w.accept(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recv_window_updates_only_past_threshold() {
|
||||
let mut w = RecvWindow::new(CHANNEL_WINDOW);
|
||||
assert_eq!(
|
||||
w.drained(UPDATE_THRESHOLD as usize - MIN_FRAME_CHARGE as usize),
|
||||
None
|
||||
);
|
||||
assert_eq!(w.drained(MIN_FRAME_CHARGE as usize), Some(UPDATE_THRESHOLD));
|
||||
// The update re-grants what was drained, so the same amount is accepted again.
|
||||
assert!(w.accept(CHANNEL_WINDOW as usize));
|
||||
assert!(w.accept(UPDATE_THRESHOLD as usize));
|
||||
assert!(!w.accept(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_credit_blocks_at_zero_and_resumes_on_add() {
|
||||
rt().block_on(async {
|
||||
let credit = std::sync::Arc::new(SendCredit::new(MIN_FRAME_CHARGE + 4));
|
||||
assert_eq!(
|
||||
credit.take(MAX_FRAME).await,
|
||||
(MIN_FRAME_CHARGE + 4) as usize
|
||||
);
|
||||
let c = credit.clone();
|
||||
let waiter = tokio::spawn(async move { c.take(MAX_FRAME).await });
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
assert!(!waiter.is_finished());
|
||||
// Below one minimum charge the taker stays parked: whatever it
|
||||
// reads next has to be payable.
|
||||
credit.add(MIN_FRAME_CHARGE - 1);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
assert!(!waiter.is_finished());
|
||||
credit.add(1);
|
||||
assert_eq!(waiter.await.unwrap(), MIN_FRAME_CHARGE as usize);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raise_initial_rebases_credit_from_initial_window() {
|
||||
rt().block_on(async {
|
||||
let credit = SendCredit::new(INITIAL_WINDOW);
|
||||
assert_eq!(credit.take(1000).await, 1000);
|
||||
credit.raise_initial(CHANNEL_WINDOW);
|
||||
// Credit is now CHANNEL_WINDOW - 1000, not CHANNEL_WINDOW - 1000 + INITIAL_WINDOW.
|
||||
assert_eq!(
|
||||
credit.take(usize::MAX).await,
|
||||
(CHANNEL_WINDOW - 1000) as usize
|
||||
);
|
||||
credit.raise_initial(0);
|
||||
// A zero or sub-INITIAL_WINDOW advertisement adds nothing.
|
||||
let c = std::sync::Arc::new(credit);
|
||||
let c2 = c.clone();
|
||||
let waiter = tokio::spawn(async move { c2.take(MAX_FRAME).await });
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
assert!(!waiter.is_finished());
|
||||
c.add(MIN_FRAME_CHARGE);
|
||||
assert_eq!(waiter.await.unwrap(), MIN_FRAME_CHARGE as usize);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_builders_set_the_expected_union_variant() {
|
||||
use hbb_common::message_proto::{message, port_forward_channel};
|
||||
let m = data_msg(7, Bytes::from_static(b"abc"));
|
||||
match m.union {
|
||||
Some(message::Union::PortForwardChannel(ch)) => match ch.union {
|
||||
Some(port_forward_channel::Union::Data(d)) => {
|
||||
assert_eq!(d.channel_id, 7);
|
||||
assert_eq!(d.data, b"abc".to_vec());
|
||||
}
|
||||
other => panic!("unexpected {:?}", other),
|
||||
},
|
||||
other => panic!("unexpected {:?}", other),
|
||||
}
|
||||
let m = opened_msg(3, false, "nope", CHANNEL_WINDOW);
|
||||
match m.union {
|
||||
Some(message::Union::PortForwardChannel(ch)) => match ch.union {
|
||||
Some(port_forward_channel::Union::Opened(o)) => {
|
||||
assert_eq!((o.channel_id, o.success, o.message.as_str(), o.window),
|
||||
(3, false, "nope", CHANNEL_WINDOW));
|
||||
}
|
||||
other => panic!("unexpected {:?}", other),
|
||||
},
|
||||
other => panic!("unexpected {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user