mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-09 22:11:02 +03:00
server: PortForwardMux channel table and per-channel tasks
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
This commit is contained in:
@@ -70,6 +70,7 @@ pub mod input_service {
|
|||||||
|
|
||||||
mod connection;
|
mod connection;
|
||||||
mod login_failure_check;
|
mod login_failure_check;
|
||||||
|
pub(crate) mod port_forward_mux;
|
||||||
pub mod display_service;
|
pub mod display_service;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub mod portable_service;
|
pub mod portable_service;
|
||||||
|
|||||||
@@ -1645,7 +1645,7 @@ impl Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_port_forward_target(pf: &mut PortForward) -> (String, bool) {
|
pub(super) fn normalize_port_forward_target(pf: &mut PortForward) -> (String, bool) {
|
||||||
let mut is_rdp = false;
|
let mut is_rdp = false;
|
||||||
if pf.host == "RDP" && pf.port == 0 {
|
if pf.host == "RDP" && pf.port == 0 {
|
||||||
pf.host = "localhost".to_owned();
|
pf.host = "localhost".to_owned();
|
||||||
|
|||||||
496
src/server/port_forward_mux.rs
Normal file
496
src/server/port_forward_mux.rs
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
use super::connection::{Connection, Sender};
|
||||||
|
use crate::port_forward_mux::{
|
||||||
|
charge, close_msg, effective_window, opened_msg, run_channel, FrameSink, Inbound, RecvWindow,
|
||||||
|
SendCredit, CHANNEL_WINDOW, INITIAL_WINDOW, MAX_CHANNELS,
|
||||||
|
};
|
||||||
|
use hbb_common::{
|
||||||
|
bytes::Bytes,
|
||||||
|
log,
|
||||||
|
message_proto::*,
|
||||||
|
timeout,
|
||||||
|
tokio::{self, net::TcpStream, sync::mpsc},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
collections::{BTreeSet, HashMap},
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
const CONNECT_TIMEOUT_MS: u64 = 3000;
|
||||||
|
|
||||||
|
/// Before `opened` the controller may only have used `INITIAL_WINDOW`.
|
||||||
|
/// `charged` is the running total of `charge(len)`, not of raw lengths.
|
||||||
|
fn pending_fits(charged: usize, add_len: usize) -> bool {
|
||||||
|
charged.saturating_add(charge(add_len) as usize) <= INITIAL_WINDOW as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
inbound: mpsc::UnboundedSender<Inbound>,
|
||||||
|
credit: Arc<SendCredit>,
|
||||||
|
window: Arc<Mutex<RecvWindow>>,
|
||||||
|
target: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The controlled side of one multiplexed tunnel. The main loop owns it and
|
||||||
|
/// forwards every `PortForwardChannel` frame here; each channel is a task.
|
||||||
|
pub struct PortForwardMux {
|
||||||
|
channels: HashMap<i32, Entry>,
|
||||||
|
tx: Sender,
|
||||||
|
login_target: String,
|
||||||
|
last_label: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PortForwardMux {
|
||||||
|
pub fn new(tx: Sender, login_target: String) -> Self {
|
||||||
|
let last_label = login_target.clone();
|
||||||
|
Self {
|
||||||
|
channels: HashMap::new(),
|
||||||
|
tx,
|
||||||
|
login_target,
|
||||||
|
last_label,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle(&mut self, frame: PortForwardChannel, permitted: bool) {
|
||||||
|
match frame.union {
|
||||||
|
Some(port_forward_channel::Union::Open(open)) => self.on_open(open, permitted),
|
||||||
|
Some(port_forward_channel::Union::Data(d)) => {
|
||||||
|
let len = d.data.len();
|
||||||
|
let Some(entry) = self.channels.get(&d.channel_id) else {
|
||||||
|
log::debug!("port forward data for unknown channel {}", d.channel_id);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let accepted = entry.window.lock().unwrap().accept(len);
|
||||||
|
let msg = if accepted {
|
||||||
|
Inbound::Data(d.data)
|
||||||
|
} else {
|
||||||
|
log::warn!("port forward channel {} overran its window", d.channel_id);
|
||||||
|
Inbound::Violation
|
||||||
|
};
|
||||||
|
if entry.inbound.send(msg).is_err() {
|
||||||
|
self.channels.remove(&d.channel_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(port_forward_channel::Union::Close(c)) => {
|
||||||
|
if let Some(entry) = self.channels.remove(&c.channel_id) {
|
||||||
|
entry.inbound.send(Inbound::Close).ok();
|
||||||
|
} else {
|
||||||
|
log::debug!("port forward close for unknown channel {}", c.channel_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(port_forward_channel::Union::WindowUpdate(u)) => {
|
||||||
|
match self.channels.get(&u.channel_id) {
|
||||||
|
Some(entry) => entry.credit.add(u.add),
|
||||||
|
None => log::debug!(
|
||||||
|
"port forward window update for unknown channel {}",
|
||||||
|
u.channel_id
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(port_forward_channel::Union::Opened(o)) => {
|
||||||
|
log::debug!("ignoring opened for channel {} on the controlled side", o.channel_id);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_open(&mut self, open: PortForwardOpen, permitted: bool) {
|
||||||
|
let id = open.channel_id;
|
||||||
|
self.channels.retain(|_, e| !e.inbound.is_closed());
|
||||||
|
if !permitted {
|
||||||
|
self.reply(opened_msg(id, false, "No permission of IP tunneling", 0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.channels.len() >= MAX_CHANNELS {
|
||||||
|
self.reply(opened_msg(id, false, "Too many port forward channels", 0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.channels.contains_key(&id) {
|
||||||
|
log::debug!("ignoring open for live channel {}", id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut pf = PortForward {
|
||||||
|
host: open.host,
|
||||||
|
port: open.port,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let (addr, _is_rdp) = Connection::normalize_port_forward_target(&mut pf);
|
||||||
|
let (inbound_tx, inbound_rx) = mpsc::unbounded_channel();
|
||||||
|
let credit = Arc::new(SendCredit::new(effective_window(open.window)));
|
||||||
|
let window = Arc::new(Mutex::new(RecvWindow::new(CHANNEL_WINDOW)));
|
||||||
|
self.channels.insert(
|
||||||
|
id,
|
||||||
|
Entry {
|
||||||
|
inbound: inbound_tx,
|
||||||
|
credit: credit.clone(),
|
||||||
|
window: window.clone(),
|
||||||
|
target: addr.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
tokio::spawn(run_controlled_channel(
|
||||||
|
id,
|
||||||
|
addr,
|
||||||
|
credit,
|
||||||
|
window,
|
||||||
|
inbound_rx,
|
||||||
|
FrameSink::Direct(self.tx.clone()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reply(&self, msg: Message) {
|
||||||
|
self.tx
|
||||||
|
.send((tokio::time::Instant::now(), Arc::new(msg)))
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drops dead entries and recomputes the CM label from the survivors.
|
||||||
|
/// Returns the label only when it differs from the last one returned.
|
||||||
|
pub fn sweep(&mut self) -> Option<String> {
|
||||||
|
self.channels.retain(|_, e| !e.inbound.is_closed());
|
||||||
|
let targets: BTreeSet<&str> = self.channels.values().map(|e| e.target.as_str()).collect();
|
||||||
|
let label = if targets.is_empty() {
|
||||||
|
self.login_target.clone()
|
||||||
|
} else {
|
||||||
|
let first = if targets.contains(self.login_target.as_str()) {
|
||||||
|
self.login_target.as_str()
|
||||||
|
} else {
|
||||||
|
targets.iter().next().copied().unwrap_or(self.login_target.as_str())
|
||||||
|
};
|
||||||
|
match targets.len() - 1 {
|
||||||
|
0 => first.to_owned(),
|
||||||
|
n => format!("{} +{}", first, n),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if label == self.last_label {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.last_label = label.clone();
|
||||||
|
Some(label)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn live_channels(&self) -> usize {
|
||||||
|
self.channels.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dropping every sender ends every task; each drops its target socket.
|
||||||
|
pub fn close_all(&mut self) {
|
||||||
|
self.channels.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Owns the whole channel lifecycle: connect under a `select!` in which a
|
||||||
|
/// queued command always wins over the connect, buffer what arrives
|
||||||
|
/// meanwhile, then relay.
|
||||||
|
async fn run_controlled_channel(
|
||||||
|
id: i32,
|
||||||
|
addr: String,
|
||||||
|
credit: Arc<SendCredit>,
|
||||||
|
window: Arc<Mutex<RecvWindow>>,
|
||||||
|
mut inbound: mpsc::UnboundedReceiver<Inbound>,
|
||||||
|
sink: FrameSink,
|
||||||
|
) {
|
||||||
|
let mut pending: Vec<Bytes> = Vec::new();
|
||||||
|
let mut pending_len = 0usize;
|
||||||
|
let connect = timeout(CONNECT_TIMEOUT_MS, TcpStream::connect(&addr));
|
||||||
|
tokio::pin!(connect);
|
||||||
|
let socket = loop {
|
||||||
|
tokio::select! {
|
||||||
|
// Biased with the command arm first: a `close` that is already
|
||||||
|
// queued must win over a connect that completed on the same poll,
|
||||||
|
// or `opened` would go out for a channel the controller has dropped.
|
||||||
|
biased;
|
||||||
|
cmd = inbound.recv() => match cmd {
|
||||||
|
Some(Inbound::Data(b)) => {
|
||||||
|
if !pending_fits(pending_len, b.len()) {
|
||||||
|
log::warn!("port forward channel {} sent more than INITIAL_WINDOW before opened", id);
|
||||||
|
sink.send_ordered(close_msg(id)).await.ok();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pending_len += charge(b.len()) as usize;
|
||||||
|
pending.push(b);
|
||||||
|
}
|
||||||
|
Some(Inbound::Close) | None => return,
|
||||||
|
Some(Inbound::Violation) => {
|
||||||
|
sink.send_ordered(close_msg(id)).await.ok();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
res = &mut connect => match res {
|
||||||
|
Ok(Ok(s)) => break s,
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
log::debug!("port forward channel {} connect {} failed: {}", id, addr, e);
|
||||||
|
sink.send_ordered(opened_msg(id, false, &format!("Failed to access remote {}", addr), 0)).await.ok();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
log::debug!("port forward channel {} connect {} timed out", id, addr);
|
||||||
|
sink.send_ordered(opened_msg(id, false, &format!("Failed to access remote {}", addr), 0)).await.ok();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if sink
|
||||||
|
.send_ordered(opened_msg(id, true, "", CHANNEL_WINDOW))
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (reader, writer) = socket.into_split();
|
||||||
|
run_channel(id, reader, writer, Vec::new(), pending, credit, window, inbound, sink).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::port_forward_mux::{CHANNEL_WINDOW, INITIAL_WINDOW, MAX_CHANNELS, MIN_FRAME_CHARGE};
|
||||||
|
use hbb_common::{
|
||||||
|
message_proto::{message, port_forward_channel},
|
||||||
|
tokio::{
|
||||||
|
self,
|
||||||
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
|
net::TcpListener,
|
||||||
|
sync::mpsc,
|
||||||
|
time::Instant,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn rt() -> tokio::runtime::Runtime {
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An echo server standing in for the forward target.
|
||||||
|
async fn echo_target() -> u16 {
|
||||||
|
let l = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let port = l.local_addr().unwrap().port();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let (mut s, _) = l.accept().await.unwrap();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut buf = [0u8; 4096];
|
||||||
|
loop {
|
||||||
|
let n = s.read(&mut buf).await.unwrap_or(0);
|
||||||
|
if n == 0 || s.write_all(&buf[..n]).await.is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
port
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open(id: i32, port: u16) -> PortForwardChannel {
|
||||||
|
let mut ch = PortForwardChannel::new();
|
||||||
|
ch.set_open(PortForwardOpen {
|
||||||
|
channel_id: id,
|
||||||
|
host: "127.0.0.1".to_owned(),
|
||||||
|
port: port as i32,
|
||||||
|
window: CHANNEL_WINDOW,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
ch
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data(id: i32, bytes: &[u8]) -> PortForwardChannel {
|
||||||
|
let mut ch = PortForwardChannel::new();
|
||||||
|
ch.set_data(PortForwardData {
|
||||||
|
channel_id: id,
|
||||||
|
data: Bytes::copy_from_slice(bytes),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
ch
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close(id: i32) -> PortForwardChannel {
|
||||||
|
let mut ch = PortForwardChannel::new();
|
||||||
|
ch.set_close(PortForwardClose { channel_id: id, ..Default::default() });
|
||||||
|
ch
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn next_frame(rx: &mut mpsc::UnboundedReceiver<(Instant, Arc<Message>)>) -> PortForwardChannel {
|
||||||
|
let (_, m) = rx.recv().await.unwrap();
|
||||||
|
match &m.union {
|
||||||
|
Some(message::Union::PortForwardChannel(ch)) => ch.clone(),
|
||||||
|
other => panic!("unexpected {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opened(ch: &PortForwardChannel) -> (i32, bool) {
|
||||||
|
match &ch.union {
|
||||||
|
Some(port_forward_channel::Union::Opened(o)) => (o.channel_id, o.success),
|
||||||
|
other => panic!("expected opened, got {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data_of(ch: &PortForwardChannel) -> (i32, Vec<u8>) {
|
||||||
|
match &ch.union {
|
||||||
|
Some(port_forward_channel::Union::Data(d)) => (d.channel_id, d.data.to_vec()),
|
||||||
|
other => panic!("expected data, got {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn open_connects_and_echoes_pipelined_data() {
|
||||||
|
rt().block_on(async {
|
||||||
|
let port = echo_target().await;
|
||||||
|
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||||
|
let mut mux = PortForwardMux::new(tx, "localhost:1".to_owned());
|
||||||
|
mux.handle(open(1, port), true);
|
||||||
|
mux.handle(data(1, b"ping"), true);
|
||||||
|
assert_eq!(opened(&next_frame(&mut rx).await), (1, true));
|
||||||
|
assert_eq!(data_of(&next_frame(&mut rx).await), (1, b"ping".to_vec()));
|
||||||
|
mux.handle(close(1), true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unreachable_target_fails_open_and_discards_pipelined_data() {
|
||||||
|
rt().block_on(async {
|
||||||
|
let l = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let port = l.local_addr().unwrap().port();
|
||||||
|
drop(l);
|
||||||
|
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||||
|
let mut mux = PortForwardMux::new(tx, "localhost:1".to_owned());
|
||||||
|
mux.handle(open(1, port), true);
|
||||||
|
mux.handle(data(1, b"lost"), true);
|
||||||
|
assert_eq!(opened(&next_frame(&mut rx).await), (1, false));
|
||||||
|
assert!(tokio::time::timeout(std::time::Duration::from_millis(50), rx.recv()).await.is_err());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn permission_denied_refuses_without_spawning() {
|
||||||
|
rt().block_on(async {
|
||||||
|
let port = echo_target().await;
|
||||||
|
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||||
|
let mut mux = PortForwardMux::new(tx, "localhost:1".to_owned());
|
||||||
|
mux.handle(open(1, port), false);
|
||||||
|
assert_eq!(opened(&next_frame(&mut rx).await), (1, false));
|
||||||
|
assert_eq!(mux.live_channels(), 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn close_while_connecting_sends_no_opened() {
|
||||||
|
rt().block_on(async {
|
||||||
|
// `close` is queued before the task is first polled. Its `select!` is
|
||||||
|
// biased towards the command arm, so even a connect that completes on
|
||||||
|
// that same poll loses: no `opened` may ever be sent.
|
||||||
|
let port = echo_target().await;
|
||||||
|
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||||
|
let mut mux = PortForwardMux::new(tx, "localhost:1".to_owned());
|
||||||
|
mux.handle(open(1, port), true);
|
||||||
|
mux.handle(close(1), true);
|
||||||
|
assert!(tokio::time::timeout(std::time::Duration::from_millis(200), rx.recv()).await.is_err());
|
||||||
|
assert_eq!(mux.live_channels(), 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn over_window_data_closes_only_that_channel() {
|
||||||
|
rt().block_on(async {
|
||||||
|
let port = echo_target().await;
|
||||||
|
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||||
|
let mut mux = PortForwardMux::new(tx, "localhost:1".to_owned());
|
||||||
|
mux.handle(open(1, port), true);
|
||||||
|
mux.handle(open(2, port), true);
|
||||||
|
let mut seen = 0;
|
||||||
|
while seen < 2 {
|
||||||
|
opened(&next_frame(&mut rx).await);
|
||||||
|
seen += 1;
|
||||||
|
}
|
||||||
|
let too_much = vec![0u8; CHANNEL_WINDOW as usize + 1];
|
||||||
|
mux.handle(data(1, &too_much), true);
|
||||||
|
let ch = next_frame(&mut rx).await;
|
||||||
|
match &ch.union {
|
||||||
|
Some(port_forward_channel::Union::Close(c)) => assert_eq!(c.channel_id, 1),
|
||||||
|
other => panic!("expected close, got {:?}", other),
|
||||||
|
}
|
||||||
|
mux.handle(data(2, b"still fine"), true);
|
||||||
|
assert_eq!(data_of(&next_frame(&mut rx).await), (2, b"still fine".to_vec()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pending_bytes_are_bounded_by_initial_window_before_opened() {
|
||||||
|
// A loopback connect completes before a task can observe "connecting",
|
||||||
|
// so the bound is pinned on the pure predicate the task uses.
|
||||||
|
assert!(pending_fits(0, INITIAL_WINDOW as usize));
|
||||||
|
assert!(pending_fits(
|
||||||
|
INITIAL_WINDOW as usize - MIN_FRAME_CHARGE as usize,
|
||||||
|
1
|
||||||
|
));
|
||||||
|
// A 1-byte frame costs a whole minimum charge here too.
|
||||||
|
assert!(!pending_fits(
|
||||||
|
INITIAL_WINDOW as usize - MIN_FRAME_CHARGE as usize + 1,
|
||||||
|
1
|
||||||
|
));
|
||||||
|
assert!(!pending_fits(usize::MAX, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A target that accepts and hangs up at once, so every channel ends on
|
||||||
|
/// the target's EOF — the case where only the sweep can free the entry.
|
||||||
|
async fn drop_target() -> u16 {
|
||||||
|
let l = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let port = l.local_addr().unwrap().port();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let (s, _) = l.accept().await.unwrap();
|
||||||
|
drop(s);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
port
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sweep_frees_dead_entries_so_the_cap_counts_live_channels() {
|
||||||
|
rt().block_on(async {
|
||||||
|
let port = drop_target().await;
|
||||||
|
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||||
|
let mut mux = PortForwardMux::new(tx, "localhost:1".to_owned());
|
||||||
|
for id in 1..=(MAX_CHANNELS as i32 * 2) {
|
||||||
|
mux.handle(open(id, port), true);
|
||||||
|
assert_eq!(opened(&next_frame(&mut rx).await), (id, true));
|
||||||
|
// The task sends `close` on the target's EOF and exits; the
|
||||||
|
// entry is dead until the next `open` sweeps it.
|
||||||
|
let ch = next_frame(&mut rx).await;
|
||||||
|
match &ch.union {
|
||||||
|
Some(port_forward_channel::Union::Close(c)) => assert_eq!(c.channel_id, id),
|
||||||
|
other => panic!("expected close, got {:?}", other),
|
||||||
|
}
|
||||||
|
tokio::task::yield_now().await;
|
||||||
|
}
|
||||||
|
// A task has exited by the time its `close` is read, so one sweep
|
||||||
|
// must leave nothing behind.
|
||||||
|
mux.sweep();
|
||||||
|
assert_eq!(mux.live_channels(), 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn label_counts_distinct_targets_only() {
|
||||||
|
rt().block_on(async {
|
||||||
|
let a = echo_target().await;
|
||||||
|
let b = echo_target().await;
|
||||||
|
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||||
|
let mut mux = PortForwardMux::new(tx, format!("127.0.0.1:{}", a));
|
||||||
|
assert_eq!(mux.sweep(), None);
|
||||||
|
mux.handle(open(1, a), true);
|
||||||
|
mux.handle(open(2, a), true);
|
||||||
|
opened(&next_frame(&mut rx).await);
|
||||||
|
opened(&next_frame(&mut rx).await);
|
||||||
|
assert_eq!(mux.sweep(), None);
|
||||||
|
mux.handle(open(3, b), true);
|
||||||
|
opened(&next_frame(&mut rx).await);
|
||||||
|
assert_eq!(mux.sweep(), Some(format!("127.0.0.1:{} +1", a)));
|
||||||
|
mux.handle(close(3), true);
|
||||||
|
tokio::task::yield_now().await;
|
||||||
|
assert_eq!(mux.sweep(), Some(format!("127.0.0.1:{}", a)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user