remove absolute dependence on topology
This commit is contained in:
@@ -1,13 +1,12 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client::encryption::{PRIVATE_KEY_SIZE, PUBLIC_KEY_SIZE};
|
||||
use futures::channel::mpsc;
|
||||
use futures::StreamExt;
|
||||
use log::*;
|
||||
use nym_client_core::client::topology_control::accessor::TopologyAccessor;
|
||||
use nym_crypto::asymmetric::encryption;
|
||||
use nym_noise::upgrade_noise_initiator;
|
||||
use nym_noise::upgrade_noise_initiator_with_topology;
|
||||
use nym_sphinx::addressing::nodes::NymNodeRoutingAddress;
|
||||
use nym_sphinx::framing::codec::NymCodec;
|
||||
use nym_sphinx::framing::packet::FramedNymPacket;
|
||||
@@ -64,8 +63,7 @@ pub struct Client {
|
||||
conn_new: HashMap<NymNodeRoutingAddress, ConnectionSender>,
|
||||
config: Config,
|
||||
topology_access: TopologyAccessor,
|
||||
public_id_key: [u8; PUBLIC_KEY_SIZE],
|
||||
private_id_key: [u8; PRIVATE_KEY_SIZE],
|
||||
local_identity: Arc<encryption::KeyPair>,
|
||||
}
|
||||
|
||||
struct ConnectionSender {
|
||||
@@ -86,14 +84,13 @@ impl Client {
|
||||
pub fn new(
|
||||
config: Config,
|
||||
topology_access: TopologyAccessor,
|
||||
id_key: &encryption::KeyPair,
|
||||
local_identity: Arc<encryption::KeyPair>,
|
||||
) -> Client {
|
||||
Client {
|
||||
conn_new: HashMap::new(),
|
||||
config,
|
||||
topology_access,
|
||||
public_id_key: id_key.public_key().to_bytes(),
|
||||
private_id_key: id_key.private_key().to_bytes(),
|
||||
local_identity,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +121,7 @@ impl Client {
|
||||
}
|
||||
};
|
||||
|
||||
let noise_stream = match upgrade_noise_initiator(
|
||||
let noise_stream = match upgrade_noise_initiator_with_topology(
|
||||
stream,
|
||||
topology_ref,
|
||||
local_public_key,
|
||||
@@ -218,8 +215,8 @@ impl Client {
|
||||
let initial_connection_timeout = self.config.initial_connection_timeout;
|
||||
|
||||
let topology_access_clone = self.topology_access.clone();
|
||||
let local_private_key = self.private_id_key.clone();
|
||||
let local_public_key = self.public_id_key.clone();
|
||||
let local_public_key = self.local_identity.public_key().to_bytes();
|
||||
let local_private_key = self.local_identity.private_key().to_bytes();
|
||||
|
||||
tokio::spawn(async move {
|
||||
// before executing the manager, wait for what was specified, if anything
|
||||
|
||||
@@ -8,6 +8,7 @@ use log::*;
|
||||
use nym_client_core::client::topology_control::accessor::TopologyAccessor;
|
||||
use nym_crypto::asymmetric::encryption;
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
pub type MixForwardingSender = mpsc::UnboundedSender<MixPacket>;
|
||||
@@ -29,7 +30,7 @@ impl PacketForwarder {
|
||||
maximum_connection_buffer_size: usize,
|
||||
use_legacy_version: bool,
|
||||
topology_access: TopologyAccessor,
|
||||
sphinx_key: &encryption::KeyPair,
|
||||
local_identity: Arc<encryption::KeyPair>,
|
||||
shutdown: nym_task::TaskClient,
|
||||
) -> (PacketForwarder, MixForwardingSender) {
|
||||
let client_config = Config::new(
|
||||
@@ -44,7 +45,7 @@ impl PacketForwarder {
|
||||
|
||||
(
|
||||
PacketForwarder {
|
||||
mixnet_client: Client::new(client_config, topology_access, sphinx_key),
|
||||
mixnet_client: Client::new(client_config, topology_access, local_identity),
|
||||
packet_receiver,
|
||||
shutdown,
|
||||
},
|
||||
|
||||
+63
-29
@@ -198,31 +198,14 @@ impl AsyncWrite for NoiseStream {
|
||||
|
||||
pub async fn upgrade_noise_initiator(
|
||||
mut conn: TcpStream,
|
||||
topology: &NymTopology,
|
||||
local_public_key: &[u8],
|
||||
local_public_key: Option<&[u8]>,
|
||||
local_private_key: &[u8],
|
||||
remote_pub_key: &[u8],
|
||||
) -> Result<NoiseStream, NoiseError> {
|
||||
trace!("Perform Noise Handshake, initiator side");
|
||||
|
||||
//Get init material
|
||||
let responder_addr = match conn.peer_addr() {
|
||||
Ok(addr) => addr,
|
||||
Err(err) => {
|
||||
error!("Unable to extract peer address from connection - {err}");
|
||||
return Err(Error::Prereq(Prerequisite::RemotePublicKey).into());
|
||||
}
|
||||
};
|
||||
let remote_pub_key = match topology.find_node_key_by_mix_host(responder_addr) {
|
||||
Some(pub_key) => pub_key.to_bytes(),
|
||||
None => {
|
||||
error!(
|
||||
"Cannot find public key for node with address {:?}",
|
||||
responder_addr
|
||||
);
|
||||
return Err(Error::Prereq(Prerequisite::RemotePublicKey).into());
|
||||
}
|
||||
};
|
||||
let secret = [local_public_key, &remote_pub_key].concat();
|
||||
//In case the local key cannot be known by the remote party, e.g. in a client-gateway connection
|
||||
let secret = [local_public_key.unwrap_or(&[]), remote_pub_key].concat();
|
||||
let secret_hash = Sha256::digest(secret);
|
||||
|
||||
let builder = Builder::new(NOISE_HS_PATTERN.parse().unwrap()); //This cannot fail, hardcoded pattern must be correct
|
||||
@@ -250,33 +233,50 @@ pub async fn upgrade_noise_initiator(
|
||||
Ok(NoiseStream::new(conn, noise))
|
||||
}
|
||||
|
||||
pub async fn upgrade_noise_responder(
|
||||
mut conn: TcpStream,
|
||||
pub async fn upgrade_noise_initiator_with_topology(
|
||||
conn: TcpStream,
|
||||
topology: &NymTopology,
|
||||
local_public_key: &[u8],
|
||||
local_private_key: &[u8],
|
||||
) -> Result<NoiseStream, NoiseError> {
|
||||
trace!("Perform Noise Handshake, responder side");
|
||||
|
||||
//Get init material
|
||||
let initiator_addr = match conn.peer_addr() {
|
||||
let responder_addr = match conn.peer_addr() {
|
||||
Ok(addr) => addr,
|
||||
Err(err) => {
|
||||
error!("Unable to extract peer address from connection - {err}");
|
||||
return Err(Error::Prereq(Prerequisite::RemotePublicKey).into());
|
||||
}
|
||||
};
|
||||
let remote_pub_key = match topology.find_node_key_by_mix_host(initiator_addr) {
|
||||
let remote_pub_key = match topology.find_node_key_by_mix_host(responder_addr) {
|
||||
Some(pub_key) => pub_key.to_bytes(),
|
||||
None => {
|
||||
error!(
|
||||
"Cannot find public key for node with address {:?}",
|
||||
initiator_addr
|
||||
responder_addr
|
||||
);
|
||||
return Err(Error::Prereq(Prerequisite::RemotePublicKey).into());
|
||||
}
|
||||
};
|
||||
let secret = [&remote_pub_key, local_public_key].concat();
|
||||
|
||||
upgrade_noise_initiator(
|
||||
conn,
|
||||
Some(local_public_key),
|
||||
local_private_key,
|
||||
&remote_pub_key,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn upgrade_noise_responder(
|
||||
mut conn: TcpStream,
|
||||
local_public_key: &[u8],
|
||||
local_private_key: &[u8],
|
||||
remote_pub_key: Option<&[u8]>,
|
||||
) -> Result<NoiseStream, NoiseError> {
|
||||
trace!("Perform Noise Handshake, responder side");
|
||||
|
||||
//If the remote_key cannot be kwnown, e.g. in a client-gateway connection
|
||||
let secret = [&remote_pub_key.unwrap_or(&[]), local_public_key].concat();
|
||||
let secret_hash = Sha256::digest(secret);
|
||||
|
||||
let builder = Builder::new(NOISE_HS_PATTERN.parse().unwrap()); //This cannot fail, hardcoded pattern must be correct
|
||||
@@ -302,6 +302,40 @@ pub async fn upgrade_noise_responder(
|
||||
Ok(NoiseStream::new(conn, noise))
|
||||
}
|
||||
|
||||
pub async fn upgrade_noise_responder_with_topology(
|
||||
conn: TcpStream,
|
||||
topology: &NymTopology,
|
||||
local_public_key: &[u8],
|
||||
local_private_key: &[u8],
|
||||
) -> Result<NoiseStream, NoiseError> {
|
||||
//Get init material
|
||||
let initiator_addr = match conn.peer_addr() {
|
||||
Ok(addr) => addr,
|
||||
Err(err) => {
|
||||
error!("Unable to extract peer address from connection - {err}");
|
||||
return Err(Error::Prereq(Prerequisite::RemotePublicKey).into());
|
||||
}
|
||||
};
|
||||
let remote_pub_key = match topology.find_node_key_by_mix_host(initiator_addr) {
|
||||
Some(pub_key) => pub_key.to_bytes(),
|
||||
None => {
|
||||
error!(
|
||||
"Cannot find public key for node with address {:?}",
|
||||
initiator_addr
|
||||
);
|
||||
return Err(Error::Prereq(Prerequisite::RemotePublicKey).into());
|
||||
}
|
||||
};
|
||||
|
||||
upgrade_noise_responder(
|
||||
conn,
|
||||
local_public_key,
|
||||
local_private_key,
|
||||
Some(&remote_pub_key),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Hyper-basic stream transport receiver. 16-bit BE size followed by payload.
|
||||
async fn recv(stream: &mut TcpStream) -> io::Result<Vec<u8>> {
|
||||
let mut msg_len_buf = [0u8; HEADER_SIZE];
|
||||
|
||||
@@ -6,8 +6,9 @@ use crate::node::client_handling::websocket::connection_handler::coconut::Coconu
|
||||
use crate::node::client_handling::websocket::connection_handler::FreshHandler;
|
||||
use crate::node::storage::Storage;
|
||||
use log::*;
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_crypto::asymmetric::{encryption, identity};
|
||||
use nym_mixnet_client::forwarder::MixForwardingSender;
|
||||
use nym_noise::upgrade_noise_responder;
|
||||
use rand::rngs::OsRng;
|
||||
use std::net::SocketAddr;
|
||||
use std::process;
|
||||
@@ -17,6 +18,7 @@ use tokio::task::JoinHandle;
|
||||
pub(crate) struct Listener {
|
||||
address: SocketAddr,
|
||||
local_identity: Arc<identity::KeyPair>,
|
||||
local_sphinx: Arc<encryption::KeyPair>,
|
||||
only_coconut_credentials: bool,
|
||||
pub(crate) coconut_verifier: Arc<CoconutVerifier>,
|
||||
}
|
||||
@@ -25,12 +27,14 @@ impl Listener {
|
||||
pub(crate) fn new(
|
||||
address: SocketAddr,
|
||||
local_identity: Arc<identity::KeyPair>,
|
||||
local_sphinx: Arc<encryption::KeyPair>,
|
||||
only_coconut_credentials: bool,
|
||||
coconut_verifier: Arc<CoconutVerifier>,
|
||||
) -> Self {
|
||||
Listener {
|
||||
address,
|
||||
local_identity,
|
||||
local_sphinx,
|
||||
only_coconut_credentials,
|
||||
coconut_verifier,
|
||||
}
|
||||
@@ -58,34 +62,49 @@ impl Listener {
|
||||
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.recv() => {
|
||||
log::trace!("client_handling::Listener: received shutdown");
|
||||
}
|
||||
connection = tcp_listener.accept() => {
|
||||
match connection {
|
||||
Ok((socket, remote_addr)) => {
|
||||
trace!("received a socket connection from {remote_addr}");
|
||||
// TODO: I think we *REALLY* need a mechanism for having a maximum number of connected
|
||||
// clients or spawned tokio tasks -> perhaps a worker system?
|
||||
let handle = FreshHandler::new(
|
||||
OsRng,
|
||||
socket,
|
||||
self.only_coconut_credentials,
|
||||
outbound_mix_sender.clone(),
|
||||
Arc::clone(&self.local_identity),
|
||||
storage.clone(),
|
||||
active_clients_store.clone(),
|
||||
Arc::clone(&self.coconut_verifier),
|
||||
);
|
||||
let shutdown = shutdown.clone();
|
||||
tokio::spawn(async move { handle.start_handling(shutdown).await });
|
||||
}
|
||||
Err(err) => warn!("failed to get client: {err}"),
|
||||
}
|
||||
}
|
||||
biased;
|
||||
_ = shutdown.recv() => {
|
||||
log::trace!("client_handling::Listener: received shutdown");
|
||||
}
|
||||
connection = tcp_listener.accept() => {
|
||||
match connection {
|
||||
Ok((socket, remote_addr)) => {
|
||||
trace!("received a socket connection from {remote_addr}");
|
||||
// TODO: I think we *REALLY* need a mechanism for having a maximum number of connected
|
||||
// clients or spawned tokio tasks -> perhaps a worker system?
|
||||
// let noise_stream = match upgrade_noise_responder(
|
||||
// socket,
|
||||
// &self.local_sphinx.public_key().to_bytes(),
|
||||
// &self.local_sphinx.private_key().to_bytes(),
|
||||
// None, //connection from client, no remote pub key
|
||||
// )
|
||||
// .await
|
||||
// {
|
||||
// Ok(noise_stream) => noise_stream,
|
||||
// Err(err) => {
|
||||
// error!("Failed to perform Noise handshake with {remote_addr} - {err}");
|
||||
// return;
|
||||
// }
|
||||
// };
|
||||
let handle = FreshHandler::new(
|
||||
OsRng,
|
||||
socket,
|
||||
// noise_stream,
|
||||
self.only_coconut_credentials,
|
||||
outbound_mix_sender.clone(),
|
||||
Arc::clone(&self.local_identity),
|
||||
storage.clone(),
|
||||
active_clients_store.clone(),
|
||||
Arc::clone(&self.coconut_verifier),
|
||||
);
|
||||
let shutdown = shutdown.clone();
|
||||
tokio::spawn(async move { handle.start_handling(shutdown).await });
|
||||
}
|
||||
Err(err) => warn!("failed to get client: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
use crate::node::client_handling::active_clients::ActiveClientsStore;
|
||||
use crate::node::client_handling::websocket::message_receiver::MixMessageSender;
|
||||
use crate::node::encryption::{PRIVATE_KEY_SIZE, PUBLIC_KEY_SIZE};
|
||||
use crate::node::mixnet_handling::receiver::packet_processing::PacketProcessor;
|
||||
use crate::node::storage::error::StorageError;
|
||||
use crate::node::storage::Storage;
|
||||
@@ -13,7 +12,7 @@ use nym_client_core::client::topology_control::accessor::TopologyAccessor;
|
||||
use nym_crypto::asymmetric::encryption;
|
||||
use nym_mixnet_client::forwarder::MixForwardingSender;
|
||||
use nym_mixnode_common::packet_processor::processor::ProcessedFinalHop;
|
||||
use nym_noise::upgrade_noise_responder;
|
||||
use nym_noise::upgrade_noise_responder_with_topology;
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
use nym_sphinx::framing::codec::NymCodec;
|
||||
use nym_sphinx::framing::packet::FramedNymPacket;
|
||||
@@ -21,6 +20,7 @@ use nym_sphinx::DestinationAddressBytes;
|
||||
use nym_task::TaskClient;
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_util::codec::Framed;
|
||||
|
||||
@@ -36,8 +36,7 @@ pub(crate) struct ConnectionHandler<St: Storage> {
|
||||
storage: St,
|
||||
ack_sender: MixForwardingSender,
|
||||
topology_access: TopologyAccessor,
|
||||
public_identity_key: [u8; PUBLIC_KEY_SIZE],
|
||||
private_identity_key: [u8; PRIVATE_KEY_SIZE],
|
||||
local_identity: Arc<encryption::KeyPair>,
|
||||
}
|
||||
|
||||
impl<St: Storage + Clone> Clone for ConnectionHandler<St> {
|
||||
@@ -57,8 +56,7 @@ impl<St: Storage + Clone> Clone for ConnectionHandler<St> {
|
||||
storage: self.storage.clone(),
|
||||
ack_sender: self.ack_sender.clone(),
|
||||
topology_access: self.topology_access.clone(),
|
||||
public_identity_key: self.public_identity_key.clone(),
|
||||
private_identity_key: self.private_identity_key.clone(),
|
||||
local_identity: self.local_identity.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +68,7 @@ impl<St: Storage> ConnectionHandler<St> {
|
||||
ack_sender: MixForwardingSender,
|
||||
active_clients_store: ActiveClientsStore,
|
||||
topology_access: TopologyAccessor,
|
||||
identity_key: &encryption::KeyPair,
|
||||
local_identity: Arc<encryption::KeyPair>,
|
||||
) -> Self {
|
||||
ConnectionHandler {
|
||||
packet_processor,
|
||||
@@ -79,8 +77,7 @@ impl<St: Storage> ConnectionHandler<St> {
|
||||
active_clients_store,
|
||||
ack_sender,
|
||||
topology_access,
|
||||
public_identity_key: identity_key.public_key().to_bytes(),
|
||||
private_identity_key: identity_key.private_key().to_bytes(),
|
||||
local_identity,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,11 +205,11 @@ impl<St: Storage> ConnectionHandler<St> {
|
||||
}
|
||||
};
|
||||
|
||||
let noise_stream = match upgrade_noise_responder(
|
||||
let noise_stream = match upgrade_noise_responder_with_topology(
|
||||
conn,
|
||||
topology_ref,
|
||||
&self.public_identity_key,
|
||||
&self.private_identity_key,
|
||||
&self.local_identity.public_key().to_bytes(),
|
||||
&self.local_identity.private_key().to_bytes(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -146,7 +146,7 @@ impl<St> Gateway<St> {
|
||||
ack_sender,
|
||||
active_clients_store,
|
||||
topology_access,
|
||||
&self.sphinx_keypair,
|
||||
Arc::clone(&self.sphinx_keypair),
|
||||
);
|
||||
|
||||
let listening_address = SocketAddr::new(
|
||||
@@ -176,6 +176,7 @@ impl<St> Gateway<St> {
|
||||
websocket::Listener::new(
|
||||
listening_address,
|
||||
Arc::clone(&self.identity_keypair),
|
||||
Arc::clone(&self.sphinx_keypair),
|
||||
self.config.gateway.only_coconut_credentials,
|
||||
coconut_verifier,
|
||||
)
|
||||
@@ -201,7 +202,7 @@ impl<St> Gateway<St> {
|
||||
self.config.debug.maximum_connection_buffer_size,
|
||||
self.config.debug.use_legacy_framed_packet_version,
|
||||
topology_access,
|
||||
&self.sphinx_keypair,
|
||||
Arc::clone(&self.sphinx_keypair),
|
||||
shutdown,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::node::encryption::{PRIVATE_KEY_SIZE, PUBLIC_KEY_SIZE};
|
||||
use crate::node::listener::connection_handler::packet_processing::{
|
||||
MixProcessingResult, PacketProcessor,
|
||||
};
|
||||
@@ -11,12 +10,13 @@ use futures::StreamExt;
|
||||
use nym_client_core::client::topology_control::accessor::TopologyAccessor;
|
||||
use nym_crypto::asymmetric::encryption;
|
||||
use nym_mixnode_common::measure;
|
||||
use nym_noise::upgrade_noise_responder;
|
||||
use nym_noise::upgrade_noise_responder_with_topology;
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
use nym_sphinx::framing::codec::NymCodec;
|
||||
use nym_sphinx::framing::packet::FramedNymPacket;
|
||||
use nym_sphinx::Delay as SphinxDelay;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::Instant;
|
||||
use tokio_util::codec::Framed;
|
||||
@@ -30,8 +30,7 @@ pub(crate) struct ConnectionHandler {
|
||||
packet_processor: PacketProcessor,
|
||||
delay_forwarding_channel: PacketDelayForwardSender,
|
||||
topology_access: TopologyAccessor,
|
||||
private_identity_key: [u8; PRIVATE_KEY_SIZE],
|
||||
public_identity_key: [u8; PUBLIC_KEY_SIZE],
|
||||
local_identity: Arc<encryption::KeyPair>,
|
||||
}
|
||||
|
||||
impl ConnectionHandler {
|
||||
@@ -39,14 +38,13 @@ impl ConnectionHandler {
|
||||
packet_processor: PacketProcessor,
|
||||
delay_forwarding_channel: PacketDelayForwardSender,
|
||||
topology_access: TopologyAccessor,
|
||||
identity_key: &encryption::KeyPair,
|
||||
local_identity: Arc<encryption::KeyPair>,
|
||||
) -> Self {
|
||||
ConnectionHandler {
|
||||
packet_processor,
|
||||
delay_forwarding_channel,
|
||||
topology_access,
|
||||
public_identity_key: identity_key.public_key().to_bytes(),
|
||||
private_identity_key: identity_key.private_key().to_bytes(),
|
||||
local_identity,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,11 +107,11 @@ impl ConnectionHandler {
|
||||
}
|
||||
};
|
||||
|
||||
let noise_stream = match upgrade_noise_responder(
|
||||
let noise_stream = match upgrade_noise_responder_with_topology(
|
||||
conn,
|
||||
topology_ref,
|
||||
&self.public_identity_key,
|
||||
&self.private_identity_key,
|
||||
&self.local_identity.public_key().to_bytes(),
|
||||
&self.local_identity.private_key().to_bytes(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -164,7 +164,7 @@ impl MixNode {
|
||||
packet_processor,
|
||||
delay_forwarding_channel,
|
||||
topology_access,
|
||||
&self.sphinx_keypair,
|
||||
Arc::clone(&self.sphinx_keypair),
|
||||
);
|
||||
|
||||
let listening_address = SocketAddr::new(
|
||||
@@ -192,7 +192,11 @@ impl MixNode {
|
||||
);
|
||||
|
||||
let mut packet_forwarder = DelayForwarder::new(
|
||||
nym_mixnet_client::Client::new(client_config, topology_access, &self.sphinx_keypair),
|
||||
nym_mixnet_client::Client::new(
|
||||
client_config,
|
||||
topology_access,
|
||||
Arc::clone(&self.sphinx_keypair),
|
||||
),
|
||||
node_stats_update_sender,
|
||||
shutdown,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user