backpressure, bufferbloat fix and legs reconnects
This commit is contained in:
@@ -19,20 +19,15 @@ use netrunner_logger::{debug, info};
|
||||
|
||||
pub struct ConnectionCore<T> {
|
||||
pub handle: SocketHandle,
|
||||
pub tx: mpsc::UnboundedSender<T>, // 🔥 Полностью Unbounded
|
||||
pub rx: mpsc::UnboundedReceiver<Bytes>, // 🔥 Полностью Unbounded
|
||||
pub tx: mpsc::Sender<T>,
|
||||
pub rx: mpsc::Receiver<Bytes>,
|
||||
}
|
||||
|
||||
impl<T> ConnectionCore<T> {
|
||||
pub fn new(
|
||||
handle: SocketHandle,
|
||||
) -> (
|
||||
Self,
|
||||
mpsc::UnboundedReceiver<T>,
|
||||
mpsc::UnboundedSender<Bytes>,
|
||||
) {
|
||||
let (tx_to_net, rx_from_smol) = mpsc::unbounded_channel::<T>();
|
||||
let (tx_to_smol, rx_from_net) = mpsc::unbounded_channel::<Bytes>();
|
||||
pub fn new(handle: SocketHandle) -> (Self, mpsc::Receiver<T>, mpsc::Sender<Bytes>) {
|
||||
let cap = NetworkConfig::global().channel_capacity;
|
||||
let (tx_to_net, rx_from_smol) = mpsc::channel::<T>(cap);
|
||||
let (tx_to_smol, rx_from_net) = mpsc::channel::<Bytes>(cap);
|
||||
|
||||
let core = Self {
|
||||
handle,
|
||||
@@ -56,6 +51,7 @@ pub struct TcpConnection {
|
||||
core: ConnectionCore<Bytes>,
|
||||
state: ConnectionState,
|
||||
pending_data: VecDeque<Bytes>,
|
||||
pending_bytes: usize,
|
||||
handshake_rx: Option<oneshot::Receiver<()>>,
|
||||
chunk_buf: Vec<u8>,
|
||||
server_eof: bool,
|
||||
@@ -66,8 +62,8 @@ impl TcpConnection {
|
||||
handle: SocketHandle,
|
||||
) -> (
|
||||
Self,
|
||||
mpsc::UnboundedReceiver<Bytes>,
|
||||
mpsc::UnboundedSender<Bytes>,
|
||||
mpsc::Receiver<Bytes>,
|
||||
mpsc::Sender<Bytes>,
|
||||
oneshot::Sender<()>,
|
||||
) {
|
||||
let (core, rx_from_smol, tx_to_smol) = ConnectionCore::new(handle);
|
||||
@@ -77,6 +73,7 @@ impl TcpConnection {
|
||||
core,
|
||||
state: ConnectionState::Handshaking,
|
||||
pending_data: VecDeque::new(),
|
||||
pending_bytes: 0,
|
||||
handshake_rx: Some(handshake_rx),
|
||||
chunk_buf: vec![0u8; NetworkConfig::global().tcp_chunk_size],
|
||||
server_eof: false,
|
||||
@@ -139,7 +136,8 @@ impl TcpConnection {
|
||||
let rtt = Duration::from_millis(current_rtt as u64);
|
||||
socket.set_tunnel_rtt(rtt);
|
||||
|
||||
if self.app_pending_out_size() < 512 * 1024 {
|
||||
// Читаем из браузера в Туннель
|
||||
if self.core.tx.capacity() > 0 {
|
||||
while socket.can_recv() {
|
||||
if let Ok(n) = socket.peek_slice(&mut self.chunk_buf) {
|
||||
if n == 0 {
|
||||
@@ -147,11 +145,17 @@ impl TcpConnection {
|
||||
}
|
||||
let chunk = Bytes::copy_from_slice(&self.chunk_buf[..n]);
|
||||
|
||||
if self.core.tx.send(chunk).is_ok() {
|
||||
socket.recv_slice(&mut self.chunk_buf[..n]).unwrap();
|
||||
} else {
|
||||
self.server_eof = true;
|
||||
break;
|
||||
match self.core.tx.try_send(chunk) {
|
||||
Ok(_) => {
|
||||
socket.recv_slice(&mut self.chunk_buf[..n]).unwrap();
|
||||
}
|
||||
Err(mpsc::error::TrySendError::Full(_)) => {
|
||||
break;
|
||||
}
|
||||
Err(mpsc::error::TrySendError::Closed(_)) => {
|
||||
self.server_eof = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
@@ -159,10 +163,15 @@ impl TcpConnection {
|
||||
}
|
||||
}
|
||||
|
||||
// Читаем из Туннеля в браузер
|
||||
if !self.server_eof {
|
||||
loop {
|
||||
// 🔥 РЕАЛЬНЫЙ BACKPRESSURE:
|
||||
// Читаем из канала, ТОЛЬКО если в очереди < 4 МБ данных.
|
||||
// Если больше - оставляем лежать в канале Tokio, пока smoltcp не освободится!
|
||||
while self.pending_bytes < 4 * 1024 * 1024 {
|
||||
match self.core.rx.try_recv() {
|
||||
Ok(data) => {
|
||||
self.pending_bytes += data.len();
|
||||
self.pending_data.push_back(data);
|
||||
}
|
||||
Err(mpsc::error::TryRecvError::Empty) => break,
|
||||
@@ -179,6 +188,7 @@ impl TcpConnection {
|
||||
if let Some(mut chunk) = self.pending_data.pop_front() {
|
||||
match socket.send_slice(&chunk) {
|
||||
Ok(n) => {
|
||||
self.pending_bytes -= n;
|
||||
if n < chunk.len() {
|
||||
chunk.advance(n);
|
||||
self.pending_data.push_front(chunk);
|
||||
@@ -205,22 +215,24 @@ impl TcpConnection {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn app_pending_out_size(&self) -> usize {
|
||||
self.pending_bytes
|
||||
}
|
||||
|
||||
pub fn spawn(
|
||||
socket_id: u64,
|
||||
dst_ip: std::net::Ipv4Addr,
|
||||
dst_port: u16,
|
||||
target: String,
|
||||
mut rx_smol: mpsc::UnboundedReceiver<Bytes>,
|
||||
mut rx_smol: mpsc::Receiver<Bytes>,
|
||||
handshake_tx: oneshot::Sender<()>,
|
||||
// 🔥 ФИКС: Тип изменен на UnboundedSender
|
||||
tx_tunnel: mpsc::UnboundedSender<RawCastFrame>,
|
||||
tx_tunnel: mpsc::Sender<RawCastFrame>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
let mut frame = RawCastFrame::connect(LocalProtocol::Tcp, socket_id, dst_ip, dst_port);
|
||||
frame.payload = Bytes::from(target);
|
||||
|
||||
// send() на UnboundedSender не асинхронный и возвращает Result<(), T>
|
||||
if tx_tunnel.send(frame).is_err() {
|
||||
if tx_tunnel.send(frame).await.is_err() {
|
||||
netrunner_logger::error!("❌ [TCP {}] Failed to send CONNECT to tunnel", socket_id);
|
||||
return;
|
||||
}
|
||||
@@ -236,21 +248,17 @@ impl TcpConnection {
|
||||
data.to_vec(),
|
||||
);
|
||||
|
||||
if tx_tunnel.send(data_frame).is_err() {
|
||||
if tx_tunnel.send(data_frame).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let close_frame = RawCastFrame::close(LocalProtocol::Tcp, socket_id, dst_ip, dst_port);
|
||||
let _ = tx_tunnel.send(close_frame);
|
||||
let _ = tx_tunnel.send(close_frame).await;
|
||||
|
||||
debug!("🏁 [TCP {}] Spawned task finished", socket_id);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn app_pending_out_size(&self) -> usize {
|
||||
self.pending_data.iter().map(|b| b.len()).sum()
|
||||
}
|
||||
}
|
||||
|
||||
pub type UdpPacketTarget = (Bytes, std::net::Ipv4Addr, u16);
|
||||
@@ -266,11 +274,7 @@ impl UdpConnection {
|
||||
handle: SocketHandle,
|
||||
client_addr: smoltcp::wire::IpAddress,
|
||||
client_port: u16,
|
||||
) -> (
|
||||
Self,
|
||||
mpsc::UnboundedReceiver<UdpPacketTarget>,
|
||||
mpsc::UnboundedSender<Bytes>,
|
||||
) {
|
||||
) -> (Self, mpsc::Receiver<UdpPacketTarget>, mpsc::Sender<Bytes>) {
|
||||
let (core, rx_from_smol, tx_to_smol) = ConnectionCore::new(handle);
|
||||
let conn = Self {
|
||||
core,
|
||||
@@ -299,7 +303,7 @@ impl UdpConnection {
|
||||
let target_port = metadata.endpoint.port;
|
||||
let payload = (Bytes::copy_from_slice(data), target_ip, target_port);
|
||||
|
||||
if self.core.tx.send(payload).is_ok() {
|
||||
if self.core.tx.try_send(payload).is_ok() {
|
||||
self.last_activity = std::time::Instant::now();
|
||||
}
|
||||
}
|
||||
@@ -328,16 +332,15 @@ impl UdpConnection {
|
||||
dst_ip: std::net::Ipv4Addr,
|
||||
dst_port: u16,
|
||||
target: String,
|
||||
mut rx_smol: mpsc::UnboundedReceiver<UdpPacketTarget>,
|
||||
// 🔥 ФИКС: Тип изменен на UnboundedSender
|
||||
tx_tunnel: mpsc::UnboundedSender<RawCastFrame>,
|
||||
mut rx_smol: mpsc::Receiver<UdpPacketTarget>,
|
||||
tx_tunnel: mpsc::Sender<RawCastFrame>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
debug!("📡 [UDP {}] Task started for {}", socket_id, target);
|
||||
let mut frame = RawCastFrame::connect(LocalProtocol::Udp, socket_id, dst_ip, dst_port);
|
||||
frame.payload = Bytes::from(target);
|
||||
|
||||
if tx_tunnel.send(frame).is_err() {
|
||||
if tx_tunnel.send(frame).await.is_err() {
|
||||
netrunner_logger::error!("❌ [UDP {}] Failed to send CONNECT to tunnel", socket_id);
|
||||
return;
|
||||
}
|
||||
@@ -345,19 +348,18 @@ impl UdpConnection {
|
||||
while let Some((data, ip, port)) = rx_smol.recv().await {
|
||||
let data_frame =
|
||||
RawCastFrame::data(LocalProtocol::Udp, socket_id, ip, port, data.to_vec());
|
||||
if tx_tunnel.send(data_frame).is_err() {
|
||||
if tx_tunnel.send(data_frame).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let close_frame = RawCastFrame::close(LocalProtocol::Udp, socket_id, dst_ip, dst_port);
|
||||
let _ = tx_tunnel.send(close_frame);
|
||||
let _ = tx_tunnel.send(close_frame).await;
|
||||
info!("🛑 [UDP {}] Task stopped", socket_id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// IcmpResponder оставляем как был (он не использует каналы)
|
||||
use smoltcp::socket::icmp;
|
||||
|
||||
pub struct IcmpResponder;
|
||||
|
||||
@@ -13,7 +13,7 @@ use smoltcp::{
|
||||
wire::{IpAddress, IpListenEndpoint, IpProtocol, Ipv4Packet, Ipv6Packet, TcpPacket, UdpPacket},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant; // Используем DashMap для потокобезопасного трекинга
|
||||
use std::time::Instant;
|
||||
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
@@ -24,7 +24,6 @@ use crate::net::{
|
||||
socket_factory::SocketProvider,
|
||||
};
|
||||
|
||||
// Ключ для идентификации уникального потока (4-tuple)
|
||||
type FlowKey = (IpAddress, u16, IpAddress, u16);
|
||||
|
||||
struct Flow {
|
||||
@@ -78,7 +77,7 @@ impl TargetResolver {
|
||||
pub struct ConnectionManager {
|
||||
tracker: SessionTracker,
|
||||
resolver: TargetResolver,
|
||||
tx_to_tunnel: mpsc::UnboundedSender<RawCastFrame>,
|
||||
tx_to_tunnel: mpsc::Sender<RawCastFrame>, // 🔥 Bounded
|
||||
factory: Arc<dyn SocketProvider>,
|
||||
pending_connects: DashMap<FlowKey, Instant>,
|
||||
}
|
||||
@@ -86,7 +85,7 @@ pub struct ConnectionManager {
|
||||
impl ConnectionManager {
|
||||
pub fn new(
|
||||
dns_handler: DnsHandler,
|
||||
tx_to_tunnel: mpsc::UnboundedSender<RawCastFrame>, // 🔥 Unbounded
|
||||
tx_to_tunnel: mpsc::Sender<RawCastFrame>,
|
||||
factory: Arc<dyn SocketProvider>,
|
||||
) -> Self {
|
||||
Self {
|
||||
@@ -109,9 +108,15 @@ impl ConnectionManager {
|
||||
}
|
||||
|
||||
if let Some(tx) = self.tracker.get_inbound_tx(frame.socket_id) {
|
||||
// 🔥 UnboundedSender::send не требует await и не возвращает Full
|
||||
if let Err(_) = tx.send(frame.payload.clone()) {
|
||||
self.tracker.close_tunnel_session(frame.socket_id);
|
||||
// 🔥 Защита от переполнения: Если очередь Bounded канала полна,
|
||||
// мы тихо удаляем пакет. Очередь освободится, и TCP/BBR сделает свое дело (надежная доставка).
|
||||
// Нельзя закрывать соединение!
|
||||
if let Err(e) = tx.try_send(frame.payload.clone()) {
|
||||
if let mpsc::error::TrySendError::Closed(_) = e {
|
||||
self.tracker.close_tunnel_session(frame.socket_id);
|
||||
} else {
|
||||
// Backpressure in action: drop the packet, TCP will handle it
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -174,7 +179,6 @@ impl ConnectionManager {
|
||||
if let Ok(p) = TcpPacket::new_checked(ip.payload()) {
|
||||
flow.src_p = p.src_port();
|
||||
flow.dst_p = p.dst_port();
|
||||
// Игнорируем пакеты, если это не SYN (новое соединение)
|
||||
if p.syn() && !p.ack() {
|
||||
self.intercept_tcp(flow, socket_set);
|
||||
}
|
||||
@@ -262,7 +266,6 @@ impl ConnectionManager {
|
||||
|
||||
if socket.is_open() {
|
||||
let handle = socket_set.add(socket);
|
||||
// 🔥 Принимаем Unbounded части из конструктора
|
||||
let (conn, rx_smol, tx_smol) = UdpConnection::new(handle, f.src, f.src_p);
|
||||
|
||||
self.tracker.register_udp(handle, socket_id, conn, tx_smol);
|
||||
@@ -278,11 +281,9 @@ impl ConnectionManager {
|
||||
}
|
||||
|
||||
pub fn process_sockets(&mut self, socket_set: &mut SocketSet) {
|
||||
// Собираем хендлы, чтобы избежать конфликтов заимствования (borrow checker)
|
||||
let handles: Vec<SocketHandle> = socket_set.iter().map(|(h, _)| h).collect();
|
||||
|
||||
for handle in handles {
|
||||
// 🔥 ИСПРАВЛЕНИЕ: get_mut возвращает &mut Socket, а не Option<&mut Socket>
|
||||
let socket = socket_set.get_mut(handle);
|
||||
|
||||
match socket {
|
||||
@@ -311,7 +312,6 @@ impl ConnectionManager {
|
||||
let socket_id = self.tracker.next_id();
|
||||
let (dst_ip, target) = self.resolver.resolve_destination(local.addr, local.port);
|
||||
|
||||
// 🔥 Принимаем Unbounded части
|
||||
let (conn, rx_smol, tx_smol, handshake_tx) = TcpConnection::new(handle);
|
||||
self.tracker.register_tcp(handle, socket_id, conn, tx_smol);
|
||||
|
||||
@@ -350,7 +350,6 @@ impl ConnectionManager {
|
||||
}
|
||||
|
||||
pub fn cleanup(&mut self, socket_set: &mut SocketSet) {
|
||||
// 🔥 ФИКС 3: Очистка зависших pending_connects (например, если SYN потерялся)
|
||||
self.pending_connects
|
||||
.retain(|_, timestamp| timestamp.elapsed() < TCP_HANDSHAKE_TIMEOUT);
|
||||
|
||||
@@ -362,7 +361,3 @@ impl ConnectionManager {
|
||||
self.tracker.get_app_buffer_info(handle)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_handshaking(state: tcp::State) -> bool {
|
||||
matches!(state, tcp::State::SynSent | tcp::State::SynReceived)
|
||||
}
|
||||
|
||||
+10
-13
@@ -15,8 +15,7 @@ use std::{
|
||||
time::Instant as StdInstant,
|
||||
};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
use tokio::sync::mpsc::{self, UnboundedSender}; // 🔥 Добавили UnboundedSender
|
||||
use tokio::sync::mpsc::{self, Receiver, Sender, UnboundedReceiver, UnboundedSender};
|
||||
use tokio::time::{Duration, sleep};
|
||||
use tun::{DeviceReader, DeviceWriter};
|
||||
|
||||
@@ -36,10 +35,10 @@ pub struct Engine {
|
||||
socket_set: SocketSet<'static>,
|
||||
manager: ConnectionManager,
|
||||
device: VirtTunDevice,
|
||||
to_smoltcp_tx: UnboundedSender<TokenBuffer>, // 🔥 Стало Unbounded
|
||||
to_smoltcp_tx: UnboundedSender<TokenBuffer>,
|
||||
from_smoltcp_rx: Option<UnboundedReceiver<TokenBuffer>>,
|
||||
avail: Arc<AtomicBool>,
|
||||
rx_from_tunnel: mpsc::UnboundedReceiver<RawCastFrame>, // 🔥 Стало Unbounded
|
||||
rx_from_tunnel: Receiver<RawCastFrame>, // 🔥 Bounded
|
||||
factory: Arc<dyn SocketProvider>,
|
||||
}
|
||||
|
||||
@@ -48,13 +47,12 @@ impl Engine {
|
||||
config: Config,
|
||||
caps: DeviceCapabilities,
|
||||
dns_handler: DnsHandler,
|
||||
tx_to_tunnel: mpsc::UnboundedSender<RawCastFrame>, // 🔥 Стало Unbounded
|
||||
rx_from_tunnel: mpsc::UnboundedReceiver<RawCastFrame>, // 🔥 Стало Unbounded
|
||||
tx_to_tunnel: Sender<RawCastFrame>, // 🔥 Bounded
|
||||
rx_from_tunnel: Receiver<RawCastFrame>, // 🔥 Bounded
|
||||
factory: Arc<dyn SocketProvider>,
|
||||
) -> Self {
|
||||
let now = Engine::current_time();
|
||||
|
||||
// VirtTunDevice::new возвращает UnboundedSender и UnboundedReceiver
|
||||
let (mut device, to_smoltcp_tx, from_smoltcp_rx, avail) = VirtTunDevice::new(caps);
|
||||
let interface = Interface::new(config, &mut device, now);
|
||||
|
||||
@@ -79,7 +77,6 @@ impl Engine {
|
||||
info!("Current routes: {:?}", self.interface.routes());
|
||||
let (writer, reader) = tun.split().expect("Failed to split TUN");
|
||||
|
||||
// TUN Reader теперь тоже Unbounded
|
||||
let (tun_to_engine_tx, mut tun_to_engine_rx) = mpsc::unbounded_channel::<TokenBuffer>();
|
||||
|
||||
Self::spawn_tun_reader(reader, tun_to_engine_tx, self.avail.clone());
|
||||
@@ -118,7 +115,6 @@ impl Engine {
|
||||
msg = self.rx_from_tunnel.recv() => {
|
||||
if let Some(frame) = msg {
|
||||
let _ = self.manager.try_inject_inbound(frame);
|
||||
// Выгребаем пачку
|
||||
let mut count = 0;
|
||||
while let Ok(frame) = self.rx_from_tunnel.try_recv() {
|
||||
let _ = self.manager.try_inject_inbound(frame);
|
||||
@@ -131,7 +127,6 @@ impl Engine {
|
||||
msg = tun_to_engine_rx.recv() => {
|
||||
if let Some(token) = msg {
|
||||
self.manager.try_create_socket_from_packet(&token, &mut self.socket_set);
|
||||
// Отправка в Unbounded всегда мгновенная
|
||||
if self.to_smoltcp_tx.send(token).is_ok() {
|
||||
self.device.mark_rx_available();
|
||||
}
|
||||
@@ -149,7 +144,7 @@ impl Engine {
|
||||
|
||||
fn spawn_tun_reader(
|
||||
mut reader: DeviceReader,
|
||||
to_engine: mpsc::UnboundedSender<TokenBuffer>, // 🔥 Unbounded
|
||||
to_engine: mpsc::UnboundedSender<TokenBuffer>,
|
||||
is_avail: Arc<AtomicBool>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
@@ -303,8 +298,10 @@ impl EngineBuilder {
|
||||
caps.max_transmission_unit = self.config.mtu;
|
||||
caps.medium = smoltcp::phy::Medium::Ip;
|
||||
|
||||
let (tx_to_tunnel, rx_for_client_handler) = mpsc::unbounded_channel::<RawCastFrame>();
|
||||
let (tx_for_client_handler, rx_from_tunnel) = mpsc::unbounded_channel::<RawCastFrame>();
|
||||
// 🔥 Используем Bounded каналы для Tunnel <-> Engine
|
||||
let cap = NetworkConfig::global().channel_capacity;
|
||||
let (tx_to_tunnel, rx_for_client_handler) = mpsc::channel::<RawCastFrame>(cap);
|
||||
let (tx_for_client_handler, rx_from_tunnel) = mpsc::channel::<RawCastFrame>(cap);
|
||||
|
||||
info!("Establishing secure tunnel to proxy server...");
|
||||
ClientHandler::connect(
|
||||
|
||||
@@ -17,8 +17,7 @@ pub struct SessionTracker {
|
||||
last_activity: HashMap<SocketHandle, StdInstant>,
|
||||
active_tcp: HashMap<SocketHandle, TcpConnection>,
|
||||
active_udp: HashMap<SocketHandle, UdpConnection>,
|
||||
// 🔥 Теперь храним UnboundedSender
|
||||
inbound_tx: HashMap<u64, mpsc::UnboundedSender<Bytes>>,
|
||||
inbound_tx: HashMap<u64, mpsc::Sender<Bytes>>, // 🔥 Bounded
|
||||
handle_to_id: HashMap<SocketHandle, u64>,
|
||||
|
||||
id_to_handle: HashMap<u64, SocketHandle>,
|
||||
@@ -57,7 +56,7 @@ impl SessionTracker {
|
||||
handle: SocketHandle,
|
||||
id: u64,
|
||||
conn: TcpConnection,
|
||||
tx: mpsc::UnboundedSender<Bytes>, // 🔥 Unbounded
|
||||
tx: mpsc::Sender<Bytes>,
|
||||
) {
|
||||
self.pending_tcp.remove(&handle);
|
||||
self.handle_to_id.insert(handle, id);
|
||||
@@ -72,7 +71,7 @@ impl SessionTracker {
|
||||
handle: SocketHandle,
|
||||
id: u64,
|
||||
conn: UdpConnection,
|
||||
tx: mpsc::UnboundedSender<Bytes>, // 🔥 Unbounded
|
||||
tx: mpsc::Sender<Bytes>,
|
||||
) {
|
||||
self.handle_to_id.insert(handle, id);
|
||||
self.id_to_handle.insert(id, handle);
|
||||
@@ -123,7 +122,7 @@ impl SessionTracker {
|
||||
self.last_activity.insert(handle, StdInstant::now());
|
||||
}
|
||||
|
||||
pub fn get_inbound_tx(&self, id: u64) -> Option<&mpsc::UnboundedSender<Bytes>> {
|
||||
pub fn get_inbound_tx(&self, id: u64) -> Option<&mpsc::Sender<Bytes>> {
|
||||
self.inbound_tx.get(&id)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user