Merge pull request #6694 from nymtech/bdq/port-test-agent
Testing port checks in NS Agents
This commit is contained in:
Generated
+2
-2
@@ -7870,7 +7870,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-status-agent"
|
||||
version = "2.0.1-rc3"
|
||||
version = "2.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -7891,7 +7891,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-status-api"
|
||||
version = "4.6.2-rc10"
|
||||
version = "4.6.2"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"anyhow",
|
||||
|
||||
@@ -17,7 +17,7 @@ anyhow = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
bs58 = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
clap = { workspace = true, features = ["cargo", "derive"] }
|
||||
clap = { workspace = true, features = ["cargo", "derive", "env"] }
|
||||
futures = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
@@ -9,6 +9,7 @@ use vergen_gitcl::{BuildBuilder, CargoBuilder, Emitter, GitclBuilder, RustcBuild
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
build_go()?;
|
||||
generate_exit_policy_ports()?;
|
||||
|
||||
Emitter::default()
|
||||
.add_instructions(&BuildBuilder::all_build()?)?
|
||||
@@ -18,6 +19,99 @@ fn main() -> anyhow::Result<()> {
|
||||
.emit()
|
||||
}
|
||||
|
||||
/// Parse PORT_MAPPINGS from network-tunnel-manager.sh and generate a sorted
|
||||
/// Rust const with every unique port. Ranges are represented by their start
|
||||
/// and end values so a single TCP check can confirm the iptables rule exists.
|
||||
/// TODO: consider runtime fetch from NS API exit policy endpoint instead of parsing the script
|
||||
fn generate_exit_policy_ports() -> anyhow::Result<()> {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
let manifest_dir =
|
||||
PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").context("CARGO_MANIFEST_DIR not set")?);
|
||||
let script_path = manifest_dir.join("../scripts/nym-node-setup/network-tunnel-manager.sh");
|
||||
let out_dir = PathBuf::from(std::env::var("OUT_DIR").context("OUT_DIR not set")?);
|
||||
|
||||
println!("cargo::rerun-if-changed={}", script_path.display());
|
||||
|
||||
let content = std::fs::read_to_string(&script_path).context(
|
||||
"failed to read network-tunnel-manager.sh — is it present at ../scripts/nym-node-setup/ ?",
|
||||
)?;
|
||||
|
||||
// port → service name (BTreeMap keeps them sorted)
|
||||
let mut port_map: BTreeMap<u16, String> = BTreeMap::new();
|
||||
let mut in_mappings = false;
|
||||
|
||||
for line in content.lines() {
|
||||
let trimmed = line.trim();
|
||||
|
||||
if trimmed.starts_with("declare -A PORT_MAPPINGS=(") {
|
||||
in_mappings = true;
|
||||
continue;
|
||||
}
|
||||
if in_mappings && trimmed == ")" {
|
||||
break;
|
||||
}
|
||||
if !in_mappings {
|
||||
continue;
|
||||
}
|
||||
|
||||
// strip comment prefix so we still pick up ports that are opened
|
||||
// via a separate mechanism (e.g. SMTP/465 with rate limiting)
|
||||
let stripped = trimmed.trim_start_matches('#').trim();
|
||||
|
||||
// match ["ServiceName"]="port-or-range"
|
||||
let Some(name_start) = stripped.find("[\"") else {
|
||||
continue;
|
||||
};
|
||||
let Some(name_end) = stripped.find("\"]=") else {
|
||||
continue;
|
||||
};
|
||||
let service = &stripped[name_start + 2..name_end];
|
||||
let value = stripped[name_end + 3..].trim_matches('"');
|
||||
|
||||
if value.contains('-') {
|
||||
let parts: Vec<&str> = value.split('-').collect();
|
||||
if parts.len() == 2
|
||||
&& let (Ok(lo), Ok(hi)) = (parts[0].parse::<u16>(), parts[1].parse::<u16>())
|
||||
{
|
||||
port_map
|
||||
.entry(lo)
|
||||
.or_insert_with(|| format!("{service} (range start)"));
|
||||
port_map
|
||||
.entry(hi)
|
||||
.or_insert_with(|| format!("{service} (range end)"));
|
||||
}
|
||||
} else if let Ok(port) = value.parse::<u16>() {
|
||||
port_map.entry(port).or_insert_with(|| service.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if port_map.is_empty() {
|
||||
bail!("No ports found in PORT_MAPPINGS — is network-tunnel-manager.sh correct?");
|
||||
}
|
||||
|
||||
// write generated Rust source
|
||||
let mut out = String::new();
|
||||
out.push_str(
|
||||
"// Auto-generated from scripts/nym-node-setup/network-tunnel-manager.sh PORT_MAPPINGS.\n",
|
||||
);
|
||||
out.push_str("// Do not edit — changes are overwritten on rebuild.\n");
|
||||
out.push_str("// To add or remove ports, update PORT_MAPPINGS in the shell script.\n\n");
|
||||
out.push_str(&format!(
|
||||
"/// {} unique ports parsed from the canonical exit policy at build time.\n",
|
||||
port_map.len()
|
||||
));
|
||||
out.push_str("pub const EXIT_POLICY_PORTS: &[u16] = &[\n");
|
||||
for (port, service) in &port_map {
|
||||
let entry = format!("{port},");
|
||||
out.push_str(&format!(" {entry:<7}// {service}\n"));
|
||||
}
|
||||
out.push_str("];\n");
|
||||
|
||||
std::fs::write(out_dir.join("exit_policy_ports.rs"), out)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_go() -> anyhow::Result<()> {
|
||||
const LIB_NAME: &str = "netstack_ping";
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
netUrl "net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
@@ -62,21 +63,27 @@ type NetstackRequestGo struct {
|
||||
DownloadTimeoutSec uint64 `json:"download_timeout_sec"`
|
||||
MetadataTimeoutSec uint64 `json:"metadata_timeout_sec"`
|
||||
AwgArgs string `json:"awg_args"`
|
||||
// exit policy port check
|
||||
PortCheckTarget string `json:"port_check_target"`
|
||||
PortCheckPorts []uint16 `json:"port_check_ports"`
|
||||
PortCheckOnly bool `json:"port_check_only"`
|
||||
PortCheckTimeoutSec uint64 `json:"port_check_timeout_sec"`
|
||||
}
|
||||
|
||||
type NetstackResponse struct {
|
||||
CanHandshake bool `json:"can_handshake"`
|
||||
CanQueryMetadata bool `json:"can_query_metadata"`
|
||||
SentIps uint16 `json:"sent_ips"`
|
||||
ReceivedIps uint16 `json:"received_ips"`
|
||||
SentHosts uint16 `json:"sent_hosts"`
|
||||
ReceivedHosts uint16 `json:"received_hosts"`
|
||||
CanResolveDns bool `json:"can_resolve_dns"`
|
||||
DownloadedFile string `json:"downloaded_file"`
|
||||
DownloadedFileSizeBytes uint64 `json:"downloaded_file_size_bytes"`
|
||||
DownloadDurationSec uint64 `json:"download_duration_sec"`
|
||||
DownloadDurationMilliseconds uint64 `json:"download_duration_milliseconds"`
|
||||
DownloadError string `json:"download_error"`
|
||||
CanHandshake bool `json:"can_handshake"`
|
||||
CanQueryMetadata bool `json:"can_query_metadata"`
|
||||
SentIps uint16 `json:"sent_ips"`
|
||||
ReceivedIps uint16 `json:"received_ips"`
|
||||
SentHosts uint16 `json:"sent_hosts"`
|
||||
ReceivedHosts uint16 `json:"received_hosts"`
|
||||
CanResolveDns bool `json:"can_resolve_dns"`
|
||||
DownloadedFile string `json:"downloaded_file"`
|
||||
DownloadedFileSizeBytes uint64 `json:"downloaded_file_size_bytes"`
|
||||
DownloadDurationSec uint64 `json:"download_duration_sec"`
|
||||
DownloadDurationMilliseconds uint64 `json:"download_duration_milliseconds"`
|
||||
DownloadError string `json:"download_error"`
|
||||
PortCheckResults map[string]bool `json:"port_check_results,omitempty"`
|
||||
}
|
||||
|
||||
type SuccessResult = struct {
|
||||
@@ -201,7 +208,7 @@ func pingTwoHop(req TwoHopNetstackRequest) (NetstackResponse, error) {
|
||||
log.Printf("Exit WG IP: %s", req.ExitWgIp)
|
||||
log.Printf("IP version: %d", req.IpVersion)
|
||||
|
||||
response := NetstackResponse{false, false, 0, 0, 0, 0, false, "", 0, 0, 0, ""}
|
||||
response := NetstackResponse{}
|
||||
|
||||
// Parse the exit endpoint to determine IP version for forwarder
|
||||
exitEndpoint, err := netip.ParseAddrPort(req.ExitEndpoint)
|
||||
@@ -439,7 +446,7 @@ func ping(req NetstackRequestGo) (NetstackResponse, error) {
|
||||
ipc.WriteString("\nallowed_ip=::/0\n")
|
||||
}
|
||||
|
||||
response := NetstackResponse{false, false, 0, 0, 0, 0, false, "", 0, 0, 0, ""}
|
||||
response := NetstackResponse{}
|
||||
|
||||
err = dev.IpcSet(ipc.String())
|
||||
if err != nil {
|
||||
@@ -463,6 +470,19 @@ func ping(req NetstackRequestGo) (NetstackResponse, error) {
|
||||
|
||||
response.CanHandshake = true
|
||||
|
||||
// port-check-only mode: skip pings/download, only test TCP port reachability
|
||||
if req.PortCheckOnly && len(req.PortCheckPorts) > 0 {
|
||||
log.Printf("=== Port Check Only Mode ===")
|
||||
response.PortCheckResults = checkPorts(req.PortCheckTarget, req.PortCheckPorts, req.PortCheckTimeoutSec, tnet)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// run port checks alongside normal tests if ports were requested
|
||||
if len(req.PortCheckPorts) > 0 {
|
||||
log.Printf("=== Running Port Checks (alongside normal tests) ===")
|
||||
response.PortCheckResults = checkPorts(req.PortCheckTarget, req.PortCheckPorts, req.PortCheckTimeoutSec, tnet)
|
||||
}
|
||||
|
||||
// Skip metadata query if endpoint is empty (e.g., for IPv6 where the IPv4 metadata endpoint is not reachable)
|
||||
if req.MetadataEndpoint != "" {
|
||||
version, duration, err := queryMetadata(req.MetadataEndpoint, req.MetadataTimeoutSec, tnet)
|
||||
@@ -569,6 +589,126 @@ func ping(req NetstackRequestGo) (NetstackResponse, error) {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
const portquizBatchSize = 20
|
||||
const portquizBatchDelay = 25 * time.Second
|
||||
const portquizDialGap = 20 * time.Millisecond
|
||||
|
||||
func checkPorts(target string, ports []uint16, timeoutSec uint64, tnet *netstack.Net) map[string]bool {
|
||||
if target == "" {
|
||||
target = "portquiz.net"
|
||||
}
|
||||
if timeoutSec == 0 {
|
||||
timeoutSec = 5
|
||||
}
|
||||
|
||||
targetIP := target
|
||||
if net.ParseIP(target) == nil {
|
||||
addrs, err := net.LookupHost(target)
|
||||
if err != nil || len(addrs) == 0 {
|
||||
log.Printf("Port check: DNS lookup for %s failed (%v), using hostname as-is", target, err)
|
||||
} else {
|
||||
chosen := addrs[0]
|
||||
for _, a := range addrs {
|
||||
if net.ParseIP(a).To4() != nil {
|
||||
chosen = a
|
||||
break
|
||||
}
|
||||
}
|
||||
targetIP = chosen
|
||||
log.Printf("Port check: resolved %s -> %s", target, targetIP)
|
||||
}
|
||||
}
|
||||
|
||||
timeout := time.Duration(timeoutSec) * time.Second
|
||||
results := make(map[string]bool, len(ports))
|
||||
|
||||
if strings.Contains(target, "portquiz.net") {
|
||||
// portquiz.net rate-limits after ~29 connections per window; use large batches with a long cooldown
|
||||
log.Printf("Port check: testing %d ports on %s in batches of %d with %v cooldown (timeout %v each)",
|
||||
len(ports), target, portquizBatchSize, portquizBatchDelay, timeout)
|
||||
|
||||
for batchIdx := 0; batchIdx < len(ports); batchIdx += portquizBatchSize {
|
||||
end := batchIdx + portquizBatchSize
|
||||
if end > len(ports) {
|
||||
end = len(ports)
|
||||
}
|
||||
batch := ports[batchIdx:end]
|
||||
|
||||
for i, p := range batch {
|
||||
addr := net.JoinHostPort(targetIP, fmt.Sprintf("%d", p))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
c, err := tnet.DialContext(ctx, "tcp", addr)
|
||||
cancel()
|
||||
key := fmt.Sprintf("%d", p)
|
||||
if err != nil {
|
||||
log.Printf("Port %d: CLOSED (%v)", p, err)
|
||||
results[key] = false
|
||||
} else {
|
||||
c.Close()
|
||||
log.Printf("Port %d: OPEN", p)
|
||||
results[key] = true
|
||||
}
|
||||
if i < len(batch)-1 {
|
||||
time.Sleep(portquizDialGap)
|
||||
}
|
||||
}
|
||||
|
||||
if batchIdx+portquizBatchSize < len(ports) {
|
||||
time.Sleep(portquizBatchDelay)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// All other targets can handle concurrent connections, probably.
|
||||
// A semaphore caps concurrent tnet.DialContext calls to avoid
|
||||
// overwhelming the single userspace netstack instance.
|
||||
const maxConcurrentDials = 64
|
||||
log.Printf("Port check: testing %d ports on %s concurrently (max %d at a time, timeout %v each)",
|
||||
len(ports), target, maxConcurrentDials, timeout)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
sem = make(chan struct{}, maxConcurrentDials)
|
||||
)
|
||||
for _, p := range ports {
|
||||
wg.Add(1)
|
||||
go func(port uint16) {
|
||||
defer wg.Done()
|
||||
sem <- struct{}{}
|
||||
defer func() { <-sem }()
|
||||
addr := net.JoinHostPort(targetIP, fmt.Sprintf("%d", port))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
c, err := tnet.DialContext(ctx, "tcp", addr)
|
||||
cancel()
|
||||
key := fmt.Sprintf("%d", port)
|
||||
if err != nil {
|
||||
log.Printf("Port %d: CLOSED (%v)", port, err)
|
||||
mu.Lock()
|
||||
results[key] = false
|
||||
mu.Unlock()
|
||||
} else {
|
||||
c.Close()
|
||||
log.Printf("Port %d: OPEN", port)
|
||||
mu.Lock()
|
||||
results[key] = true
|
||||
mu.Unlock()
|
||||
}
|
||||
}(p)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
openCount := 0
|
||||
for _, open := range results {
|
||||
if open {
|
||||
openCount++
|
||||
}
|
||||
}
|
||||
log.Printf("Port check complete: %d/%d ports open", openCount, len(ports))
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func sendPing(address string, seq uint8, sendTtimeoutSecs uint64, receiveTimoutSecs uint64, tnet *netstack.Net, ipVersion uint8) (time.Duration, error) {
|
||||
maxPingRetries := 2
|
||||
baseTimeout := receiveTimoutSecs
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use crate::config::NetstackArgs;
|
||||
use anyhow::Context;
|
||||
use serde::Deserialize;
|
||||
use std::collections::BTreeMap;
|
||||
use std::ffi::{CStr, CString};
|
||||
|
||||
mod sys {
|
||||
@@ -15,6 +16,19 @@ mod sys {
|
||||
}
|
||||
}
|
||||
|
||||
/// Port-check fields shared between `NetstackRequest` and `NetstackRequestGo`
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PortCheckConfig {
|
||||
#[serde(rename = "port_check_target")]
|
||||
pub target: String,
|
||||
#[serde(rename = "port_check_ports")]
|
||||
pub ports: Vec<u16>,
|
||||
#[serde(rename = "port_check_only")]
|
||||
pub only: bool,
|
||||
#[serde(rename = "port_check_timeout_sec")]
|
||||
pub timeout_sec: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct NetstackRequest {
|
||||
private_key: String,
|
||||
@@ -25,6 +39,8 @@ pub struct NetstackRequest {
|
||||
v6_ping_config: PingConfig,
|
||||
download_timeout_sec: u64,
|
||||
awg_args: String,
|
||||
#[serde(flatten)]
|
||||
pub port_check: PortCheckConfig,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
@@ -76,6 +92,7 @@ impl NetstackRequest {
|
||||
download_timeout_sec: u64,
|
||||
awg_args: &str,
|
||||
netstack_args: NetstackArgs,
|
||||
port_check_only: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
private_key: private_key.to_string(),
|
||||
@@ -86,6 +103,12 @@ impl NetstackRequest {
|
||||
v4_ping_config: PingConfig::from_netstack_args_v4(wg_ip4, &netstack_args),
|
||||
v6_ping_config: PingConfig::from_netstack_args_v6(wg_ip6, &netstack_args),
|
||||
download_timeout_sec,
|
||||
port_check: PortCheckConfig {
|
||||
target: netstack_args.port_check_target.clone(),
|
||||
ports: netstack_args.port_check_ports.clone(),
|
||||
only: port_check_only,
|
||||
timeout_sec: netstack_args.port_check_timeout_sec,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +139,8 @@ pub struct NetstackRequestGo {
|
||||
recv_timeout_sec: u64,
|
||||
download_timeout_sec: u64,
|
||||
awg_args: String,
|
||||
#[serde(flatten)]
|
||||
pub port_check: PortCheckConfig,
|
||||
}
|
||||
|
||||
impl NetstackRequestGo {
|
||||
@@ -135,6 +160,7 @@ impl NetstackRequestGo {
|
||||
recv_timeout_sec: req.v4_ping_config.recv_timeout_sec,
|
||||
download_timeout_sec: req.download_timeout_sec,
|
||||
awg_args: req.awg_args.clone(),
|
||||
port_check: req.port_check.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +181,7 @@ impl NetstackRequestGo {
|
||||
recv_timeout_sec: req.v6_ping_config.recv_timeout_sec,
|
||||
download_timeout_sec: req.download_timeout_sec,
|
||||
awg_args: req.awg_args.clone(),
|
||||
port_check: req.port_check.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,6 +200,8 @@ pub struct NetstackResponse {
|
||||
pub downloaded_file_size_bytes: u64,
|
||||
pub download_duration_milliseconds: u64,
|
||||
pub download_error: String,
|
||||
#[serde(default)]
|
||||
pub port_check_results: Option<BTreeMap<String, bool>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
|
||||
@@ -46,6 +46,7 @@ pub async fn wg_probe(
|
||||
auth_version: AuthenticatorVersion,
|
||||
awg_args: Option<String>,
|
||||
netstack_args: NetstackArgs,
|
||||
port_check_only: bool,
|
||||
// TODO: update type
|
||||
credential: CredentialSpendingData,
|
||||
) -> anyhow::Result<WgProbeResults> {
|
||||
@@ -79,8 +80,6 @@ pub async fn wg_probe(
|
||||
AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => bail!("unknown version number"),
|
||||
};
|
||||
|
||||
let mut wg_outcome = WgProbeResults::default();
|
||||
|
||||
info!(
|
||||
"connecting to authenticator: {}...",
|
||||
auth_client.auth_recipient
|
||||
@@ -134,9 +133,9 @@ pub async fn wg_probe(
|
||||
|
||||
info!("Successfully registered with the gateway");
|
||||
|
||||
wg_outcome.can_register = true;
|
||||
|
||||
// Run tunnel connectivity tests using shared helper
|
||||
// Run tunnel connectivity tests using shared helper.
|
||||
// run_tunnel_tests issues blocking CGo calls into Go, so it must run on
|
||||
// tokio's dedicated blocking thread pool to avoid stalling the async runtime.
|
||||
let tunnel_config = WgTunnelConfig::new(
|
||||
registered_data.private_ips().ipv4.to_string(),
|
||||
registered_data.private_ips().ipv6.to_string(),
|
||||
@@ -144,15 +143,18 @@ pub async fn wg_probe(
|
||||
public_key_hex,
|
||||
wg_endpoint,
|
||||
);
|
||||
let awg_str = awg_args.unwrap_or_default();
|
||||
|
||||
run_tunnel_tests(
|
||||
&tunnel_config,
|
||||
&netstack_args,
|
||||
&awg_args.unwrap_or_default(),
|
||||
&mut wg_outcome,
|
||||
);
|
||||
let mut tunnel_result = tokio::task::spawn_blocking(move || {
|
||||
run_tunnel_tests(&tunnel_config, &netstack_args, &awg_str, port_check_only)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("netstack task panicked: {e}"))?;
|
||||
|
||||
Ok(wg_outcome)
|
||||
// can_register is determined by the auth handshake above, not by netstack
|
||||
tunnel_result.can_register = true;
|
||||
|
||||
Ok(tunnel_result)
|
||||
}
|
||||
|
||||
pub async fn lp_registration_probe(
|
||||
|
||||
@@ -1,10 +1,50 @@
|
||||
use nym_connection_monitor::ConnectionStatusEvent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub use super::bandwidth_helpers::{AttachedTicket, AttachedTicketMaterials};
|
||||
pub use super::socks5_test::HttpsConnectivityResult;
|
||||
pub use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PortsCheckSummary {
|
||||
pub all_pass: bool,
|
||||
pub error: Option<String>,
|
||||
pub port_check_target: String,
|
||||
pub failed_ports: Vec<String>,
|
||||
}
|
||||
|
||||
impl PortsCheckSummary {
|
||||
pub fn from_port_map(
|
||||
can_register: bool,
|
||||
port_check_target: impl Into<String>,
|
||||
ports: &BTreeMap<String, bool>,
|
||||
) -> Self {
|
||||
let failed_ports: Vec<String> = ports
|
||||
.iter()
|
||||
.filter_map(|(port, open)| if *open { None } else { Some(port.clone()) })
|
||||
.collect();
|
||||
|
||||
let all_pass = can_register && failed_ports.is_empty() && !ports.is_empty();
|
||||
|
||||
Self {
|
||||
all_pass,
|
||||
error: None,
|
||||
port_check_target: port_check_target.into(),
|
||||
failed_ports,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn probe_error(port_check_target: impl Into<String>, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
all_pass: false,
|
||||
error: Some(message.into()),
|
||||
port_check_target: port_check_target.into(),
|
||||
failed_ports: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProbeResult {
|
||||
pub node: String,
|
||||
@@ -50,6 +90,10 @@ pub struct WgProbeResults {
|
||||
pub download_duration_milliseconds_v6: u64,
|
||||
pub downloaded_file_v6: String,
|
||||
pub download_error_v6: String,
|
||||
|
||||
/// Per-port exit-policy check
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub port_check_results: Option<BTreeMap<String, bool>>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
@@ -195,6 +239,43 @@ impl Socks5ProbeResults {
|
||||
}
|
||||
}
|
||||
|
||||
/// Output of the `run-ports` subcommand — per-port TCP reachability through
|
||||
/// the WG exit tunnel, without the full probe outcome.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PortCheckResult {
|
||||
pub gateway: String,
|
||||
pub can_register: bool,
|
||||
pub port_check_target: String,
|
||||
/// port → open/closed (BTreeMap for deterministic bincode serialization in signed requests)
|
||||
pub ports: BTreeMap<String, bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl PortCheckResult {
|
||||
/// Returns the list of ports that were found **closed** on this gateway.
|
||||
pub fn closed_ports(&self) -> Vec<u16> {
|
||||
self.ports
|
||||
.iter()
|
||||
.filter_map(|(k, &open)| {
|
||||
if open {
|
||||
return None;
|
||||
}
|
||||
match k.parse::<u16>() {
|
||||
Ok(port) => Some(port),
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"Skipping port key {:?} that could not be parsed as u16: {e}",
|
||||
k
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct IpPingReplies {
|
||||
pub ipr_tun_ip_v4: bool,
|
||||
|
||||
@@ -63,23 +63,27 @@ impl WgTunnelConfig {
|
||||
/// - DNS resolution
|
||||
/// - ICMP ping to specified hosts and IPs
|
||||
/// - Optional download test
|
||||
/// - Optional exit policy port check (TCP connect through tunnel)
|
||||
///
|
||||
/// Results are written directly into the provided `wg_outcome` to avoid field-by-field
|
||||
/// copying at call sites.
|
||||
/// **Important:** this function issues blocking FFI calls into Go (CGo) and MUST be
|
||||
/// called via `tokio::task::spawn_blocking` at any async call site. It returns a
|
||||
/// fresh `WgProbeResults`; the caller sets `can_register` after verifying WG
|
||||
/// registration succeeded.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `config` - WireGuard tunnel configuration
|
||||
/// * `netstack_args` - Netstack test parameters (DNS, hosts to ping, timeouts, etc.)
|
||||
/// * `awg_args` - Amnezia WireGuard arguments (empty string for standard WG)
|
||||
/// * `wg_outcome` - Mutable reference to write test results into
|
||||
// This function extracts the shared netstack testing logic from
|
||||
// wg_probe() and wg_probe_lp() to eliminate code duplication.
|
||||
/// * `port_check_only` - If true, skip pings/download and only run TCP port checks
|
||||
// This function extracts the shared netstack testing logic from wg_probe()
|
||||
// to eliminate code duplication across probe modes.
|
||||
pub fn run_tunnel_tests(
|
||||
config: &WgTunnelConfig,
|
||||
netstack_args: &NetstackArgs,
|
||||
awg_args: &str,
|
||||
wg_outcome: &mut WgProbeResults,
|
||||
) {
|
||||
port_check_only: bool,
|
||||
) -> WgProbeResults {
|
||||
let mut wg_outcome = WgProbeResults::default();
|
||||
// Build the netstack request
|
||||
let netstack_request = NetstackRequest::new(
|
||||
&config.private_ipv4,
|
||||
@@ -91,9 +95,10 @@ pub fn run_tunnel_tests(
|
||||
netstack_args.netstack_download_timeout_sec,
|
||||
awg_args,
|
||||
netstack_args.clone(),
|
||||
port_check_only,
|
||||
);
|
||||
|
||||
// Perform IPv4 ping test
|
||||
// Perform IPv4 ping test (also carries port check results in port-check-only mode)
|
||||
info!("Testing IPv4 tunnel connectivity...");
|
||||
let ipv4_request = NetstackRequestGo::from_rust_v4(&netstack_request);
|
||||
|
||||
@@ -122,6 +127,11 @@ pub fn run_tunnel_tests(
|
||||
netstack_response_v4.downloaded_file_size_bytes;
|
||||
wg_outcome.downloaded_file_v4 = netstack_response_v4.downloaded_file;
|
||||
wg_outcome.download_error_v4 = netstack_response_v4.download_error;
|
||||
|
||||
// capture port check results (present when ports were requested)
|
||||
if netstack_response_v4.port_check_results.is_some() {
|
||||
wg_outcome.port_check_results = netstack_response_v4.port_check_results;
|
||||
}
|
||||
}
|
||||
Ok(NetstackResult::Error { error }) => {
|
||||
error!("Netstack runtime error (IPv4): {error}")
|
||||
@@ -131,6 +141,12 @@ pub fn run_tunnel_tests(
|
||||
}
|
||||
}
|
||||
|
||||
// in port-check-only mode, skip IPv6 tests — port checks ran through IPv4 above
|
||||
if port_check_only {
|
||||
info!("Port-check-only mode: skipping IPv6 tunnel tests");
|
||||
return wg_outcome;
|
||||
}
|
||||
|
||||
// Perform IPv6 ping test
|
||||
info!("Testing IPv6 tunnel connectivity...");
|
||||
let ipv6_request = NetstackRequestGo::from_rust_v6(&netstack_request);
|
||||
@@ -167,4 +183,6 @@ pub fn run_tunnel_tests(
|
||||
error!("Internal error (IPv6): {error}")
|
||||
}
|
||||
}
|
||||
|
||||
wg_outcome
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ mod socks5;
|
||||
mod test_mode;
|
||||
|
||||
pub use credentials::{CredentialArgs, CredentialMode};
|
||||
pub use netstack::NetstackArgs;
|
||||
pub use netstack::{EXIT_POLICY_PORTS, NetstackArgs};
|
||||
pub use socks5::Socks5Args;
|
||||
pub use test_mode::TestMode;
|
||||
|
||||
@@ -22,7 +22,7 @@ pub struct ProbeConfig {
|
||||
/// Test mode - explicitly specify which tests to run
|
||||
///
|
||||
/// Modes:
|
||||
/// core. - Traditional mixnet testing (entry/exit pings + WireGuard via authenticator)
|
||||
/// core - Traditional mixnet testing (entry/exit pings + WireGuard via authenticator)
|
||||
/// wg-mix - Wireguard via authenticator
|
||||
/// wg-lp - Entry LP + Exit LP (nested forwarding) + WireGuard
|
||||
/// lp-only - LP registration only (no WireGuard)
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
|
||||
use clap::Args;
|
||||
|
||||
// EXIT_POLICY_PORTS is generated at build time by parsing PORT_MAPPINGS
|
||||
// from scripts/nym-node-setup/network-tunnel-manager.sh.
|
||||
// To add or remove ports, update PORT_MAPPINGS in the shell script and rebuild.
|
||||
include!(concat!(env!("OUT_DIR"), "/exit_policy_ports.rs"));
|
||||
|
||||
#[derive(Args, Clone, Debug)]
|
||||
pub struct NetstackArgs {
|
||||
#[arg(long, hide = true, env = "PROBE_NETSTACK_DOWNLOAD_TIMEOUT_SEC", default_value_t = NetstackArgs::default().netstack_download_timeout_sec)]
|
||||
@@ -37,6 +42,22 @@ pub struct NetstackArgs {
|
||||
|
||||
#[arg(long, hide = true, env = "PROBE_NETSTACK_PING_IPS_V6", default_values_t = NetstackArgs::default().netstack_ping_ips_v6)]
|
||||
pub netstack_ping_ips_v6: Vec<String>,
|
||||
|
||||
/// Target host for exit policy port checks (must listen on all tested ports)
|
||||
#[arg(
|
||||
long = "use-target",
|
||||
env = "PROBE_NETSTACK_PORT_CHECK_TARGET",
|
||||
default_value = "portquiz.net"
|
||||
)]
|
||||
pub port_check_target: String,
|
||||
|
||||
/// List ports to check, separated by a comma.
|
||||
#[arg(long = "check-ports", value_delimiter = ',', default_values_t = Vec::<u16>::new())]
|
||||
pub port_check_ports: Vec<u16>,
|
||||
|
||||
/// Timeout in seconds for each individual port check attempt
|
||||
#[arg(long, default_value_t = NetstackArgs::default().port_check_timeout_sec)]
|
||||
pub port_check_timeout_sec: u64,
|
||||
}
|
||||
|
||||
impl Default for NetstackArgs {
|
||||
@@ -57,6 +78,9 @@ impl Default for NetstackArgs {
|
||||
"2606:4700:4700::1111".to_string(),
|
||||
"2620:fe::fe".to_string(),
|
||||
],
|
||||
port_check_target: "portquiz.net".to_string(),
|
||||
port_check_ports: vec![],
|
||||
port_check_timeout_sec: 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,6 +111,9 @@ mod tests {
|
||||
"2606:4700:4700::1111".to_string(),
|
||||
"2620:fe::fe".to_string(),
|
||||
],
|
||||
port_check_target: "portquiz.net".to_string(),
|
||||
port_check_ports: vec![],
|
||||
port_check_timeout_sec: 5,
|
||||
};
|
||||
|
||||
// Test IPv4 defaults
|
||||
@@ -111,6 +138,11 @@ mod tests {
|
||||
assert_eq!(args.netstack_num_ping, 5);
|
||||
assert_eq!(args.netstack_send_timeout_sec, 3);
|
||||
assert_eq!(args.netstack_recv_timeout_sec, 3);
|
||||
|
||||
// Test port check defaults
|
||||
assert_eq!(args.port_check_target, "portquiz.net");
|
||||
assert!(args.port_check_ports.is_empty());
|
||||
assert_eq!(args.port_check_timeout_sec, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -128,6 +160,9 @@ mod tests {
|
||||
netstack_ping_ips_v4: vec!["8.8.8.8".to_string()],
|
||||
netstack_ping_hosts_v6: vec!["ipv6.example.com".to_string()],
|
||||
netstack_ping_ips_v6: vec!["2001:4860:4860::8888".to_string()],
|
||||
port_check_target: "portquiz.net".to_string(),
|
||||
port_check_ports: vec![80, 443, 8332],
|
||||
port_check_timeout_sec: 10,
|
||||
};
|
||||
|
||||
assert_eq!(args.netstack_ping_hosts_v4, vec!["example.com"]);
|
||||
@@ -163,6 +198,9 @@ mod tests {
|
||||
"2001:4860:4860::8888".to_string(),
|
||||
"2606:4700:4700::1111".to_string(),
|
||||
],
|
||||
port_check_target: "portquiz.net".to_string(),
|
||||
port_check_ports: vec![],
|
||||
port_check_timeout_sec: 5,
|
||||
};
|
||||
|
||||
assert_eq!(args.netstack_ping_hosts_v4, vec!["nym.com", "example.com"]);
|
||||
@@ -192,6 +230,9 @@ mod tests {
|
||||
netstack_ping_ips_v4: vec![],
|
||||
netstack_ping_hosts_v6: vec![],
|
||||
netstack_ping_ips_v6: vec![],
|
||||
port_check_target: "portquiz.net".to_string(),
|
||||
port_check_ports: vec![],
|
||||
port_check_timeout_sec: 0,
|
||||
};
|
||||
|
||||
assert_eq!(args.netstack_num_ping, 0);
|
||||
@@ -219,6 +260,9 @@ mod tests {
|
||||
netstack_ping_ips_v4: vec!["1.1.1.1".to_string()],
|
||||
netstack_ping_hosts_v6: vec!["cloudflare.com".to_string()],
|
||||
netstack_ping_ips_v6: vec!["2001:4860:4860::8888".to_string()],
|
||||
port_check_target: "portquiz.net".to_string(),
|
||||
port_check_ports: vec![],
|
||||
port_check_timeout_sec: 5,
|
||||
};
|
||||
|
||||
assert!(args.netstack_ping_hosts_v4[0].contains("nym"));
|
||||
@@ -228,4 +272,27 @@ mod tests {
|
||||
assert_eq!(args.netstack_v4_dns, "1.1.1.1");
|
||||
assert_eq!(args.netstack_v6_dns, "2606:4700:4700::1111");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exit_policy_ports_no_duplicates_and_sorted() {
|
||||
let ports = EXIT_POLICY_PORTS;
|
||||
assert!(!ports.is_empty(), "EXIT_POLICY_PORTS should not be empty");
|
||||
|
||||
// verify sorted
|
||||
for window in ports.windows(2) {
|
||||
assert!(
|
||||
window[0] < window[1],
|
||||
"EXIT_POLICY_PORTS out of order or duplicate: {} >= {}",
|
||||
window[0],
|
||||
window[1]
|
||||
);
|
||||
}
|
||||
|
||||
// spot-check a few well-known ports
|
||||
assert!(ports.contains(&22), "should contain SSH (22)");
|
||||
assert!(ports.contains(&443), "should contain HTTPS (443)");
|
||||
assert!(ports.contains(&22021), "should contain Session (22021)");
|
||||
assert!(ports.contains(&8332), "should contain Bitcoin (8332)");
|
||||
assert!(ports.contains(&9735), "should contain Lightning (9735)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
//! - LpOnly: LP registration only, no WireGuard
|
||||
//! - Socks5Only: Socks5 test
|
||||
//! - All: Mixnet, wireguard over authenticator and LP registration
|
||||
//!
|
||||
//! Note: Exit policy port checking is handled by the `run-ports` subcommand,
|
||||
//! not via a test mode.
|
||||
|
||||
/// Test mode for the gateway probe.
|
||||
///
|
||||
|
||||
@@ -9,7 +9,9 @@ use crate::common::probe_tests::{
|
||||
};
|
||||
use crate::common::types::{Entry, LpProbeResults};
|
||||
use crate::config::{CredentialArgs, CredentialMode, NetstackArgs, ProbeConfig};
|
||||
use nym_authenticator_client::{AuthClientMixnetListener, AuthenticatorClient};
|
||||
use nym_authenticator_client::{
|
||||
AuthClientMixnetListener, AuthClientMixnetListenerHandle, AuthenticatorClient,
|
||||
};
|
||||
use nym_bandwidth_controller::BandwidthTicketProvider;
|
||||
use nym_client_core::config::ForgetMe;
|
||||
use nym_config::defaults::NymNetworkDetails;
|
||||
@@ -20,13 +22,16 @@ use nym_sdk::mixnet::{
|
||||
};
|
||||
use nym_topology::{HardcodedTopologyProvider, NymTopology};
|
||||
use rand::rngs::OsRng;
|
||||
use std::collections::BTreeMap;
|
||||
use std::net::IpAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::*;
|
||||
|
||||
pub use crate::common::nodes::{NymApiDirectory, query_gateway_by_ip};
|
||||
pub use crate::common::types::{ProbeOutcome, ProbeResult};
|
||||
pub use crate::common::types::{PortCheckResult, PortsCheckSummary, ProbeOutcome, ProbeResult};
|
||||
|
||||
mod common;
|
||||
pub use common::types;
|
||||
@@ -45,7 +50,295 @@ pub struct Probe {
|
||||
topology: Option<NymTopology>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RunPortsConfig {
|
||||
pub min_gateway_mixnet_performance: Option<u8>,
|
||||
pub ignore_egress_epoch_role: bool,
|
||||
pub netstack_args: NetstackArgs,
|
||||
}
|
||||
|
||||
// Port checks always target a bonded gateway. There are two entry points:
|
||||
// - `run_ports` : local CLI, on-disk storage + mnemonic.
|
||||
// - `run_ports_for_agent`: NS agent, ephemeral storage + ticket materials.
|
||||
|
||||
struct PortScanRun {
|
||||
can_register: bool,
|
||||
port_results: BTreeMap<String, bool>,
|
||||
last_error: Option<String>,
|
||||
}
|
||||
|
||||
/// Auth endpoint to probe during a port scan.
|
||||
struct PortScanTarget {
|
||||
authenticator: nym_sdk::mixnet::Recipient,
|
||||
authenticator_version: nym_authenticator_requests::AuthenticatorVersion,
|
||||
ip_address: IpAddr,
|
||||
}
|
||||
|
||||
/// Ticket-acquisition parameters for a single port scan session.
|
||||
struct PortScanBandwidth<'a> {
|
||||
provider: &'a dyn BandwidthTicketProvider,
|
||||
ticket_type: TicketType,
|
||||
credential_provider: nym_sdk::mixnet::NodeIdentity,
|
||||
}
|
||||
|
||||
/// Validated info needed to run a WG port-check via the mixnet.
|
||||
struct PortCheckSetup {
|
||||
exit_node: TestedNodeDetails,
|
||||
exit_identity: String,
|
||||
authenticator: nym_sdk::mixnet::Recipient,
|
||||
ip_address: IpAddr,
|
||||
port_check_target: String,
|
||||
ports_count: usize,
|
||||
}
|
||||
|
||||
impl PortCheckSetup {
|
||||
fn new(exit_node: TestedNodeDetails, config: &RunPortsConfig) -> anyhow::Result<Self> {
|
||||
let exit_identity = exit_node.identity.to_string();
|
||||
|
||||
let (authenticator, ip_address) =
|
||||
match (exit_node.authenticator_address, exit_node.ip_address) {
|
||||
(Some(auth), Some(ip)) => (auth, ip),
|
||||
_ => anyhow::bail!(
|
||||
"Gateway {} missing authenticator address or IP — not a functional exit",
|
||||
exit_identity
|
||||
),
|
||||
};
|
||||
|
||||
let ports_count = config.netstack_args.port_check_ports.len();
|
||||
if ports_count == 0 {
|
||||
anyhow::bail!(
|
||||
"No ports specified. Use --check-ports 80,443,22021 or --check-all-ports"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
exit_node,
|
||||
exit_identity,
|
||||
authenticator,
|
||||
ip_address,
|
||||
port_check_target: config.netstack_args.port_check_target.clone(),
|
||||
ports_count,
|
||||
})
|
||||
}
|
||||
|
||||
fn failed_to_connect(&self, err: impl std::fmt::Display) -> PortCheckResult {
|
||||
PortCheckResult {
|
||||
gateway: self.exit_identity.clone(),
|
||||
can_register: false,
|
||||
port_check_target: self.port_check_target.clone(),
|
||||
ports: BTreeMap::new(),
|
||||
error: Some(format!("Failed to connect to mixnet: {err}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Probe {
|
||||
fn parse_port_check_targets(raw: &str) -> Vec<String> {
|
||||
raw.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn is_target_reachable(host: &str) -> bool {
|
||||
let timeout = Duration::from_secs(2);
|
||||
|
||||
for port in [80u16, 443u16] {
|
||||
if tokio::time::timeout(timeout, tokio::net::TcpStream::connect((host, port)))
|
||||
.await
|
||||
.is_ok_and(|res| res.is_ok())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
async fn run_port_scan_with_retries(
|
||||
mixnet_listener_task: &AuthClientMixnetListenerHandle,
|
||||
nym_address: nym_sdk::mixnet::Recipient,
|
||||
target: PortScanTarget,
|
||||
bandwidth: PortScanBandwidth<'_>,
|
||||
netstack_args: NetstackArgs,
|
||||
awg_args: Option<String>,
|
||||
) -> PortScanRun {
|
||||
let mut port_results: BTreeMap<String, bool> = BTreeMap::new();
|
||||
let mut can_register = false;
|
||||
let mut last_error = None;
|
||||
let max_attempts = 3;
|
||||
|
||||
for attempt in 1..=max_attempts {
|
||||
if attempt > 1 {
|
||||
info!("Retrying authenticator registration (attempt {attempt}/{max_attempts})...");
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
|
||||
let credential = match bandwidth
|
||||
.provider
|
||||
.get_ecash_ticket(bandwidth.ticket_type, bandwidth.credential_provider, 1)
|
||||
.await
|
||||
{
|
||||
Ok(ticket) => ticket.data,
|
||||
Err(e) => {
|
||||
error!("Failed to get ecash ticket: {e}");
|
||||
last_error = Some(format!("Failed to get ecash ticket: {e}"));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
let auth_client = AuthenticatorClient::new(
|
||||
mixnet_listener_task.subscribe(),
|
||||
mixnet_listener_task.mixnet_sender(),
|
||||
nym_address,
|
||||
target.authenticator,
|
||||
target.authenticator_version,
|
||||
Arc::new(x25519::KeyPair::new(&mut rng)),
|
||||
target.ip_address,
|
||||
);
|
||||
|
||||
match wg_probe(
|
||||
auth_client,
|
||||
target.ip_address,
|
||||
target.authenticator_version,
|
||||
awg_args.clone(),
|
||||
netstack_args.clone(),
|
||||
true, // port_check_only
|
||||
credential,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(outcome) => {
|
||||
if outcome.can_register {
|
||||
can_register = true;
|
||||
port_results = outcome
|
||||
.port_check_results
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.collect();
|
||||
let open = port_results.values().filter(|&&v| v).count();
|
||||
info!(
|
||||
"Port check complete: {}/{} ports open",
|
||||
open,
|
||||
port_results.len()
|
||||
);
|
||||
break;
|
||||
}
|
||||
warn!(
|
||||
"Auth registration returned but can_register=false (attempt {attempt}/{max_attempts})"
|
||||
);
|
||||
last_error = Some("Auth registration did not complete".into());
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("WG probe error: {e} (attempt {attempt}/{max_attempts})");
|
||||
last_error = Some(format!("WG probe error: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PortScanRun {
|
||||
can_register,
|
||||
port_results,
|
||||
last_error,
|
||||
}
|
||||
}
|
||||
|
||||
/// Warm up routes, register with the authenticator, run the port scan and tear down.
|
||||
async fn port_check_after_connect(
|
||||
mixnet_client: MixnetClient,
|
||||
setup: PortCheckSetup,
|
||||
bandwidth_provider: &dyn BandwidthTicketProvider,
|
||||
netstack_args: NetstackArgs,
|
||||
) -> PortCheckResult {
|
||||
let targets = Self::parse_port_check_targets(&setup.port_check_target);
|
||||
let mut selected_target = targets
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| setup.port_check_target.clone());
|
||||
|
||||
if targets.len() > 1 {
|
||||
let mut found = false;
|
||||
for candidate in &targets {
|
||||
if Self::is_target_reachable(candidate).await {
|
||||
selected_target = candidate.clone();
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
warn!(
|
||||
"Port-check target '{}' is unreachable, trying next",
|
||||
candidate
|
||||
);
|
||||
}
|
||||
if !found {
|
||||
warn!(
|
||||
"All port-check targets unreachable; falling back to first: '{}'",
|
||||
selected_target
|
||||
);
|
||||
} else if selected_target != targets[0] {
|
||||
info!(
|
||||
"Port check: selected target '{}' (first reachable from list)",
|
||||
selected_target
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut netstack_args = netstack_args;
|
||||
netstack_args.port_check_target = selected_target.clone();
|
||||
|
||||
info!("Warming up mixnet routes...");
|
||||
let nym_address = *mixnet_client.nym_address();
|
||||
let (warmup_result, mixnet_client) = do_ping(
|
||||
mixnet_client,
|
||||
nym_address,
|
||||
setup.exit_node.exit_router_address,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
match warmup_result {
|
||||
Ok(_) => info!("Mixnet warmup done"),
|
||||
Err(e) => warn!("Warmup had issues ({e}), auth may be less reliable"),
|
||||
}
|
||||
|
||||
let nym_address = *mixnet_client.nym_address();
|
||||
let mixnet_listener_task =
|
||||
AuthClientMixnetListener::new(mixnet_client, CancellationToken::new()).start();
|
||||
|
||||
let scan = Self::run_port_scan_with_retries(
|
||||
&mixnet_listener_task,
|
||||
nym_address,
|
||||
PortScanTarget {
|
||||
authenticator: setup.authenticator,
|
||||
authenticator_version: setup.exit_node.authenticator_version,
|
||||
ip_address: setup.ip_address,
|
||||
},
|
||||
PortScanBandwidth {
|
||||
provider: bandwidth_provider,
|
||||
ticket_type: TicketType::V1WireguardExit,
|
||||
credential_provider: setup.exit_node.identity,
|
||||
},
|
||||
netstack_args,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
mixnet_listener_task.stop().await;
|
||||
|
||||
PortCheckResult {
|
||||
gateway: setup.exit_identity,
|
||||
can_register: scan.can_register,
|
||||
port_check_target: selected_target,
|
||||
ports: scan.port_results,
|
||||
error: if scan.can_register {
|
||||
None
|
||||
} else {
|
||||
scan.last_error
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a probe with pre-queried gateway nodes
|
||||
pub fn new(
|
||||
entry_node: TestedNodeDetails,
|
||||
@@ -90,7 +383,6 @@ impl Probe {
|
||||
})
|
||||
}
|
||||
|
||||
/// Run a probe as an NS agent (orchestrator for multiple probe runs for NS API)
|
||||
pub async fn probe_run_agent(
|
||||
mut self,
|
||||
credential_args: CredentialArgs,
|
||||
@@ -269,6 +561,174 @@ impl Probe {
|
||||
self.do_probe_test(mixnet_client, bandwidth_provider).await
|
||||
}
|
||||
|
||||
pub async fn run_ports(
|
||||
entry_node: TestedNodeDetails,
|
||||
exit_node: Option<TestedNodeDetails>,
|
||||
network: NymNetworkDetails,
|
||||
config: &RunPortsConfig,
|
||||
config_dir: &PathBuf,
|
||||
credential: CredentialMode,
|
||||
) -> anyhow::Result<PortCheckResult> {
|
||||
let exit_node = exit_node.unwrap_or(entry_node.clone());
|
||||
let setup = PortCheckSetup::new(exit_node, config)?;
|
||||
|
||||
info!(
|
||||
"Port check: testing {} ports on gateway {} via {}",
|
||||
setup.ports_count, setup.exit_identity, setup.port_check_target
|
||||
);
|
||||
|
||||
let storage_paths = StoragePaths::new_from_dir(config_dir)?;
|
||||
let storage = storage_paths
|
||||
.initialise_default_persistent_storage()
|
||||
.await?;
|
||||
|
||||
let mixnet_debug_config = helpers::mixnet_debug_config(
|
||||
config.min_gateway_mixnet_performance,
|
||||
config.ignore_egress_epoch_role,
|
||||
);
|
||||
|
||||
let topology = helpers::fetch_topology(&network, &mixnet_debug_config)
|
||||
.await
|
||||
.inspect_err(|e| warn!("Failed to fetch topology: {e}"))
|
||||
.ok();
|
||||
|
||||
let mut mixnet_client_builder = MixnetClientBuilder::new_with_storage(storage.clone())
|
||||
.request_gateway(entry_node.identity.to_string())
|
||||
.network_details(network.clone())
|
||||
.debug_config(mixnet_debug_config)
|
||||
.with_forget_me(ForgetMe::new_stats())
|
||||
.credentials_mode(!credential.use_mock_ecash);
|
||||
|
||||
if let Some(topology) = &topology {
|
||||
mixnet_client_builder = mixnet_client_builder.custom_topology_provider(Box::new(
|
||||
HardcodedTopologyProvider::new(topology.clone()),
|
||||
));
|
||||
}
|
||||
|
||||
let disconnected_mixnet_client = mixnet_client_builder.build()?;
|
||||
|
||||
// make sure identity keys exist before credential acquisition
|
||||
// (acquire_bandwidth → create_bandwidth_client needs them on disk)
|
||||
let key_store = storage.key_store();
|
||||
if key_store.load_keys().await.is_err() {
|
||||
debug!("Generating new client keys");
|
||||
let mut rng = OsRng;
|
||||
nym_client_core::init::generate_new_client_keys(&mut rng, key_store).await?;
|
||||
}
|
||||
|
||||
credential
|
||||
.acquire(&disconnected_mixnet_client, &storage)
|
||||
.await?;
|
||||
|
||||
let bandwidth_provider = build_bandwidth_controller(
|
||||
&network,
|
||||
storage.credential_store().clone(),
|
||||
credential.use_mock_ecash,
|
||||
)?;
|
||||
|
||||
let mixnet_client = match disconnected_mixnet_client.connect_to_mixnet().await {
|
||||
Ok(client) => {
|
||||
info!(
|
||||
"Connected to mixnet via entry gateway: {}",
|
||||
entry_node.identity
|
||||
);
|
||||
info!("Our nym address: {}", *client.nym_address());
|
||||
client
|
||||
}
|
||||
Err(e) => return Ok(setup.failed_to_connect(e)),
|
||||
};
|
||||
|
||||
Ok(Self::port_check_after_connect(
|
||||
mixnet_client,
|
||||
setup,
|
||||
bandwidth_provider.as_ref(),
|
||||
config.netstack_args.clone(),
|
||||
)
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Bonded gateway port-check, run by the NS agent. Uses ephemeral storage and ticket
|
||||
/// materials provided by the NS API instead of mnemonic-based acquisition.
|
||||
pub async fn run_ports_for_agent(
|
||||
entry_gateway: nym_sdk::mixnet::ed25519::PublicKey,
|
||||
network: NymNetworkDetails,
|
||||
config: &RunPortsConfig,
|
||||
credential_args: CredentialArgs,
|
||||
) -> anyhow::Result<PortCheckResult> {
|
||||
let api_url = network
|
||||
.endpoints
|
||||
.first()
|
||||
.and_then(|ep| ep.api_url())
|
||||
.ok_or(anyhow::anyhow!("missing api url"))?;
|
||||
|
||||
let directory = NymApiDirectory::new(api_url).await?;
|
||||
let entry_node = directory
|
||||
.entry_gateway(&entry_gateway)?
|
||||
.to_testable_node()?;
|
||||
|
||||
// agent always uses the entry gateway as the exit
|
||||
let setup = PortCheckSetup::new(entry_node.clone(), config)?;
|
||||
|
||||
info!(
|
||||
"Port check (agent): testing {} ports on gateway {} via {}",
|
||||
setup.ports_count, setup.exit_identity, setup.port_check_target
|
||||
);
|
||||
|
||||
let storage = Ephemeral::default();
|
||||
|
||||
let mixnet_debug_config = helpers::mixnet_debug_config(
|
||||
config.min_gateway_mixnet_performance,
|
||||
config.ignore_egress_epoch_role,
|
||||
);
|
||||
|
||||
let topology = helpers::fetch_topology(&network, &mixnet_debug_config)
|
||||
.await
|
||||
.inspect_err(|e| warn!("Failed to fetch topology: {e}"))
|
||||
.ok();
|
||||
|
||||
let mut mixnet_client_builder = MixnetClientBuilder::new_with_storage(storage.clone())
|
||||
.request_gateway(entry_node.identity.to_string())
|
||||
.network_details(network.clone())
|
||||
.debug_config(mixnet_debug_config)
|
||||
.with_forget_me(ForgetMe::new_stats())
|
||||
.credentials_mode(true);
|
||||
|
||||
if let Some(topology) = &topology {
|
||||
mixnet_client_builder = mixnet_client_builder.custom_topology_provider(Box::new(
|
||||
HardcodedTopologyProvider::new(topology.clone()),
|
||||
));
|
||||
}
|
||||
|
||||
let disconnected_mixnet_client = mixnet_client_builder.build()?;
|
||||
|
||||
credential_args
|
||||
.import_credential(&disconnected_mixnet_client)
|
||||
.await?;
|
||||
|
||||
let bandwidth_provider =
|
||||
build_bandwidth_controller(&network, storage.credential_store().clone(), false)?;
|
||||
|
||||
let mixnet_client = match disconnected_mixnet_client.connect_to_mixnet().await {
|
||||
Ok(client) => {
|
||||
info!(
|
||||
"Connected to mixnet via entry gateway: {}",
|
||||
entry_node.identity
|
||||
);
|
||||
info!("Our nym address: {}", *client.nym_address());
|
||||
client
|
||||
}
|
||||
Err(e) => return Ok(setup.failed_to_connect(e)),
|
||||
};
|
||||
|
||||
Ok(Self::port_check_after_connect(
|
||||
mixnet_client,
|
||||
setup,
|
||||
bandwidth_provider.as_ref(),
|
||||
config.netstack_args.clone(),
|
||||
)
|
||||
.await)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn do_probe_test(
|
||||
self,
|
||||
@@ -369,6 +829,7 @@ impl Probe {
|
||||
let mixnet_listener_task =
|
||||
AuthClientMixnetListener::new(mixnet_client, CancellationToken::new())
|
||||
.start();
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
let auth_client = AuthenticatorClient::new(
|
||||
mixnet_listener_task.subscribe(),
|
||||
@@ -397,6 +858,7 @@ impl Probe {
|
||||
exit_node.authenticator_version,
|
||||
self.config.amnezia_args.clone(),
|
||||
self.config.netstack_args.clone(),
|
||||
false,
|
||||
credential,
|
||||
)
|
||||
.await
|
||||
@@ -404,6 +866,7 @@ impl Probe {
|
||||
|
||||
// Add wg results to probe result
|
||||
probe_result.outcome.wg = Some(outcome);
|
||||
|
||||
mixnet_listener_task.stop().await;
|
||||
} else {
|
||||
warn!("Not enough information to run WireGuard via mixnet registration tests");
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use nym_bin_common::bin_info;
|
||||
use nym_config::defaults::setup_env;
|
||||
use nym_gateway_probe::config::{CredentialArgs, CredentialMode, ProbeConfig};
|
||||
use nym_gateway_probe::{NymApiDirectory, ProbeResult, query_gateway_by_ip};
|
||||
use nym_gateway_probe::config::{CredentialArgs, CredentialMode, NetstackArgs, ProbeConfig};
|
||||
use nym_gateway_probe::{
|
||||
NymApiDirectory, PortCheckResult, ProbeResult, RunPortsConfig, query_gateway_by_ip,
|
||||
};
|
||||
use nym_sdk::mixnet::NodeIdentity;
|
||||
use serde::Serialize;
|
||||
use std::path::Path;
|
||||
use std::{path::PathBuf, sync::OnceLock};
|
||||
use tracing::*;
|
||||
@@ -28,7 +31,7 @@ struct CliArgs {
|
||||
config_env_file: Option<PathBuf>,
|
||||
|
||||
/// Disable logging during probe
|
||||
#[arg(long)]
|
||||
#[arg(long, global = true)]
|
||||
no_log: bool,
|
||||
}
|
||||
|
||||
@@ -81,6 +84,35 @@ enum Commands {
|
||||
probe_config: ProbeConfig,
|
||||
},
|
||||
|
||||
/// Check WG exit policy ports on a bonded gateway.
|
||||
/// Tests TCP connectivity through the WG tunnel for each port.
|
||||
/// Use --check-ports to pick specific ports, or --check-all-ports for the full exit policy list.
|
||||
RunPorts {
|
||||
/// Directory for credential and mixnet storage
|
||||
#[arg(long)]
|
||||
config_dir: Option<PathBuf>,
|
||||
|
||||
/// Bonded gateway identity.
|
||||
#[arg(long, short = 'g', alias = "gateway")]
|
||||
entry_gateway: NodeIdentity,
|
||||
|
||||
/// Separate exit gateway to test (entry_gateway is used for mixnet entry).
|
||||
#[arg(long)]
|
||||
exit_gateway: Option<NodeIdentity>,
|
||||
|
||||
/// Test every port in the canonical exit policy (network-tunnel-manager.sh PORT_MAPPINGS).
|
||||
/// Overrides --check-ports.
|
||||
#[arg(long)]
|
||||
check_all_ports: bool,
|
||||
|
||||
/// Arguments to manage credentials
|
||||
#[command(flatten)]
|
||||
credential_mode: CredentialMode,
|
||||
|
||||
#[command(flatten)]
|
||||
probe_config: RunPortsProbeConfig,
|
||||
},
|
||||
|
||||
/// Run the probe by NS agents
|
||||
RunAgent {
|
||||
/// The specific gateway specified by ID.
|
||||
@@ -96,6 +128,29 @@ enum Commands {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Args, Clone)]
|
||||
struct RunPortsProbeConfig {
|
||||
/// Only choose gateway with that minimum performance
|
||||
#[arg(long)]
|
||||
min_gateway_mixnet_performance: Option<u8>,
|
||||
|
||||
/// Ignore egress epoch role constraints
|
||||
#[arg(long, global = true)]
|
||||
ignore_egress_epoch_role: bool,
|
||||
|
||||
/// Arguments to manage netstack downloads and port checks
|
||||
#[command(flatten)]
|
||||
netstack_args: NetstackArgs,
|
||||
}
|
||||
|
||||
/// CLI output wrapper — either a standard probe result or a port-check result
|
||||
#[derive(Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum ProbeOutput {
|
||||
Standard(Box<ProbeResult>),
|
||||
PortCheck(PortCheckResult),
|
||||
}
|
||||
|
||||
fn setup_logging() {
|
||||
// SAFETY: those are valid directives
|
||||
#[allow(clippy::unwrap_used)]
|
||||
@@ -112,7 +167,7 @@ fn setup_logging() {
|
||||
.init();
|
||||
}
|
||||
|
||||
pub(crate) async fn run() -> anyhow::Result<ProbeResult> {
|
||||
pub(crate) async fn run() -> anyhow::Result<ProbeOutput> {
|
||||
let args = CliArgs::parse();
|
||||
if !args.no_log {
|
||||
setup_logging();
|
||||
@@ -122,7 +177,7 @@ pub(crate) async fn run() -> anyhow::Result<ProbeResult> {
|
||||
setup_env(args.config_env_file.as_ref());
|
||||
let network = nym_sdk::NymNetworkDetails::new_from_env();
|
||||
|
||||
info!("{:#?}", network);
|
||||
debug!("{:#?}", network);
|
||||
|
||||
match args.command {
|
||||
Commands::RunLocal {
|
||||
@@ -165,7 +220,9 @@ pub(crate) async fn run() -> anyhow::Result<ProbeResult> {
|
||||
let trial =
|
||||
nym_gateway_probe::Probe::new(entry_details, exit_details, network, probe_config);
|
||||
|
||||
Box::pin(trial.probe_run_locally(&config_dir, credential_mode)).await
|
||||
Box::pin(trial.probe_run_locally(&config_dir, credential_mode))
|
||||
.await
|
||||
.map(|r| ProbeOutput::Standard(Box::new(r)))
|
||||
}
|
||||
Commands::Run {
|
||||
entry_gateway,
|
||||
@@ -209,7 +266,79 @@ pub(crate) async fn run() -> anyhow::Result<ProbeResult> {
|
||||
|
||||
let trial =
|
||||
nym_gateway_probe::Probe::new(entry_details, exit_details, network, probe_config);
|
||||
Box::pin(trial.probe_run(&config_dir, credential_mode)).await
|
||||
Box::pin(trial.probe_run(&config_dir, credential_mode))
|
||||
.await
|
||||
.map(|r| ProbeOutput::Standard(Box::new(r)))
|
||||
}
|
||||
Commands::RunPorts {
|
||||
entry_gateway,
|
||||
exit_gateway,
|
||||
config_dir,
|
||||
check_all_ports,
|
||||
credential_mode,
|
||||
probe_config,
|
||||
} => {
|
||||
let mut run_ports_config = RunPortsConfig {
|
||||
min_gateway_mixnet_performance: probe_config.min_gateway_mixnet_performance,
|
||||
ignore_egress_epoch_role: probe_config.ignore_egress_epoch_role,
|
||||
netstack_args: probe_config.netstack_args,
|
||||
};
|
||||
|
||||
// --check-all-ports overrides --check-ports with the full exit policy list
|
||||
if check_all_ports {
|
||||
use nym_gateway_probe::config::EXIT_POLICY_PORTS;
|
||||
run_ports_config.netstack_args.port_check_ports = EXIT_POLICY_PORTS.to_vec();
|
||||
info!(
|
||||
"Using full exit policy port list ({} ports)",
|
||||
EXIT_POLICY_PORTS.len()
|
||||
);
|
||||
}
|
||||
|
||||
let api_url = network
|
||||
.endpoints
|
||||
.first()
|
||||
.and_then(|ep| ep.api_url())
|
||||
.ok_or(anyhow::anyhow!("missing api url"))?;
|
||||
|
||||
let directory = NymApiDirectory::new(api_url).await?;
|
||||
|
||||
let entry_details = directory
|
||||
.entry_gateway(&entry_gateway)?
|
||||
.to_testable_node()?;
|
||||
|
||||
let exit_details = exit_gateway
|
||||
.map(|id_key| directory.exit_gateway(&id_key))
|
||||
.transpose()?
|
||||
.map(|node| node.to_testable_node())
|
||||
.transpose()?;
|
||||
|
||||
let config_dir = config_dir
|
||||
.clone()
|
||||
.unwrap_or_else(|| Path::new(DEFAULT_CONFIG_DIR).join(&network.network_name));
|
||||
|
||||
if config_dir.is_file() {
|
||||
anyhow::bail!("provided configuration directory is a file");
|
||||
}
|
||||
|
||||
if !config_dir.exists() {
|
||||
std::fs::create_dir_all(config_dir.clone())?;
|
||||
}
|
||||
|
||||
info!(
|
||||
"using the following directory for the probe config: {}",
|
||||
config_dir.display()
|
||||
);
|
||||
|
||||
Box::pin(nym_gateway_probe::Probe::run_ports(
|
||||
entry_details,
|
||||
exit_details,
|
||||
network,
|
||||
&run_ports_config,
|
||||
&config_dir,
|
||||
credential_mode,
|
||||
))
|
||||
.await
|
||||
.map(ProbeOutput::PortCheck)
|
||||
}
|
||||
Commands::RunAgent {
|
||||
entry_gateway,
|
||||
@@ -219,7 +348,9 @@ pub(crate) async fn run() -> anyhow::Result<ProbeResult> {
|
||||
let trial =
|
||||
nym_gateway_probe::Probe::new_for_agent(entry_gateway, network, probe_config)
|
||||
.await?;
|
||||
Box::pin(trial.probe_run_agent(credential_args)).await
|
||||
Box::pin(trial.probe_run_agent(credential_args))
|
||||
.await
|
||||
.map(|r| ProbeOutput::Standard(Box::new(r)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-node-status-agent"
|
||||
version = "2.0.1-rc3"
|
||||
version = "2.0.1"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
use crate::cli::{ServerConfig, parse_server_config};
|
||||
use anyhow::anyhow;
|
||||
use nym_gateway_probe::config::CredentialArgs;
|
||||
use nym_gateway_probe::types::{AttachedTicketMaterials, VersionedSerialise};
|
||||
use nym_node_status_client::NsApiClient;
|
||||
use nym_sdk::mixnet::ed25519::PublicKey;
|
||||
|
||||
pub(crate) fn parse_servers(raw: &[String]) -> anyhow::Result<Vec<ServerConfig>> {
|
||||
raw.iter()
|
||||
.map(|s| {
|
||||
parse_server_config(s).map_err(|e| {
|
||||
tracing::error!("Invalid server config '{}': {}", s, e);
|
||||
anyhow!("Invalid server config '{}': {}", s, e)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn primary(servers: &[ServerConfig]) -> anyhow::Result<&ServerConfig> {
|
||||
servers
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("No servers configured"))
|
||||
}
|
||||
|
||||
pub(crate) fn build_client(server: &ServerConfig) -> NsApiClient {
|
||||
let auth_key =
|
||||
nym_crypto::asymmetric::ed25519::PrivateKey::from_bytes(&server.auth_key.to_bytes())
|
||||
.expect("Failed to clone auth key");
|
||||
NsApiClient::new(&server.address, server.port, auth_key)
|
||||
}
|
||||
|
||||
pub(crate) fn parse_gateway_pubkey(key: &str) -> anyhow::Result<PublicKey> {
|
||||
PublicKey::from_base58_string(key).map_err(|e| anyhow!("Failed to parse GW identity key: {e}"))
|
||||
}
|
||||
|
||||
pub(crate) fn credential_args_from(materials: AttachedTicketMaterials) -> CredentialArgs {
|
||||
CredentialArgs {
|
||||
ticket_materials: materials.to_serialised_string(),
|
||||
ticket_materials_revision:
|
||||
<AttachedTicketMaterials as VersionedSerialise>::CURRENT_SERIALISATION_REVISION,
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,9 @@ use std::{env, sync::OnceLock};
|
||||
use tokio::time::Instant;
|
||||
use tracing::info;
|
||||
|
||||
pub(crate) mod common;
|
||||
pub(crate) mod generate_keypair;
|
||||
pub(crate) mod run_ports_check;
|
||||
pub(crate) mod run_probe;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -22,7 +24,7 @@ fn pretty_build_info_static() -> &'static str {
|
||||
PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print())
|
||||
}
|
||||
|
||||
fn parse_server_config(s: &str) -> Result<ServerConfig, String> {
|
||||
pub(super) fn parse_server_config(s: &str) -> Result<ServerConfig, String> {
|
||||
let parts: Vec<&str> = s.split('|').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err("Server config must be in format 'address|port'".to_string());
|
||||
@@ -32,8 +34,10 @@ fn parse_server_config(s: &str) -> Result<ServerConfig, String> {
|
||||
let port = parts[1]
|
||||
.parse::<u16>()
|
||||
.map_err(|_| "Invalid port number".to_string())?;
|
||||
let auth_key =
|
||||
PrivateKey::from_base58_string(env::var("NODE_STATUS_AGENT_AUTH_KEY").unwrap()).unwrap();
|
||||
let raw_key = env::var("NODE_STATUS_AGENT_AUTH_KEY")
|
||||
.map_err(|_| "NODE_STATUS_AGENT_AUTH_KEY environment variable is not set".to_string())?;
|
||||
let auth_key = PrivateKey::from_base58_string(raw_key)
|
||||
.map_err(|e| format!("Failed to decode NODE_STATUS_AGENT_AUTH_KEY as base58: {e}"))?;
|
||||
|
||||
Ok(ServerConfig {
|
||||
address,
|
||||
@@ -53,6 +57,7 @@ pub(crate) struct Args {
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub(crate) enum Command {
|
||||
RunProbe(RunProbeArgs),
|
||||
RunPortsCheck(RunPortsCheckArgs),
|
||||
|
||||
GenerateKeypair {
|
||||
#[arg(long)]
|
||||
@@ -73,6 +78,26 @@ pub(crate) struct RunProbeArgs {
|
||||
pub probe_config: nym_gateway_probe::config::ProbeConfig,
|
||||
}
|
||||
|
||||
#[derive(clap::Args, Debug)]
|
||||
pub(crate) struct RunPortsCheckArgs {
|
||||
/// Server configurations in format "address|port"
|
||||
/// Can be specified multiple times for multiple servers
|
||||
#[arg(short, long, required = true)]
|
||||
pub server: Vec<String>,
|
||||
|
||||
/// Only choose gateway with that minimum performance
|
||||
#[arg(long)]
|
||||
pub min_gateway_mixnet_performance: Option<u8>,
|
||||
|
||||
/// Ignore egress epoch role constraints
|
||||
#[arg(long)]
|
||||
pub ignore_egress_epoch_role: bool,
|
||||
|
||||
/// Arguments to manage netstack downloads and port checks
|
||||
#[command(flatten)]
|
||||
pub netstack_args: nym_gateway_probe::config::NetstackArgs,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
pub(crate) async fn execute(self, log_capture: LogCapture) -> anyhow::Result<()> {
|
||||
match self.command {
|
||||
@@ -99,6 +124,21 @@ impl Args {
|
||||
|
||||
res?;
|
||||
}
|
||||
Command::RunPortsCheck(args) => {
|
||||
let servers = common::parse_servers(&args.server)?;
|
||||
|
||||
run_ports_check::run_ports_check(
|
||||
&servers,
|
||||
args.min_gateway_mixnet_performance,
|
||||
args.ignore_egress_epoch_role,
|
||||
args.netstack_args,
|
||||
log_capture,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
tracing::error!("{err}");
|
||||
})?
|
||||
}
|
||||
Command::GenerateKeypair { path } => {
|
||||
let path = path
|
||||
.to_owned()
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
use crate::cli::ServerConfig;
|
||||
use crate::cli::common;
|
||||
use crate::log_capture::LogCapture;
|
||||
use nym_gateway_probe::RunPortsConfig;
|
||||
use tracing::instrument;
|
||||
// Hard deadline for a single port-scan job.
|
||||
const PORT_SCAN_HARD_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5400);
|
||||
|
||||
pub(crate) async fn run_ports_check(
|
||||
servers: &[ServerConfig],
|
||||
min_gateway_mixnet_performance: Option<u8>,
|
||||
ignore_egress_epoch_role: bool,
|
||||
mut netstack_args: nym_gateway_probe::config::NetstackArgs,
|
||||
log_capture: LogCapture,
|
||||
) -> anyhow::Result<()> {
|
||||
let primary_server = common::primary(servers)?;
|
||||
tracing::info!(
|
||||
"Requesting ports-check testrun from primary server: {}:{}",
|
||||
primary_server.address,
|
||||
primary_server.port
|
||||
);
|
||||
|
||||
let ns_api_client = common::build_client(primary_server);
|
||||
|
||||
let testrun = match ns_api_client.request_ports_check_testrun().await {
|
||||
Ok(Some(testrun)) => testrun,
|
||||
Ok(None) => {
|
||||
tracing::info!("No ports-check testruns available from primary server");
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to contact primary server: {err}");
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
let testrun_id = testrun.assignment.testrun_id;
|
||||
let testrun_assigned_at = testrun.assignment.assigned_at_utc;
|
||||
let gateway_identity_key = testrun.assignment.gateway_identity_key.clone();
|
||||
let gateway_identity_pubkey = common::parse_gateway_pubkey(&gateway_identity_key)?;
|
||||
|
||||
tracing::info!(
|
||||
"Received ports-check testrun {testrun_id} for gateway {gateway_identity_key} from primary",
|
||||
);
|
||||
|
||||
let network = nym_sdk::NymNetworkDetails::new_from_env();
|
||||
|
||||
// Force full exit policy list for this job kind
|
||||
netstack_args.port_check_ports = nym_gateway_probe::config::EXIT_POLICY_PORTS.to_vec();
|
||||
|
||||
let run_ports_config = RunPortsConfig {
|
||||
min_gateway_mixnet_performance,
|
||||
ignore_egress_epoch_role,
|
||||
netstack_args,
|
||||
};
|
||||
|
||||
let credentials_args = common::credential_args_from(testrun.ticket_materials);
|
||||
|
||||
log_capture.start();
|
||||
let probe_future = nym_gateway_probe::Probe::run_ports_for_agent(
|
||||
gateway_identity_pubkey,
|
||||
network,
|
||||
&run_ports_config,
|
||||
credentials_args,
|
||||
);
|
||||
let port_check_result_res = tokio::time::timeout(PORT_SCAN_HARD_TIMEOUT, probe_future).await;
|
||||
let probe_log = log_capture.stop_and_drain();
|
||||
|
||||
let port_check_result = match port_check_result_res {
|
||||
Ok(inner) => inner?,
|
||||
Err(_elapsed) => {
|
||||
tracing::error!(
|
||||
gateway = %gateway_identity_key,
|
||||
testrun = testrun_id,
|
||||
timeout_secs = PORT_SCAN_HARD_TIMEOUT.as_secs(),
|
||||
"Port scan exceeded hard timeout; aborting to free resources"
|
||||
);
|
||||
return Err(anyhow::anyhow!(
|
||||
"port scan timed out after {}s",
|
||||
PORT_SCAN_HARD_TIMEOUT.as_secs()
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
submit_ports_check_results_to_servers(
|
||||
servers,
|
||||
testrun_id,
|
||||
testrun_assigned_at,
|
||||
&gateway_identity_key,
|
||||
port_check_result,
|
||||
probe_log,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, fields(gateway_id = %gateway_identity_key, testrun = testrun_id))]
|
||||
async fn submit_ports_check_results_to_servers(
|
||||
servers: &[ServerConfig],
|
||||
testrun_id: i32,
|
||||
testrun_assigned_at: i64,
|
||||
gateway_identity_key: &str,
|
||||
port_check_result: nym_gateway_probe::PortCheckResult,
|
||||
probe_log: String,
|
||||
) {
|
||||
let handles = servers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, server)| {
|
||||
let port_check_result = port_check_result.clone();
|
||||
let probe_log = probe_log.clone();
|
||||
let gateway_identity_key = gateway_identity_key.to_string();
|
||||
|
||||
async move {
|
||||
let client = common::build_client(server);
|
||||
|
||||
let result = client
|
||||
.submit_ports_check_results_with_context(
|
||||
testrun_id,
|
||||
port_check_result,
|
||||
probe_log,
|
||||
testrun_assigned_at,
|
||||
gateway_identity_key,
|
||||
)
|
||||
.await;
|
||||
|
||||
(idx, server.address.clone(), server.port, result)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let results = futures::future::join_all(handles).await;
|
||||
|
||||
for (index, server_address, server_port, result) in results {
|
||||
match result {
|
||||
Ok(()) => {
|
||||
tracing::info!(
|
||||
"✅ Successfully submitted ports-check to server[{index}] {server_address}:{server_port}",
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"❌ Failed to submit ports-check to server[{index}] {server_address}:{server_port} - {e}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
use crate::cli::ServerConfig;
|
||||
use crate::cli::common;
|
||||
use crate::log_capture::LogCapture;
|
||||
use anyhow::anyhow;
|
||||
use nym_gateway_probe::config::CredentialArgs;
|
||||
use nym_gateway_probe::types::{AttachedTicketMaterials, VersionedSerialise};
|
||||
use nym_sdk::mixnet::ed25519::PublicKey;
|
||||
use tracing::instrument;
|
||||
|
||||
pub(crate) async fn run_probe(
|
||||
@@ -11,27 +8,14 @@ pub(crate) async fn run_probe(
|
||||
probe_config: nym_gateway_probe::config::ProbeConfig,
|
||||
log_capture: LogCapture,
|
||||
) -> anyhow::Result<()> {
|
||||
if servers.is_empty() {
|
||||
anyhow::bail!("No servers configured");
|
||||
}
|
||||
|
||||
// Always use first server as primary
|
||||
let primary_server = &servers[0];
|
||||
let primary_server = common::primary(servers)?;
|
||||
tracing::info!(
|
||||
"Requesting testrun from primary server: {}:{}",
|
||||
primary_server.address,
|
||||
primary_server.port
|
||||
);
|
||||
|
||||
let auth_key = nym_crypto::asymmetric::ed25519::PrivateKey::from_bytes(
|
||||
&primary_server.auth_key.to_bytes(),
|
||||
)
|
||||
.expect("Failed to clone auth key");
|
||||
let ns_api_client = nym_node_status_client::NsApiClient::new(
|
||||
&primary_server.address,
|
||||
primary_server.port,
|
||||
auth_key,
|
||||
);
|
||||
let ns_api_client = common::build_client(primary_server);
|
||||
|
||||
let testrun = match ns_api_client.request_testrun().await {
|
||||
Ok(Some(testrun)) => testrun,
|
||||
@@ -48,8 +32,7 @@ pub(crate) async fn run_probe(
|
||||
let testrun_id = testrun.assignment.testrun_id;
|
||||
let testrun_assigned_at = testrun.assignment.assigned_at_utc;
|
||||
let gateway_identity_key = testrun.assignment.gateway_identity_key.clone();
|
||||
let gateway_identity_pubkey = PublicKey::from_base58_string(gateway_identity_key.clone())
|
||||
.map_err(|e| anyhow!("Failed to parse GW identity key: {e}"))?;
|
||||
let gateway_identity_pubkey = common::parse_gateway_pubkey(&gateway_identity_key)?;
|
||||
|
||||
tracing::info!("Received testrun {testrun_id} for gateway {gateway_identity_key} from primary",);
|
||||
|
||||
@@ -61,17 +44,13 @@ pub(crate) async fn run_probe(
|
||||
// probe constructor might modify config to suit the testing mode, so log afterwards
|
||||
tracing::info!("Using probe config:\n{:#?}", &probe.config());
|
||||
|
||||
let serialized_ticket_materials = testrun.ticket_materials.to_serialised_string();
|
||||
let credentials_args = CredentialArgs {
|
||||
ticket_materials: serialized_ticket_materials,
|
||||
ticket_materials_revision:
|
||||
<AttachedTicketMaterials as VersionedSerialise>::CURRENT_SERIALISATION_REVISION,
|
||||
};
|
||||
let credentials_args = common::credential_args_from(testrun.ticket_materials);
|
||||
|
||||
// Run the probe, capturing all tracing output
|
||||
log_capture.start();
|
||||
let probe_result = Box::pin(probe.probe_run_agent(credentials_args)).await?;
|
||||
let probe_result_res = Box::pin(probe.probe_run_agent(credentials_args)).await;
|
||||
let probe_log = log_capture.stop_and_drain();
|
||||
let probe_result = probe_result_res?;
|
||||
|
||||
// Inspect the probe output for socks5 field
|
||||
match probe_result.outcome.socks5.as_ref() {
|
||||
@@ -110,15 +89,7 @@ async fn submit_results_to_servers(
|
||||
let gateway_identity_key = gateway_identity_key.to_string();
|
||||
|
||||
async move {
|
||||
let auth_key = nym_crypto::asymmetric::ed25519::PrivateKey::from_bytes(
|
||||
&server.auth_key.to_bytes(),
|
||||
)
|
||||
.expect("Failed to clone auth key");
|
||||
let client = nym_node_status_client::NsApiClient::new(
|
||||
&server.address,
|
||||
server.port,
|
||||
auth_key,
|
||||
);
|
||||
let client = common::build_client(server);
|
||||
|
||||
let result = if idx == 0 {
|
||||
// Primary server: submit regular results without context
|
||||
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_key, serialization_revision as \"serialization_revision: u8\"\n FROM master_verification_key WHERE epoch_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "epoch_id: u32",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "serialised_key",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "serialization_revision: u8",
|
||||
"ordinal": 2,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0112296b190328a3856d1adf51aafa2525da6c0b871633aad80ad555db9cf47c"
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT EXISTS (SELECT 1 FROM registered_gateway WHERE gateway_id_bs58 = ?) AS 'exists'",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "exists",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "06e743d143fcc4be20ca2af5e99b19f15d22fff72490473587a14cdc046fda32"
|
||||
}
|
||||
+13
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n id,\n gateway_id,\n status,\n created_utc,\n ip_address,\n log,\n last_assigned_utc\n FROM testruns\n WHERE id = $1",
|
||||
"query": "SELECT\n id,\n gateway_id,\n status,\n kind,\n created_utc,\n ip_address,\n log,\n last_assigned_utc\n FROM testruns\n WHERE gateway_id = $1 AND status != 2 AND kind = $2\n ORDER BY id DESC\n LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -20,28 +20,34 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "kind",
|
||||
"type_info": "Int2"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "created_utc",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"ordinal": 5,
|
||||
"name": "ip_address",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 6,
|
||||
"name": "log",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "last_assigned_utc",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4"
|
||||
"Int4",
|
||||
"Int2"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
@@ -51,8 +57,9 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "ddf003e7e13653388f487e3adfc1aad0a285e29c797f99ec00bcccb063f76b64"
|
||||
"hash": "0c635c539930c10a9be35a12ba3f2a66aae5be1c37af2eca521bf75261cecf28"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT OR IGNORE INTO expiration_date_signatures(expiration_date, epoch_id, serialised_signatures, serialization_revision)\n VALUES (?, ?, ?, ?);\n UPDATE expiration_date_signatures\n SET\n serialised_signatures = ?,\n serialization_revision = ?\n WHERE expiration_date = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "16d10f0ac0ed9ce4239937f46df3797a6a9ee7db2aab9f1b5e55f7c13c53bcc1"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO remote_gateway_details(gateway_id_bs58, derived_aes256_gcm_siv_key, gateway_listener, fallback_listener, expiration_timestamp)\n VALUES (?, ?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1b43cc81ce6b6007ccc59172fc64c270fd5dd7f00eaab0fe82e6fe927e604294"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "DELETE FROM remote_gateway_details WHERE gateway_id_bs58 = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1da6904e72b5abb9abf75affb13af7974d7795b4cbdba234273345fe161df233"
|
||||
}
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO testruns (\n id,\n gateway_id,\n status,\n created_utc,\n last_assigned_utc,\n ip_address,\n log\n ) VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||
"query": "INSERT INTO testruns (\n id,\n gateway_id,\n status,\n kind,\n created_utc,\n last_assigned_utc,\n ip_address,\n log\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -8,6 +8,7 @@
|
||||
"Int4",
|
||||
"Int4",
|
||||
"Int4",
|
||||
"Int2",
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Varchar",
|
||||
@@ -16,5 +17,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "88a4554c2857288c314768c56648a5f1811d2053582380ca602335a122cef8db"
|
||||
"hash": "1dc7d1e6c2173cd1ca70d699124c2b71a1a91ef114ffdd0572a738b577fab15d"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "DELETE FROM reply_key;",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1f4fde2cafa3b5fae95ab5fb653b5905d4dd6b7ac0e20f1cf100a51a1a40b35d"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE status SET previous_flush = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1fa61c23a1504de280d67c7943282b4dc00ba6a580de1faf83b137365f44b36d"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO ecash_deposit_usage (deposit_id, ticketbooks_requested_on, client_pubkey, request_uuid)\n VALUES (?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1fc72f8ba24039548047e1766c9105614dea7fd301f0ec38bfe85bfe546dad40"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE remote_gateway_details SET gateway_listener = ?, fallback_listener = ?, expiration_timestamp = ? WHERE gateway_id_bs58 = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "20797a3e0bb951ed77216a906d7997139172411b10c7444dea74a1438de4f343"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO reply_surb(reply_surb_sender_id, reply_surb, encoded_key_rotation) VALUES (?, ?, ?);\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "22bc9c7bcf96ec817c83c26104408cb6c3b99c6b808ba50c67b066fc3f36073b"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO ecash_ticketbook\n (serialization_revision, ticketbook_data, expiration_date, ticketbook_type, epoch_id, total_tickets, used_tickets)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "284b3ceae42f9320c30323dde47765854899103fd3c0fa670eb6809492270e02"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n DELETE FROM blinded_shares WHERE created < ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "28681fcd8e2d4326f628681b8f2a317aabce063a650be362d3a8ed83cc7c3549"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO global_expiration_date_signatures(expiration_date, epoch_id, serialised_signatures, serialization_revision)\n VALUES (?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2930ca6e3875c74acb7abb9ad889f166ad7f57681f76a1d0c7723d007c1f2c1e"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO custom_gateway_details(gateway_id_bs58, data)\n VALUES (?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2c113b37864f9fec7e64c0f8fdd38edcdf149acfd38c56a4db3bbf97bdb13210"
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n WITH oldest_queued AS (\n SELECT testruns.id\n FROM testruns\n JOIN gateways ON gateways.id = testruns.gateway_id\n WHERE testruns.status = $1\n AND testruns.kind = $4\n AND gateways.bonded = true\n AND gateways.performance > 0\n ORDER BY testruns.created_utc asc\n LIMIT 1\n FOR UPDATE OF testruns SKIP LOCKED\n )\n UPDATE testruns\n SET\n status = $3,\n last_assigned_utc = $2\n FROM oldest_queued\n WHERE testruns.id = oldest_queued.id\n RETURNING\n testruns.id,\n testruns.gateway_id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "gateway_id",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Int8",
|
||||
"Int4",
|
||||
"Int2"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "2e825cb61d34ed7b41d622743b4528594a84f560130a6956507ef8c4500e4bb9"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n DELETE FROM emergency_credential\n WHERE id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2f3c12cb0c48084b569e12ecb0315212a6f526f78258e173c96ec177988696ef"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "DELETE FROM ecash_ticketbook WHERE expiration_date <= ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "37f82c9ec26b53d01601a2d6df82038a77ec37cca9f9aef18008dcd03030c2c4"
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT error_message\n FROM blinded_shares\n WHERE id = ?;\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "error_message",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "396f40c33f0f62796eb7449d640bd97845350f4fb9f806c60b93c7cebd5e410d"
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT serialised_signatures, serialization_revision as \"serialization_revision: u8\"\n FROM global_expiration_date_signatures\n WHERE expiration_date = ? AND epoch_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "serialised_signatures",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "serialization_revision: u8",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3cc446220668fb3e02f0578104291d2a2af57656b405212af414d765b2263347"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE status SET client_in_use = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "42dfc51aea793fc60484cff6c481bbace1bcf110e048e594e8bd03fa45290732"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n DELETE FROM emergency_credential\n WHERE type = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4a4f3b32b313f7fbc6eb579659e7cec1442967e53764b83ba0a66cd9a72494f9"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "DELETE FROM custom_gateway_details WHERE gateway_id_bs58 = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4f78619aca933484cd67cb89a376b2a5bec1c191993ff58f0c71c03e3ef6d92d"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n DELETE FROM partial_blinded_wallet_failure WHERE created < ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "52b378e282d93db941eff53b5b311e5732ece0bf84ea98f2328b20add8f2b5ef"
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT * FROM custom_gateway_details WHERE gateway_id_bs58 = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "gateway_id_bs58",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "data",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "54f552a9dbe95236f946ac2b6615e03504afa58e345ae16a128629d8e76f0a11"
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT gateway_id_bs58 FROM registered_gateway",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "gateway_id_bs58",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5661cf1ad8bd5ca062e855e1971a8787133ee41814bd3efdd501f9ee0c050f2b"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "DELETE FROM pending_issuance WHERE deposit_id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5c5d4bfabf18bc6fa56e76a9b98e38b7f6ceb8e9191a7b9201922efcf6b07966"
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT serialised_signatures, serialization_revision as \"serialization_revision: u8\"\n FROM expiration_date_signatures\n WHERE expiration_date = ? AND epoch_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "serialised_signatures",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "serialization_revision: u8",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5d3b8ad051ab6f46c702308c2fc751a5ca340ac9c6dd86da1a5e9a3e65ea589f"
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT name FROM sqlite_master WHERE type='table' AND name='status'",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5e51396c534409a4b55c08170e00fd083e87cc9a18d798b2cf8d6774224aebed"
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n WITH oldest_queued AS (\n SELECT testruns.id\n FROM testruns\n JOIN gateways ON gateways.id = testruns.gateway_id\n WHERE testruns.status = $1\n AND gateways.bonded = true\n AND gateways.performance > 0\n ORDER BY testruns.created_utc asc\n LIMIT 1\n FOR UPDATE OF testruns SKIP LOCKED\n )\n UPDATE testruns\n SET\n status = $3,\n last_assigned_utc = $2\n FROM oldest_queued\n WHERE testruns.id = oldest_queued.id\n RETURNING\n testruns.id,\n testruns.gateway_id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "gateway_id",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Int8",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "62be5c3a94e6b2e72823e0fe6d26b5ac23f7f7eabc6b2ba13ccfb8ef14fcfd30"
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT flush_in_progress FROM status;",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "flush_in_progress",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "62f471858c831ac762a0ecf60876067dbf82d0dc376ad3f0df835b77dfcc3c37"
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT min_reply_surb_threshold as \"min_reply_surb_threshold: u32\", max_reply_surb_threshold as \"max_reply_surb_threshold: u32\" FROM reply_surb_storage_metadata;\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "min_reply_surb_threshold: u32",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "max_reply_surb_threshold: u32",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "648fff18c4016fb356c164f813e0d5ebd37d62612b65b8ac4c9eb5d7b67a8884"
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO testruns (gateway_id, status, kind, created_utc, last_assigned_utc, ip_address, log)\n SELECT\n gw.id,\n $1,\n $2,\n $3,\n NULL,\n 'ports_check_scheduler',\n ''\n FROM gateways gw\n WHERE gw.bonded = true\n AND (gw.last_ports_check_utc IS NULL OR gw.last_ports_check_utc < $4)\n AND NOT EXISTS (\n SELECT 1\n FROM testruns t\n WHERE t.gateway_id = gw.id\n AND t.kind = $2\n AND t.status IN ($1, $5)\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Int2",
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "65b5a3ed9a73f1304badfa8d9bba57fb24f6b21655039a9bfa21c44409c87910"
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT client_in_use FROM status;",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "client_in_use",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "67db5e91bcdc831e0d1659eda358d18a0c6792d7a4244a65243d4b22d578a1e5"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO master_verification_key(epoch_id, serialised_key, serialization_revision) VALUES (?, ?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "70d8f240ad6edda6b8c7f2e800e7fca89d80869484f2f3c66cabb898f0298c62"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO registered_gateway(gateway_id_bs58, registration_timestamp, gateway_type)\n VALUES (?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "727598e516090da6d26e36d09062b60ccb76d6468f359891428c0bfb96ddd7ef"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE status SET flush_in_progress = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "74d77a3e15f49bde7b8b17dd638de04e5ece789fb0b0cd27ad09858fbf5c5e27"
|
||||
}
|
||||
+11
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n id as \"id!\",\n gateway_id as \"gateway_id!\",\n status as \"status!\",\n created_utc as \"created_utc!\",\n ip_address as \"ip_address!\",\n log as \"log!\",\n last_assigned_utc\n FROM testruns\n WHERE\n id = $1\n AND\n status = $2\n ORDER BY created_utc\n LIMIT 1",
|
||||
"query": "SELECT\n id as \"id!\",\n gateway_id as \"gateway_id!\",\n status as \"status!\",\n kind as \"kind!\",\n created_utc as \"created_utc!\",\n ip_address as \"ip_address!\",\n log as \"log!\",\n last_assigned_utc\n FROM testruns\n WHERE\n id = $1\n AND\n status = $2\n ORDER BY created_utc\n LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -20,21 +20,26 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "kind!",
|
||||
"type_info": "Int2"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "created_utc!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"ordinal": 5,
|
||||
"name": "ip_address!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 6,
|
||||
"name": "log!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "last_assigned_utc",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
@@ -52,8 +57,9 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d41cafc76cb49c03df7452c405a4e2e5e3951c41dc35c20261c1d959c0d6403f"
|
||||
"hash": "75bdccbff8ea6a59d56230075163783249a03f2ae04ec89f734e67ceb897a385"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE active_gateway SET active_gateway_id_bs58 = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "80476cf2906eb0ecf7f66c16bc5682169b87f488b6927fa67fade6bf5abf7582"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO emergency_credential\n (type, content, expiration)\n VALUES (?, ?, ?)\n ON CONFLICT(type, content) DO NOTHING;\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "805ad4f26e0234d7f482a263e186156311713d2e9f69d39c868cd16296b56326"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO pending_issuance\n (deposit_id, serialization_revision, pending_ticketbook_data, expiration_date)\n VALUES (?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "81a12a8a419c88b1c28a5533fde4d63462e9ea0049e2edafea1dc3f8476b33e4"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE ecash_ticketbook SET used_tickets = used_tickets + ? WHERE id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "84cad8b1078a4000830835e6349de3eb76fed954b7336530401db72cd008aff3"
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT reply_surb_sender_id, reply_surb, encoded_key_rotation as \"encoded_key_rotation: u8\" FROM reply_surb\n WHERE reply_surb_sender_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "reply_surb_sender_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "reply_surb",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "encoded_key_rotation: u8",
|
||||
"ordinal": 2,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "85d542a8cb2e1fbba6142729056b57e6ea8685e9473a3a8bd635552810493a58"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO partial_blinded_wallet_failure(corresponding_deposit, epoch_id, expiration_date, node_id, created, failure_message)\n VALUES (?, ?, ?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 6
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "97d97ebb6bc8f4114fdea9ebc9f57f91a11f5057273cb70bd0e629712d17dd41"
|
||||
}
|
||||
+21
-9
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n gw.gateway_identity_key as \"gateway_identity_key!\",\n gw.bonded as \"bonded: bool\",\n gw.performance as \"performance!\",\n gw.self_described as \"self_described?\",\n gw.explorer_pretty_bond as \"explorer_pretty_bond?\",\n gw.last_probe_result as \"last_probe_result?\",\n gw.last_probe_log as \"last_probe_log?\",\n gw.last_testrun_utc as \"last_testrun_utc?\",\n gw.last_updated_utc as \"last_updated_utc!\",\n gw.bridges as \"bridges?: serde_json::Value\",\n COALESCE(gd.moniker, 'NA') as \"moniker!\",\n COALESCE(gd.website, 'NA') as \"website!\",\n COALESCE(gd.security_contact, 'NA') as \"security_contact!\",\n COALESCE(gd.details, 'NA') as \"details!\"\n FROM gateways gw\n LEFT JOIN gateway_description gd\n ON gw.gateway_identity_key = gd.gateway_identity_key\n ORDER BY gw.gateway_identity_key",
|
||||
"query": "SELECT\n gw.gateway_identity_key as \"gateway_identity_key!\",\n gw.bonded as \"bonded: bool\",\n gw.performance as \"performance!\",\n gw.self_described as \"self_described?\",\n gw.explorer_pretty_bond as \"explorer_pretty_bond?\",\n gw.last_probe_result as \"last_probe_result?\",\n gw.last_probe_log as \"last_probe_log?\",\n gw.ports_check as \"ports_check?\",\n gw.last_ports_check_utc as \"last_ports_check_utc?\",\n gw.last_testrun_utc as \"last_testrun_utc?\",\n gw.last_updated_utc as \"last_updated_utc!\",\n gw.bridges as \"bridges?: serde_json::Value\",\n COALESCE(gd.moniker, 'NA') as \"moniker!\",\n COALESCE(gd.website, 'NA') as \"website!\",\n COALESCE(gd.security_contact, 'NA') as \"security_contact!\",\n COALESCE(gd.details, 'NA') as \"details!\"\n FROM gateways gw\n LEFT JOIN gateway_description gd\n ON gw.gateway_identity_key = gd.gateway_identity_key\n ORDER BY gw.gateway_identity_key",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -40,36 +40,46 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "last_testrun_utc?",
|
||||
"type_info": "Int8"
|
||||
"name": "ports_check?",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "last_updated_utc!",
|
||||
"name": "last_ports_check_utc?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "last_testrun_utc?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "last_updated_utc!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "bridges?: serde_json::Value",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 12,
|
||||
"name": "moniker!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 13,
|
||||
"name": "website!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 14,
|
||||
"name": "security_contact!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"ordinal": 15,
|
||||
"name": "details!",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
@@ -86,6 +96,8 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
@@ -94,5 +106,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "1668f5a5a0abc8a73454953c3f5b61d2afb1b37720f5756b9c6fb3aef55a3027"
|
||||
"hash": "9c077a2322ad141cfae4c81d1046671cd0c8c28b3f1ed049f008d14b711ef788"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO status(flush_in_progress, previous_flush, client_in_use) VALUES (0, 0, 1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a5a71e480118639397f7464abb09db66d1dc1307c0873627b39cb22a245b5a8c"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT OR IGNORE INTO master_verification_key(epoch_id, serialised_key, serialization_revision) VALUES (?, ?, ?);\n UPDATE master_verification_key\n SET\n serialised_key = ?,\n serialization_revision = ?\n WHERE epoch_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 6
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a5b18e66d77ff802e274623605e15dcfcffb502ba8398caefd56c481f44eb84e"
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_signatures, serialization_revision as \"serialization_revision: u8\"\n FROM global_coin_index_signatures WHERE epoch_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "epoch_id: u32",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "serialised_signatures",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "serialization_revision: u8",
|
||||
"ordinal": 2,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a8b7ce0fe4755c28b96d1e503e313ab15fed747fb0cee1c9f949fb58461b3f79"
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n id,\n gateway_identity_key\n FROM gateways\n WHERE id = $1\n LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "gateway_identity_key",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b0c588db5f7be90c6d4cb1b55c3a8ed4e9a64884ac5619e0abdecb04f2d13a74"
|
||||
}
|
||||
+11
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n id,\n gateway_id,\n status,\n created_utc,\n ip_address,\n log,\n last_assigned_utc\n FROM testruns\n WHERE gateway_id = $1 AND status != 2\n ORDER BY id DESC\n LIMIT 1",
|
||||
"query": "SELECT\n id,\n gateway_id,\n status,\n kind,\n created_utc,\n ip_address,\n log,\n last_assigned_utc\n FROM testruns\n WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -20,21 +20,26 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "kind",
|
||||
"type_info": "Int2"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "created_utc",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"ordinal": 5,
|
||||
"name": "ip_address",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 6,
|
||||
"name": "log",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "last_assigned_utc",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
@@ -51,8 +56,9 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "f343df183767af9815847cb94ccbd484010a7346de03f1e0959a09a964344de8"
|
||||
"hash": "b32d29ca3cedae93fa2fcd44cc301614edc4e967f348c57f485f736b8f2f03e8"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO reply_surb_storage_metadata(min_reply_surb_threshold, max_reply_surb_threshold)\n VALUES (?, ?);\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b6ce77fcffda2ee24ba181213e04adec312abffa604fa805cc362a2c206107b4"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n DELETE FROM partial_blinded_wallet WHERE created < ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b8257a0832d0124f0a8aaaf81dc6a811c593aea8febf1f891117e5e84213f147"
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_signatures, serialization_revision as \"serialization_revision: u8\"\n FROM coin_indices_signatures WHERE epoch_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "epoch_id: u32",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "serialised_signatures",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "serialization_revision: u8",
|
||||
"ordinal": 2,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ba96344db31b0f2155e2af53eaaeafc9b5f64061b6c9a829e2912945b6cffc82"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE ecash_ticketbook\n SET used_tickets = used_tickets - ?\n WHERE id = ?\n AND used_tickets = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "bc823c54143e2dc590b91347cd089dde284b38a3a4960afed758206d03ca1cf4"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT OR IGNORE INTO coin_indices_signatures(epoch_id, serialised_signatures, serialization_revision) VALUES (?, ?, ?);\n UPDATE coin_indices_signatures\n SET\n serialised_signatures = ?,\n serialization_revision = ?\n WHERE epoch_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 6
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "bd1973696121b6128bd75ae80fab253c071e04eb853d4b0f3b21782ea57c2f68"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO reply_surb_sender(tag, last_sent) VALUES (?, ?);\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "bdaf8e8dd711ff420807659456f5ebe0222a2a0ba96f28e3a3aa58fdc4b689db"
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT active_gateway_id_bs58 FROM active_gateway",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "active_gateway_id_bs58",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "bf249752f08c283bf5942b6ff48125c24750b523cfcad1e5e9069dbf7050e2a1"
|
||||
}
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n t1.node_id as \"node_id!\",\n t1.blinded_signature as \"blinded_signature!\",\n t1.epoch_id as \"epoch_id!\",\n t1.expiration_date as \"expiration_date!: Date\"\n FROM partial_blinded_wallet as t1\n JOIN ecash_deposit_usage as t2\n on t1.corresponding_deposit = t2.deposit_id\n JOIN blinded_shares as t3\n ON t2.request_uuid = t3.request_uuid\n WHERE t3.device_id = ? AND t3.credential_id = ?;\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "node_id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "blinded_signature!",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "epoch_id!",
|
||||
"ordinal": 2,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "expiration_date!: Date",
|
||||
"ordinal": 3,
|
||||
"type_info": "Date"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "c2b841762bdb963fff337ef5c8ec9f560017b4da6b0303ea0397d9568229e167"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO global_coin_index_signatures(epoch_id, serialised_signatures, serialization_revision) VALUES (?, ?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d3510846941fa2525926b9bfbcdabd806877ce914b514d4f7cd6be318c4debe6"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "DELETE FROM reply_surb_sender;",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "daa9ffcbe13178d773e538e96971da2809ec4bd66ad49d2e3b8d67b741835475"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO partial_blinded_wallet(corresponding_deposit, epoch_id, expiration_date, node_id, created, blinded_signature)\n VALUES (?, ?, ?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 6
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "db176e98198fe594d88eb860d918f633a94d18a19b7f0f96935a62560def7d0f"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(id) as \"count!: i64\" FROM testruns WHERE status = $1 AND kind = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count!: i64",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Int2"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "decdc7c0e6d279a137a80797cbabcb8900bca7157d3c66c5664386ca2c26e96a"
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE gateways SET last_probe_result = $1 WHERE id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "dfa035c968534926736adf0e5359cde3f6f6689a80299b5b6c1d7bd048965e6e"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "DELETE FROM reply_surb;",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e29e0c82d034626f9b369348b8e35677b9d9a1b29e4d89437134e92967fa98d1"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE ecash_deposit_usage\n SET ticketbook_request_error = ?\n WHERE deposit_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e584253e3856355899537eb8fc152f2bfed2d918b894ec0f588e38dd5e8ad726"
|
||||
}
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT t1.node_id, t1.blinded_signature, t1.epoch_id, t1.expiration_date as \"expiration_date!: Date\"\n FROM partial_blinded_wallet as t1\n JOIN ecash_deposit_usage as t2\n on t1.corresponding_deposit = t2.deposit_id\n JOIN blinded_shares as t3\n ON t2.request_uuid = t3.request_uuid\n WHERE t3.id = ?;\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "node_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "blinded_signature",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "epoch_id",
|
||||
"ordinal": 2,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "expiration_date!: Date",
|
||||
"ordinal": 3,
|
||||
"type_info": "Date"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e77ffab19b099b84470fe5611716a2e314787586a46cffd074abb67f2f4d109e"
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT previous_flush AS \"previous_flush: OffsetDateTime\" FROM status",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "previous_flush: OffsetDateTime",
|
||||
"ordinal": 0,
|
||||
"type_info": "Null"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e8717ef6523ef9bcf37a0e81570ac000904bb7fe3934786d652adc80fde78add"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO reply_key(key_digest, reply_key, sent_at) VALUES (?, ?, ?);\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "eded014a5fffbd9c359f8557bcd3d41724666bf92357bf5faf42120a2aab131c"
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT error_message\n FROM blinded_shares\n WHERE device_id = ? AND credential_id = ?;\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "error_message",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "ef60c2683211cc4ec2d3e46392518a1f62fa67dfe8f130deb876ebee11bf1602"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "DELETE FROM registered_gateway WHERE gateway_id_bs58 = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f3ebe259e26c05ecdd33bd9085dbb91cd5046a8c9d4434cf085a4fa2ebf03e93"
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-node-status-api"
|
||||
version = "4.6.2-rc10"
|
||||
version = "4.6.2"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -39,6 +39,19 @@ test-db-wait: ## Wait for the PostgreSQL database to be healthy
|
||||
sleep 1; \
|
||||
done; \
|
||||
echo " Database is healthy!"
|
||||
@echo "Waiting for host port to be ready..."
|
||||
@for i in $$(seq 1 30); do \
|
||||
if pg_isready -h localhost -p 5433 -U testuser -d nym_node_status_api_test >/dev/null 2>&1; then \
|
||||
echo " Host port is ready!"; \
|
||||
break; \
|
||||
fi; \
|
||||
if [ $$i -eq 30 ]; then \
|
||||
echo " Timed out waiting for host port"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
echo -n "."; \
|
||||
sleep 1; \
|
||||
done
|
||||
|
||||
.PHONY: test-db-down
|
||||
test-db-down: ## Stop and remove the test database
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE gateways
|
||||
ADD COLUMN IF NOT EXISTS last_ports_check_utc BIGINT;
|
||||
|
||||
ALTER TABLE testruns
|
||||
ADD COLUMN IF NOT EXISTS kind SMALLINT NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_testruns_kind_status_created
|
||||
ON testruns (kind, status, created_utc);
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
ALTER TABLE gateways ADD COLUMN IF NOT EXISTS ports_check JSONB;
|
||||
|
||||
UPDATE gateways
|
||||
SET ports_check = (last_probe_result::jsonb -> 'ports_check')
|
||||
WHERE last_probe_result IS NOT NULL
|
||||
AND btrim(last_probe_result) <> ''
|
||||
AND last_probe_result ~ '^[\[{]'
|
||||
AND last_probe_result::jsonb ? 'ports_check'
|
||||
AND ports_check IS NULL;
|
||||
|
||||
UPDATE gateways
|
||||
SET last_probe_result = (last_probe_result::jsonb - 'ports_check')::text
|
||||
WHERE last_probe_result IS NOT NULL
|
||||
AND btrim(last_probe_result) <> ''
|
||||
AND last_probe_result ~ '^[\[{]'
|
||||
AND last_probe_result::jsonb ? 'ports_check';
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
UPDATE gateways
|
||||
SET ports_check = (last_probe_result::jsonb -> 'ports_check')
|
||||
WHERE last_probe_result IS NOT NULL
|
||||
AND btrim(last_probe_result) <> ''
|
||||
AND last_probe_result ~ '^[\[{]'
|
||||
AND last_probe_result::jsonb ? 'ports_check'
|
||||
AND ports_check IS NULL;
|
||||
|
||||
UPDATE gateways
|
||||
SET last_probe_result = (last_probe_result::jsonb - 'ports_check')::text
|
||||
WHERE last_probe_result IS NOT NULL
|
||||
AND btrim(last_probe_result) <> ''
|
||||
AND last_probe_result ~ '^[\[{]'
|
||||
AND last_probe_result::jsonb ? 'ports_check';
|
||||
@@ -82,6 +82,15 @@ pub(crate) struct Cli {
|
||||
#[arg(value_parser = parse_duration_std)]
|
||||
pub(crate) testruns_refresh_interval: Duration,
|
||||
|
||||
/// Safety net for stale in-progress testruns (whole seconds, es. 7200).
|
||||
#[clap(
|
||||
long,
|
||||
default_value = "7200",
|
||||
env = "NODE_STATUS_API_TESTRUN_STALE_IN_PROGRESS"
|
||||
)]
|
||||
#[arg(value_parser = parse_duration_std)]
|
||||
pub(crate) testruns_stale_in_progress: Duration,
|
||||
|
||||
#[clap(long, default_value = "86400", env = "NODE_STATUS_API_GEODATA_TTL")]
|
||||
#[arg(value_parser = parse_duration_std)]
|
||||
pub(crate) geodata_ttl: Duration,
|
||||
|
||||
@@ -18,6 +18,103 @@ use strum_macros::{EnumString, FromRepr};
|
||||
use time::{Date, OffsetDateTime, UtcDateTime};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
fn build_ports_check_summary_json(
|
||||
can_register: bool,
|
||||
port_check_target: Option<String>,
|
||||
ports: Option<&serde_json::Map<String, serde_json::Value>>,
|
||||
error: Option<String>,
|
||||
) -> serde_json::Value {
|
||||
let failed_ports = ports
|
||||
.map(|ports| {
|
||||
ports
|
||||
.iter()
|
||||
.filter_map(|(port, open)| open.as_bool().filter(|is_open| !is_open).map(|_| port))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let has_ports = ports.is_some_and(|p| !p.is_empty());
|
||||
let all_pass = can_register && failed_ports.is_empty() && has_ports;
|
||||
|
||||
serde_json::json!({
|
||||
"all_pass": all_pass,
|
||||
"error": error,
|
||||
"port_check_target": port_check_target,
|
||||
"failed_ports": failed_ports,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn ports_check_summary_json_from_result(
|
||||
result: &nym_gateway_probe::PortCheckResult,
|
||||
) -> serde_json::Value {
|
||||
let ports = result
|
||||
.ports
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), serde_json::Value::Bool(*v)))
|
||||
.collect::<serde_json::Map<_, _>>();
|
||||
|
||||
build_ports_check_summary_json(
|
||||
result.can_register,
|
||||
Some(result.port_check_target.clone()),
|
||||
Some(&ports),
|
||||
result.error.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_ports_check_payload(value: serde_json::Value) -> Option<serde_json::Value> {
|
||||
let serde_json::Value::Object(map) = value else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// New shape is already in place; pass through untouched.
|
||||
if map.contains_key("all_pass")
|
||||
&& map.contains_key("error")
|
||||
&& map.contains_key("port_check_target")
|
||||
&& map.contains_key("failed_ports")
|
||||
{
|
||||
return Some(serde_json::Value::Object(map));
|
||||
}
|
||||
|
||||
// Legacy dedicated shape: { gateway, can_register, port_check_target, ports, error }
|
||||
if map.contains_key("can_register") && map.contains_key("ports") {
|
||||
let can_register = map
|
||||
.get("can_register")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
let port_check_target = map
|
||||
.get("port_check_target")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(ToOwned::to_owned);
|
||||
let ports = map.get("ports").and_then(serde_json::Value::as_object);
|
||||
let error = map
|
||||
.get("error")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(ToOwned::to_owned);
|
||||
|
||||
return Some(build_ports_check_summary_json(
|
||||
can_register,
|
||||
port_check_target,
|
||||
ports,
|
||||
error,
|
||||
));
|
||||
}
|
||||
|
||||
if map.contains_key("all_pass") && map.contains_key("failed_ports") {
|
||||
let mut normalized = map;
|
||||
normalized.remove("ports_tested");
|
||||
normalized
|
||||
.entry("port_check_target".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
normalized
|
||||
.entry("error".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
return Some(serde_json::Value::Object(normalized));
|
||||
}
|
||||
|
||||
Some(serde_json::Value::Object(map))
|
||||
}
|
||||
|
||||
macro_rules! serialize_opt_to_value {
|
||||
($var:expr) => {{
|
||||
match $var {
|
||||
@@ -48,6 +145,8 @@ pub(crate) struct GatewayDto {
|
||||
pub(crate) explorer_pretty_bond: Option<String>,
|
||||
pub(crate) last_probe_result: Option<String>,
|
||||
pub(crate) last_probe_log: Option<String>,
|
||||
pub(crate) ports_check: Option<serde_json::Value>,
|
||||
pub(crate) last_ports_check_utc: Option<i64>,
|
||||
pub(crate) last_testrun_utc: Option<i64>,
|
||||
pub(crate) last_updated_utc: i64,
|
||||
pub(crate) moniker: String,
|
||||
@@ -73,7 +172,7 @@ impl TryFrom<GatewayDto> for http::models::Gateway {
|
||||
.explorer_pretty_bond
|
||||
.clone()
|
||||
.unwrap_or("null".to_string());
|
||||
let last_probe_result = value
|
||||
let last_probe_result_raw = value
|
||||
.last_probe_result
|
||||
.clone()
|
||||
.unwrap_or("null".to_string());
|
||||
@@ -81,7 +180,18 @@ impl TryFrom<GatewayDto> for http::models::Gateway {
|
||||
|
||||
let self_described = serde_json::from_str(&self_described).unwrap_or(None);
|
||||
let explorer_pretty_bond = serde_json::from_str(&explorer_pretty_bond).unwrap_or(None);
|
||||
let last_probe_result = serde_json::from_str(&last_probe_result).unwrap_or(None);
|
||||
let last_probe_result = serde_json::from_str::<serde_json::Value>(&last_probe_result_raw)
|
||||
.ok()
|
||||
.and_then(|v| (!v.is_null()).then_some(v));
|
||||
|
||||
let ports_check = value
|
||||
.ports_check
|
||||
.clone()
|
||||
.filter(|v| !v.is_null())
|
||||
.and_then(normalize_ports_check_payload);
|
||||
let last_ports_check_utc = value
|
||||
.last_ports_check_utc
|
||||
.map(unix_timestamp_to_utc_rfc3339);
|
||||
|
||||
let bonded = value.bonded;
|
||||
let performance = value.performance as u8;
|
||||
@@ -104,6 +214,8 @@ impl TryFrom<GatewayDto> for http::models::Gateway {
|
||||
description,
|
||||
last_probe_result,
|
||||
last_probe_log,
|
||||
ports_check,
|
||||
last_ports_check_utc,
|
||||
routing_score,
|
||||
config_score,
|
||||
last_testrun_utc,
|
||||
@@ -292,6 +404,7 @@ pub struct TestRunDto {
|
||||
pub id: i32,
|
||||
pub gateway_id: i32,
|
||||
pub status: i32,
|
||||
pub kind: i16,
|
||||
pub created_utc: i64,
|
||||
pub ip_address: String,
|
||||
pub log: String,
|
||||
@@ -306,6 +419,13 @@ pub(crate) enum TestRunStatus {
|
||||
Queued = 0,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, strum_macros::Display, EnumString, FromRepr, PartialEq, Eq)]
|
||||
#[repr(i16)]
|
||||
pub(crate) enum TestRunKind {
|
||||
Probe = 0,
|
||||
PortsCheck = 1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GatewayIdentityDto {
|
||||
pub gateway_identity_key: String,
|
||||
|
||||
@@ -87,6 +87,8 @@ impl Storage {
|
||||
gw.explorer_pretty_bond as "explorer_pretty_bond?",
|
||||
gw.last_probe_result as "last_probe_result?",
|
||||
gw.last_probe_log as "last_probe_log?",
|
||||
gw.ports_check as "ports_check?",
|
||||
gw.last_ports_check_utc as "last_ports_check_utc?",
|
||||
gw.last_testrun_utc as "last_testrun_utc?",
|
||||
gw.last_updated_utc as "last_updated_utc!",
|
||||
gw.bridges as "bridges?: serde_json::Value",
|
||||
@@ -102,14 +104,22 @@ impl Storage {
|
||||
.fetch(&self.pool)
|
||||
.try_collect::<Vec<_>>()
|
||||
.await?;
|
||||
|
||||
let items: Vec<Gateway> = items
|
||||
.into_iter()
|
||||
.map(|item| item.try_into())
|
||||
.collect::<anyhow::Result<Vec<_>>>()
|
||||
.inspect_err(|e| error!("Conversion from DTO failed: {e}. Invalidly stored data?"))?;
|
||||
tracing::trace!("Fetched {} gateways from DB", items.len());
|
||||
Ok(items)
|
||||
let mut gateways: Vec<Gateway> = Vec::with_capacity(items.len());
|
||||
let mut failed = 0usize;
|
||||
for item in items {
|
||||
match item.try_into() {
|
||||
Ok(gw) => gateways.push(gw),
|
||||
Err(e) => {
|
||||
error!("Conversion from DTO failed: {e}. Invalidly stored data?");
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if failed > 0 {
|
||||
tracing::warn!("{failed} gateway DTO(s) failed conversion and were skipped");
|
||||
}
|
||||
tracing::trace!("Fetched {} gateways from DB", gateways.len());
|
||||
Ok(gateways)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use crate::db::DbConnection;
|
||||
use crate::db::DbPool;
|
||||
use crate::db::models::{TestRunDto, TestRunStatus};
|
||||
use crate::db::models::{TestRunDto, TestRunKind, TestRunStatus};
|
||||
use crate::http::models::TestrunAssignment;
|
||||
use crate::utils::now_utc;
|
||||
use sqlx::Row;
|
||||
use sqlx::types::Json;
|
||||
use time::Duration;
|
||||
|
||||
pub(crate) async fn count_testruns_in_progress(
|
||||
@@ -22,6 +24,20 @@ pub(crate) async fn count_testruns_in_progress(
|
||||
.map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
pub(crate) async fn count_testruns_in_progress_by_kind(
|
||||
conn: &mut DbConnection,
|
||||
kind: TestRunKind,
|
||||
) -> anyhow::Result<i64> {
|
||||
sqlx::query_scalar!(
|
||||
r#"SELECT COUNT(id) as "count!: i64" FROM testruns WHERE status = $1 AND kind = $2"#,
|
||||
TestRunStatus::InProgress as i64,
|
||||
kind as i16,
|
||||
)
|
||||
.fetch_one(conn.as_mut())
|
||||
.await
|
||||
.map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_in_progress_testrun_by_id(
|
||||
conn: &mut DbConnection,
|
||||
testrun_id: i32,
|
||||
@@ -32,6 +48,7 @@ pub(crate) async fn get_in_progress_testrun_by_id(
|
||||
id as "id!",
|
||||
gateway_id as "gateway_id!",
|
||||
status as "status!",
|
||||
kind as "kind!",
|
||||
created_utc as "created_utc!",
|
||||
ip_address as "ip_address!",
|
||||
log as "log!",
|
||||
@@ -90,6 +107,19 @@ pub(crate) async fn update_testruns_assigned_before(
|
||||
|
||||
pub(crate) async fn assign_oldest_testrun(
|
||||
conn: &mut DbConnection,
|
||||
) -> anyhow::Result<Option<TestrunAssignment>> {
|
||||
assign_oldest_testrun_by_kind(conn, TestRunKind::Probe).await
|
||||
}
|
||||
|
||||
pub(crate) async fn assign_oldest_ports_check_testrun(
|
||||
conn: &mut DbConnection,
|
||||
) -> anyhow::Result<Option<TestrunAssignment>> {
|
||||
assign_oldest_testrun_by_kind(conn, TestRunKind::PortsCheck).await
|
||||
}
|
||||
|
||||
async fn assign_oldest_testrun_by_kind(
|
||||
conn: &mut DbConnection,
|
||||
kind: TestRunKind,
|
||||
) -> anyhow::Result<Option<TestrunAssignment>> {
|
||||
let now = now_utc().unix_timestamp();
|
||||
// find & mark as "In progress" in the same transaction to avoid race conditions
|
||||
@@ -101,6 +131,7 @@ pub(crate) async fn assign_oldest_testrun(
|
||||
FROM testruns
|
||||
JOIN gateways ON gateways.id = testruns.gateway_id
|
||||
WHERE testruns.status = $1
|
||||
AND testruns.kind = $4
|
||||
AND gateways.bonded = true
|
||||
AND gateways.performance > 0
|
||||
ORDER BY testruns.created_utc asc
|
||||
@@ -120,28 +151,27 @@ pub(crate) async fn assign_oldest_testrun(
|
||||
TestRunStatus::Queued as i32,
|
||||
now,
|
||||
TestRunStatus::InProgress as i32,
|
||||
kind as i16,
|
||||
)
|
||||
.fetch_optional(conn.as_mut())
|
||||
.await?;
|
||||
|
||||
if let Some(testrun) = returning {
|
||||
let gw_identity = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
gateway_identity_key
|
||||
FROM gateways
|
||||
WHERE id = $1
|
||||
LIMIT 1"#,
|
||||
testrun.gateway_id
|
||||
let row = sqlx::query(
|
||||
r#"SELECT gateway_identity_key, last_ports_check_utc FROM gateways WHERE id = $1 LIMIT 1"#,
|
||||
)
|
||||
.bind(testrun.gateway_id)
|
||||
.fetch_one(conn.as_mut())
|
||||
.await?;
|
||||
|
||||
let gateway_identity_key: String = row.try_get("gateway_identity_key")?;
|
||||
let last_ports_check_utc: Option<i64> = row.try_get("last_ports_check_utc")?;
|
||||
|
||||
Ok(Some(TestrunAssignment {
|
||||
testrun_id: testrun.id,
|
||||
gateway_identity_key: gw_identity.gateway_identity_key,
|
||||
gateway_identity_key,
|
||||
assigned_at_utc: now,
|
||||
last_ports_check_utc,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
@@ -186,17 +216,121 @@ pub(crate) async fn update_gateway_last_probe_result(
|
||||
gateway_pk: i32,
|
||||
result: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
sqlx::query!(
|
||||
"UPDATE gateways SET last_probe_result = $1 WHERE id = $2",
|
||||
result,
|
||||
gateway_pk,
|
||||
sqlx::query("UPDATE gateways SET last_probe_result = $1 WHERE id = $2")
|
||||
.bind(result)
|
||||
.bind(gateway_pk)
|
||||
.execute(conn.as_mut())
|
||||
.await
|
||||
.map(drop)
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
// NOTE: port-check submissions must not re-embed `ports_check` into `last_probe_result`.
|
||||
|
||||
pub(crate) async fn update_gateway_ports_check_only(
|
||||
conn: &mut DbConnection,
|
||||
gateway_pk: i32,
|
||||
port_check_result: &nym_gateway_probe::PortCheckResult,
|
||||
) -> anyhow::Result<()> {
|
||||
use crate::db::models::ports_check_summary_json_from_result;
|
||||
|
||||
let now_ts = now_utc().unix_timestamp();
|
||||
let value = ports_check_summary_json_from_result(port_check_result);
|
||||
|
||||
sqlx::query(
|
||||
r#"UPDATE gateways SET
|
||||
ports_check = $1,
|
||||
last_ports_check_utc = $2
|
||||
WHERE id = $3"#,
|
||||
)
|
||||
.bind(Json(value))
|
||||
.bind(now_ts)
|
||||
.bind(gateway_pk)
|
||||
.execute(conn.as_mut())
|
||||
.await
|
||||
.map(drop)
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
pub(crate) async fn insert_external_ports_check_testrun(
|
||||
conn: &mut DbConnection,
|
||||
testrun_id: i32,
|
||||
gateway_id: i32,
|
||||
assigned_at_utc: i64,
|
||||
) -> anyhow::Result<()> {
|
||||
let now = now_utc().unix_timestamp();
|
||||
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO testruns (
|
||||
id,
|
||||
gateway_id,
|
||||
status,
|
||||
kind,
|
||||
created_utc,
|
||||
last_assigned_utc,
|
||||
ip_address,
|
||||
log
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)"#,
|
||||
testrun_id,
|
||||
gateway_id,
|
||||
TestRunStatus::InProgress as i32,
|
||||
TestRunKind::PortsCheck as i16,
|
||||
now,
|
||||
assigned_at_utc,
|
||||
"external",
|
||||
""
|
||||
)
|
||||
.execute(conn.as_mut())
|
||||
.await?;
|
||||
|
||||
tracing::debug!(
|
||||
"Created external ports-check testrun {} for gateway {}",
|
||||
testrun_id,
|
||||
gateway_id
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn enqueue_due_ports_check_testruns(db: &DbPool) -> anyhow::Result<u64> {
|
||||
let mut conn = db.acquire().await?;
|
||||
let now = now_utc().unix_timestamp();
|
||||
// 14 days soft TTL for dedicated ports-check queueing.
|
||||
let cutoff = now - time::Duration::days(14).whole_seconds();
|
||||
|
||||
let res = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO testruns (gateway_id, status, kind, created_utc, last_assigned_utc, ip_address, log)
|
||||
SELECT
|
||||
gw.id,
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
NULL,
|
||||
'ports_check_scheduler',
|
||||
''
|
||||
FROM gateways gw
|
||||
WHERE gw.bonded = true
|
||||
AND (gw.last_ports_check_utc IS NULL OR gw.last_ports_check_utc < $4)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM testruns t
|
||||
WHERE t.gateway_id = gw.id
|
||||
AND t.kind = $2
|
||||
AND t.status IN ($1, $5)
|
||||
)
|
||||
"#,
|
||||
TestRunStatus::Queued as i32,
|
||||
TestRunKind::PortsCheck as i16,
|
||||
now,
|
||||
cutoff,
|
||||
TestRunStatus::InProgress as i32,
|
||||
)
|
||||
.execute(conn.as_mut())
|
||||
.await?;
|
||||
|
||||
Ok(res.rows_affected())
|
||||
}
|
||||
|
||||
pub(crate) async fn update_gateway_score(
|
||||
conn: &mut DbConnection,
|
||||
gateway_pk: i32,
|
||||
@@ -224,6 +358,7 @@ pub(crate) async fn get_testrun_by_id(
|
||||
id,
|
||||
gateway_id,
|
||||
status,
|
||||
kind,
|
||||
created_utc,
|
||||
ip_address,
|
||||
log,
|
||||
@@ -250,14 +385,16 @@ pub(crate) async fn insert_external_testrun(
|
||||
id,
|
||||
gateway_id,
|
||||
status,
|
||||
kind,
|
||||
created_utc,
|
||||
last_assigned_utc,
|
||||
ip_address,
|
||||
log
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)"#,
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)"#,
|
||||
testrun_id,
|
||||
gateway_id,
|
||||
TestRunStatus::InProgress as i32,
|
||||
TestRunKind::Probe as i16,
|
||||
now,
|
||||
assigned_at_utc,
|
||||
"external", // Marker for external origin
|
||||
|
||||
@@ -11,6 +11,8 @@ mod db_tests {
|
||||
explorer_pretty_bond: Some("{\"key\":\"value\"}".to_string()),
|
||||
last_probe_result: Some("{\"key\":\"value\"}".to_string()),
|
||||
last_probe_log: Some("log".to_string()),
|
||||
ports_check: None,
|
||||
last_ports_check_utc: None,
|
||||
last_testrun_utc: Some(1672531200),
|
||||
last_updated_utc: 1672531200,
|
||||
moniker: "moniker".to_string(),
|
||||
@@ -37,6 +39,101 @@ mod db_tests {
|
||||
assert_eq!(http_gateway.description.details, "details");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gateway_dto_normalizes_legacy_dedicated_ports_check_shape() {
|
||||
let legacy_ports = serde_json::json!({
|
||||
"gateway": "gw1",
|
||||
"can_register": true,
|
||||
"port_check_target": "portquiz.net",
|
||||
"ports": {
|
||||
"20": false,
|
||||
"21": true,
|
||||
"43": false
|
||||
},
|
||||
"error": null
|
||||
});
|
||||
let gateway_dto = crate::db::models::GatewayDto {
|
||||
gateway_identity_key: "id1".to_string(),
|
||||
bonded: true,
|
||||
performance: 50,
|
||||
self_described: Some("{}".to_string()),
|
||||
explorer_pretty_bond: Some("{}".to_string()),
|
||||
last_probe_result: None,
|
||||
last_probe_log: None,
|
||||
ports_check: Some(legacy_ports),
|
||||
last_ports_check_utc: Some(1672531200),
|
||||
last_testrun_utc: None,
|
||||
last_updated_utc: 1672531200,
|
||||
moniker: "m".to_string(),
|
||||
security_contact: "c".to_string(),
|
||||
details: "d".to_string(),
|
||||
website: "w".to_string(),
|
||||
bridges: None,
|
||||
};
|
||||
|
||||
let http_gateway: crate::http::models::Gateway = gateway_dto.try_into().unwrap();
|
||||
let normalized = http_gateway.ports_check.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
normalized.get("all_pass"),
|
||||
Some(&serde_json::Value::Bool(false))
|
||||
);
|
||||
assert_eq!(
|
||||
normalized.get("port_check_target"),
|
||||
Some(&serde_json::Value::String("portquiz.net".to_string()))
|
||||
);
|
||||
assert_eq!(normalized.get("error"), Some(&serde_json::Value::Null));
|
||||
assert_eq!(
|
||||
normalized.get("failed_ports"),
|
||||
Some(&serde_json::json!(["20", "43"]))
|
||||
);
|
||||
assert!(normalized.get("gateway").is_none());
|
||||
assert!(normalized.get("ports").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gateway_dto_normalizes_legacy_in_probe_summary_shape() {
|
||||
let legacy_summary = serde_json::json!({
|
||||
"all_pass": true,
|
||||
"failed_ports": [],
|
||||
"error": null,
|
||||
"ports_tested": 32
|
||||
});
|
||||
let gateway_dto = crate::db::models::GatewayDto {
|
||||
gateway_identity_key: "id2".to_string(),
|
||||
bonded: true,
|
||||
performance: 50,
|
||||
self_described: Some("{}".to_string()),
|
||||
explorer_pretty_bond: Some("{}".to_string()),
|
||||
last_probe_result: None,
|
||||
last_probe_log: None,
|
||||
ports_check: Some(legacy_summary),
|
||||
last_ports_check_utc: Some(1672531200),
|
||||
last_testrun_utc: None,
|
||||
last_updated_utc: 1672531200,
|
||||
moniker: "m".to_string(),
|
||||
security_contact: "c".to_string(),
|
||||
details: "d".to_string(),
|
||||
website: "w".to_string(),
|
||||
bridges: None,
|
||||
};
|
||||
|
||||
let http_gateway: crate::http::models::Gateway = gateway_dto.try_into().unwrap();
|
||||
let normalized = http_gateway.ports_check.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
normalized.get("all_pass"),
|
||||
Some(&serde_json::Value::Bool(true))
|
||||
);
|
||||
assert_eq!(normalized.get("failed_ports"), Some(&serde_json::json!([])));
|
||||
assert_eq!(normalized.get("error"), Some(&serde_json::Value::Null));
|
||||
assert_eq!(
|
||||
normalized.get("port_check_target"),
|
||||
Some(&serde_json::Value::Null)
|
||||
);
|
||||
assert!(normalized.get("ports_tested").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixnode_dto_try_from() {
|
||||
let mixnode_dto = crate::db::models::MixnodeDto {
|
||||
@@ -267,6 +364,8 @@ fn test_gateway_dto_with_null_values() {
|
||||
explorer_pretty_bond: None,
|
||||
last_probe_result: None,
|
||||
last_probe_log: None,
|
||||
ports_check: None,
|
||||
last_ports_check_utc: None,
|
||||
last_testrun_utc: None,
|
||||
last_updated_utc: 0,
|
||||
moniker: "".to_string(),
|
||||
|
||||
@@ -106,6 +106,8 @@ fn filter_bonded_gateways_to_skinny(gateways: Vec<Gateway>) -> Vec<GatewaySkinny
|
||||
performance: g.performance,
|
||||
explorer_pretty_bond: g.explorer_pretty_bond.clone(),
|
||||
last_probe_result: g.last_probe_result.clone(),
|
||||
ports_check: g.ports_check.clone(),
|
||||
last_ports_check_utc: g.last_ports_check_utc.clone(),
|
||||
last_testrun_utc: g.last_testrun_utc.clone(),
|
||||
last_updated_utc: g.last_updated_utc.clone(),
|
||||
routing_score: g.routing_score,
|
||||
@@ -135,6 +137,8 @@ mod tests {
|
||||
},
|
||||
last_probe_result: Some(serde_json::json!({"result": "ok"})),
|
||||
last_probe_log: None,
|
||||
ports_check: None,
|
||||
last_ports_check_utc: None,
|
||||
last_testrun_utc: Some("2024-01-20T10:00:00Z".to_string()),
|
||||
last_updated_utc: "2024-01-20T11:00:00Z".to_string(),
|
||||
routing_score: 0.95,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user