nym-nr-query CLI tool for query network-requester (#3538)

* Initial version

* Add open_proxy request

* Start adding cli commands

* use commands

* Common response type

* json output

* Tidy

* Remove the All case

* Remove empty file

* Remove todo

* Add ping command

* Ping 4 times by default

* Logic for controlling number of loops

* Some print tweaks
This commit is contained in:
Jon Häggblad
2023-06-12 09:30:02 +02:00
committed by GitHub
parent 88002969b7
commit 0fa1961a04
11 changed files with 391 additions and 15 deletions
Generated
+16
View File
@@ -4150,6 +4150,22 @@ dependencies = [
"wasm-timer",
]
[[package]]
name = "nym-nr-query"
version = "0.1.0"
dependencies = [
"anyhow",
"clap 4.2.7",
"log",
"nym-bin-common",
"nym-network-defaults",
"nym-sdk",
"nym-service-providers-common",
"nym-socks5-requests",
"serde",
"tokio",
]
[[package]]
name = "nym-ordered-buffer"
version = "0.1.0"
+1
View File
@@ -87,6 +87,7 @@ members = [
"nym-api/nym-api-requests",
"nym-outfox",
"tools/nym-cli",
"tools/nym-nr-query",
"tools/ts-rs-cli"
]
@@ -101,7 +101,7 @@ impl BinaryBuildInformation {
}
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BinaryBuildInformationOwned {
// VERGEN_BUILD_TIMESTAMP
/// Provides the build timestamp, for example `2021-02-23T20:14:46.558472672+00:00`.
+7
View File
@@ -233,6 +233,13 @@ impl Socks5ResponseContent {
}
}
}
pub fn as_query(&self) -> Option<&QueryResponse> {
match self {
Socks5ResponseContent::Query(query) => Some(query),
_ => None,
}
}
}
/// A remote network network data response retrieved by the Socks5 service provider. This
+4 -1
View File
@@ -63,7 +63,10 @@ pub use nym_credential_storage::{
pub use nym_network_defaults::NymNetworkDetails;
pub use nym_socks5_client_core::config::Socks5;
pub use nym_sphinx::{
addressing::clients::{ClientIdentity, Recipient},
addressing::{
clients::{ClientIdentity, Recipient},
nodes::NodeIdentity,
},
receiver::ReconstructedMessage,
};
pub use nym_topology::{provider_trait::TopologyProvider, NymTopology};
@@ -48,7 +48,7 @@ async fn main() -> anyhow::Result<()> {
let full_request_versions: Request =
Request::new_control(ProviderInterfaceVersion::new_current(), request_versions);
// // TODO: currently we HAVE TO use surbs unfortunately
// TODO: currently we HAVE TO use surbs unfortunately
println!("Sending 'Health' request...");
client
.send_bytes(
@@ -82,5 +82,6 @@ async fn main() -> anyhow::Result<()> {
let response = wait_for_control_response(&mut client).await;
println!("response to 'SupportedRequestVersions' request: {response:#?}");
client.disconnect().await;
Ok(())
}
@@ -69,13 +69,13 @@ impl ControlRequest {
}
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BinaryInformation {
pub binary_name: String,
pub build_information: BinaryBuildInformationOwned,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SupportedVersions {
pub interface_version: String,
pub provider_version: String,
@@ -86,7 +86,7 @@ pub struct ErrorResponse {
message: String,
}
#[derive(Debug)]
#[derive(Debug, Serialize)]
pub enum ControlResponse {
Health,
BinaryInfo(Box<BinaryInformation>),
@@ -35,22 +35,22 @@ url = { workspace = true }
# internal
async-file-watcher = { path = "../../common/async-file-watcher" }
nym-bin-common = { path = "../../common/bin-common", features = ["output_format"] }
nym-client-core = { path = "../../common/client-core" }
nym-client-websocket-requests = { path = "../../clients/native/websocket-requests" }
nym-config = { path = "../../common/config" }
nym-credential-storage = { path = "../../common/credential-storage" }
nym-crypto = { path = "../../common/crypto" }
nym-bin-common = { path = "../../common/bin-common", features = ["output_format"] }
nym-network-defaults = { path = "../../common/network-defaults" }
nym-sdk = { path = "../../sdk/rust/nym-sdk" }
nym-sphinx = { path = "../../common/nymsphinx" }
nym-ordered-buffer = {path = "../../common/socks5/ordered-buffer"}
nym-socks5-proxy-helpers = { path = "../../common/socks5/proxy-helpers" }
nym-sdk = { path = "../../sdk/rust/nym-sdk" }
nym-service-providers-common = { path = "../common" }
nym-socks5-proxy-helpers = { path = "../../common/socks5/proxy-helpers" }
nym-socks5-requests = { path = "../../common/socks5/requests" }
nym-sphinx = { path = "../../common/nymsphinx" }
nym-statistics-common = { path = "../../common/statistics" }
nym-task = { path = "../../common/task" }
nym-types = { path = "../../common/types" }
nym-client-websocket-requests = { path = "../../clients/native/websocket-requests" }
[dev-dependencies]
tempfile = "3.5.0"
@@ -5,7 +5,7 @@ use nym_sdk::mixnet::{IncludedSurbs, MixnetClient, Recipient, ReconstructedMessa
use nym_service_providers_common::interface::{
ProviderInterfaceVersion, Request, Response, ResponseContent,
};
use nym_socks5_requests::{QueryRequest, Socks5Request, Socks5Response};
use nym_socks5_requests::{QueryRequest, Socks5ProtocolVersion, Socks5Request, Socks5Response};
fn parse_response(received: Vec<ReconstructedMessage>) -> Socks5Response {
assert_eq!(received.len(), 1);
@@ -31,8 +31,6 @@ async fn main() -> anyhow::Result<()> {
let mut client = MixnetClient::connect_new().await.unwrap();
let provider: Recipient = "AN8eLxYWFitCkMn92zim3PrPszxJZDYyFFKP7qnnAAew.8UAxL3LwQBis6WpM3GGXaqKGaVdnLCpGJWumHT6KNdTH@77TSuVU8d1oXKbPzjec2xh4i3Wj5WwUyy9Lr36sm8gZm".parse().unwrap();
use nym_socks5_requests::Socks5ProtocolVersion;
let open_proxy_request = Request::new_provider_data(
ProviderInterfaceVersion::new_current(),
Socks5Request::new_query(
@@ -52,7 +50,7 @@ async fn main() -> anyhow::Result<()> {
.send_bytes(
provider,
open_proxy_request.into_bytes(),
IncludedSurbs::new(10), //crashes??
IncludedSurbs::new(10),
)
.await;
let response = wait_for_response(&mut client).await;
@@ -69,5 +67,6 @@ async fn main() -> anyhow::Result<()> {
let response = wait_for_response(&mut client).await;
println!("response to 'Description' query: {response:#?}");
client.disconnect().await;
Ok(())
}
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "nym-nr-query"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.68"
clap = {version = "4.0", features = ["cargo", "derive"]}
log = { workspace = true }
nym-bin-common = { path = "../../common/bin-common", features = ["output_format"] }
nym-network-defaults = { path = "../../common/network-defaults" }
nym-sdk = { path = "../../sdk/rust/nym-sdk" }
nym-service-providers-common = { path = "../../service-providers/common" }
nym-socks5-requests = { path = "../../common/socks5/requests" }
serde = { workspace = true }
tokio = { workspace = true, features = [ "net", "rt-multi-thread", "macros" ] }
+331
View File
@@ -0,0 +1,331 @@
use std::fmt;
use clap::{Parser, ValueEnum};
use nym_bin_common::output_format::OutputFormat;
use nym_sdk::mixnet::{self, IncludedSurbs};
use nym_service_providers_common::interface::{
ControlRequest, ControlResponse, ProviderInterfaceVersion, Request, Response, ResponseContent,
};
use nym_socks5_requests::{
QueryRequest, QueryResponse, Socks5ProtocolVersion, Socks5Request, Socks5Response,
};
use serde::Serialize;
use tokio::time::{timeout, Duration};
const RESPONSE_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
#[arg(short, long)]
config_env_file: Option<std::path::PathBuf>,
#[arg(short, long)]
debug: bool,
#[arg(short, long)]
provider: mixnet::Recipient,
#[arg(short, long)]
gateway: Option<mixnet::NodeIdentity>,
#[arg(short, long, default_value_t = OutputFormat::default())]
output: OutputFormat,
#[arg(value_enum, default_value_t = Commands::Ping)]
command: Commands,
/// By default, pinging is done continuously until manually stopped. This flag
/// specifies how many pings should be sent before stopping.
#[arg(short = 'n', long)]
ping_count: Option<usize>,
}
#[derive(Clone, ValueEnum, PartialEq, Eq)]
enum Commands {
/// Binary information
BinaryInfo,
/// Supported request versions
SupportedRequestVersions,
/// Check if the network requester is acting a an open proxy
OpenProxy,
/// Ping the network requester
Ping,
}
fn parse_control_response(received: Vec<mixnet::ReconstructedMessage>) -> ControlResponse {
assert_eq!(received.len(), 1);
let response: Response = Response::try_from_bytes(&received[0].message).unwrap();
match response.content {
ResponseContent::Control(control) => control,
ResponseContent::ProviderData(_) => {
panic!("received provider data even though we sent control request!")
}
}
}
fn parse_socks5_response(received: Vec<mixnet::ReconstructedMessage>) -> Socks5Response {
assert_eq!(received.len(), 1);
let response: Response<Socks5Request> = Response::try_from_bytes(&received[0].message).unwrap();
match response.content {
ResponseContent::Control(control) => panic!("unexpected control response: {:?}", control),
ResponseContent::ProviderData(data) => data,
}
}
async fn wait_for_control_response(client: &mut mixnet::MixnetClient) -> ControlResponse {
loop {
let Ok(next) = timeout(RESPONSE_TIMEOUT, client.wait_for_messages()).await else {
eprintln!("Timeout waiting for response");
// Currently the control messages are not tagged with ID, so if we loose one we might
// en up out of sync. For now, just exit.
std::process::exit(1);
};
let next = next.unwrap();
if !next.is_empty() {
return parse_control_response(next);
}
}
}
async fn wait_for_socks5_response(client: &mut mixnet::MixnetClient) -> Socks5Response {
loop {
let Ok(next) = timeout(RESPONSE_TIMEOUT, client.wait_for_messages()).await else {
eprintln!("Timeout waiting for response");
std::process::exit(1);
};
let next = next.unwrap();
if !next.is_empty() {
return parse_socks5_response(next);
}
}
}
async fn connect_to_mixnet(gateway: Option<mixnet::NodeIdentity>) -> mixnet::MixnetClient {
match gateway {
Some(gateway) => mixnet::MixnetClientBuilder::new_ephemeral()
.request_gateway(gateway.to_base58_string())
.build()
.await
.expect("Failed to create mixnet client")
.connect_to_mixnet()
.await
.expect("Failed to connect to the mixnet"),
None => mixnet::MixnetClient::connect_new().await.unwrap(),
}
}
fn new_bin_info_request() -> Request {
let request_binary_info = ControlRequest::BinaryInfo;
Request::new_control(ProviderInterfaceVersion::new_current(), request_binary_info)
}
fn new_supported_request_versions_request() -> Request {
let request_versions = ControlRequest::SupportedRequestVersions;
Request::new_control(ProviderInterfaceVersion::new_current(), request_versions)
}
fn new_open_proxy_request() -> Request<Socks5Request> {
let request_open_proxy = Socks5Request::new_query(
Socks5ProtocolVersion::new_current(),
QueryRequest::OpenProxy,
);
Request::new_provider_data(ProviderInterfaceVersion::new_current(), request_open_proxy)
}
fn new_ping_request() -> Request {
let request_ping = ControlRequest::Health;
Request::new_control(ProviderInterfaceVersion::new_current(), request_ping)
}
struct QueryClient {
pub client: mixnet::MixnetClient,
pub provider: mixnet::Recipient,
}
impl QueryClient {
async fn new(provider: mixnet::Recipient, gateway: Option<mixnet::NodeIdentity>) -> Self {
let client = connect_to_mixnet(gateway).await;
Self { client, provider }
}
async fn query_bin_info(&mut self) -> ControlResponse {
self.client
.send_bytes(
self.provider,
new_bin_info_request().into_bytes(),
IncludedSurbs::new(10),
)
.await;
wait_for_control_response(&mut self.client).await
}
async fn query_supported_versions(&mut self) -> ControlResponse {
self.client
.send_bytes(
self.provider,
new_supported_request_versions_request().into_bytes(),
IncludedSurbs::new(10),
)
.await;
wait_for_control_response(&mut self.client).await
}
async fn query_open_proxy(&mut self) -> QueryResponse {
self.client
.send_bytes(
self.provider,
new_open_proxy_request().into_bytes(),
IncludedSurbs::new(10),
)
.await;
let response = wait_for_socks5_response(&mut self.client).await;
response
.content
.as_query()
.expect("Unexpected response type!")
.clone()
}
async fn ping(&mut self) -> PingResponse {
let now = std::time::Instant::now();
self.client
.send_bytes(
self.provider,
new_ping_request().into_bytes(),
IncludedSurbs::new(10),
)
.await;
let response = wait_for_control_response(&mut self.client).await;
assert!(matches!(response, ControlResponse::Health));
let elapsed = now.elapsed();
PingResponse {
provider: self.provider.to_string(),
ping_ms: elapsed.as_millis(),
}
}
}
#[derive(Debug, Serialize)]
struct PingResponse {
provider: String,
ping_ms: u128,
}
impl fmt::Display for PingResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: (surb) time={} ms", self.provider, self.ping_ms)
}
}
#[derive(Debug, Serialize)]
enum ClientResponse {
Control(ControlResponse),
Query(QueryResponse),
Ping(PingResponse),
}
impl fmt::Display for ClientResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ClientResponse::Control(control) => write!(f, "{:#?}", control),
ClientResponse::Query(query) => write!(f, "{:#?}", query),
ClientResponse::Ping(ping) => write!(f, "{}", ping),
}
}
}
impl From<ControlResponse> for ClientResponse {
fn from(response: ControlResponse) -> Self {
ClientResponse::Control(response)
}
}
impl From<QueryResponse> for ClientResponse {
fn from(response: QueryResponse) -> Self {
ClientResponse::Query(response)
}
}
impl From<PingResponse> for ClientResponse {
fn from(response: PingResponse) -> Self {
ClientResponse::Ping(response)
}
}
fn text_println(input: &str, output: &OutputFormat) {
if output.is_text() {
println!("{input}");
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = Cli::parse();
if args.debug {
nym_bin_common::logging::setup_logging();
}
nym_network_defaults::setup_env(args.config_env_file.as_ref());
text_println("Registering with gateway...", &args.output);
let mut client = QueryClient::new(args.provider, args.gateway).await;
let our_gateway = client.client.nym_address().gateway();
text_println(&format!(" gateway: {our_gateway}"), &args.output);
text_println("Sending request(s)...", &args.output);
if args.command == Commands::Ping {
let mut count = 0;
loop {
tokio::select! {
resp = client.ping() => {
println!("{}", args.output.format(&resp));
}
_ = tokio::signal::ctrl_c() => {
println!("Ctrl-C received, exiting...");
break;
}
}
// Normally we loop until the use stops, but we can also specify a ping count.
count += 1;
if let Some(ping_count) = args.ping_count {
if count >= ping_count {
break;
}
}
// If we specified json output, just run once, since this is likely to be called from a
// script.
if !args.output.is_text() {
break;
}
}
} else {
let resp: ClientResponse = match args.command {
Commands::BinaryInfo => client.query_bin_info().await.into(),
Commands::SupportedRequestVersions => client.query_supported_versions().await.into(),
Commands::OpenProxy => client.query_open_proxy().await.into(),
Commands::Ping => unreachable!(),
};
println!("{}", args.output.format(&resp));
}
text_println("Disconnecting...", &args.output);
client.client.disconnect().await;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn verify_cli() {
Cli::command().debug_assert();
}
}