Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 48c0130f32 | |||
| 2b237dec1f | |||
| 1dc3e94bdc | |||
| 54efd30fb8 | |||
| a095d975fb | |||
| 4c8ee91b84 | |||
| 07a5e91da2 | |||
| 88749b5bc8 | |||
| 0df359639a | |||
| a43dd38e76 | |||
| 8aaa8dc8d3 | |||
| 35515fbf8c | |||
| 5ea5cd13a2 | |||
| 2874f26f57 | |||
| 802417ccec | |||
| fdb79eb5d6 | |||
| e554f1e0ad |
Generated
+9
-10
@@ -5635,7 +5635,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-api"
|
||||
version = "1.1.78"
|
||||
version = "1.1.79"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -5880,7 +5880,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-cli"
|
||||
version = "1.1.75"
|
||||
version = "1.1.76"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
@@ -5963,7 +5963,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-client"
|
||||
version = "1.1.75"
|
||||
version = "1.1.76"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -6926,7 +6926,6 @@ dependencies = [
|
||||
"bytes",
|
||||
"cfg-if",
|
||||
"encoding_rs",
|
||||
"fastrand",
|
||||
"hickory-resolver",
|
||||
"http 1.3.1",
|
||||
"inventory",
|
||||
@@ -7374,7 +7373,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-requester"
|
||||
version = "1.1.76"
|
||||
version = "1.1.77"
|
||||
dependencies = [
|
||||
"addr",
|
||||
"anyhow",
|
||||
@@ -7424,7 +7423,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node"
|
||||
version = "1.30.0"
|
||||
version = "1.31.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -7561,7 +7560,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-status-agent"
|
||||
version = "2.0.0"
|
||||
version = "2.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -7582,7 +7581,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-status-api"
|
||||
version = "4.6.1"
|
||||
version = "4.6.2"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"anyhow",
|
||||
@@ -7976,7 +7975,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.75"
|
||||
version = "1.1.76"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -8774,7 +8773,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nymvisor"
|
||||
version = "0.1.40"
|
||||
version = "0.1.41"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
description = "Implementation of the Nym Client"
|
||||
version = "1.1.75"
|
||||
version = "1.1.76"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||
version = "1.1.75"
|
||||
version = "1.1.76"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
@@ -13,9 +13,8 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
use tokio::time::Instant;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
use tracing::{debug, info, instrument, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub use helpers::{BufferedDeposit, PerformedDeposits, make_deposits_request, split_deposits};
|
||||
@@ -147,14 +146,9 @@ impl DepositsBuffer {
|
||||
|
||||
// if we're here, we know we're below the threshold
|
||||
fn maybe_refill_deposits(&self) {
|
||||
if let Some((mut guard, completion_guard)) =
|
||||
self.inner.deposits_refill_task.try_get_new_task_guard()
|
||||
{
|
||||
if let Some(mut guard) = self.inner.deposits_refill_task.try_get_new_task_guard() {
|
||||
let this = self.clone();
|
||||
*guard = Some(tokio::spawn(async move {
|
||||
let _completion_guard = completion_guard;
|
||||
this.refill_deposits().await
|
||||
}));
|
||||
*guard = Some(tokio::spawn(async move { this.refill_deposits().await }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,8 +179,6 @@ impl DepositsBuffer {
|
||||
requested_on: OffsetDateTime,
|
||||
client_pubkey: PublicKeyUser,
|
||||
) -> Result<BufferedDeposit, CredentialProxyError> {
|
||||
let wait_start = Instant::now();
|
||||
let mut i = 0;
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
if let Some(buffered_deposit) = self.inner.unused_deposits.lock().await.pop() {
|
||||
@@ -203,15 +195,6 @@ impl DepositsBuffer {
|
||||
// make sure there's always a task working in the background in case deposits get used up too quickly
|
||||
self.maybe_refill_deposits()
|
||||
}
|
||||
i += 1;
|
||||
let elapsed = wait_start.elapsed();
|
||||
if elapsed > Duration::from_secs(5) && i % 10 == 0 {
|
||||
warn!("we've been waiting for over 5s to make a deposit - something is wrong!")
|
||||
} else if elapsed > Duration::from_secs(10) && i % 5 == 0 {
|
||||
error!(
|
||||
"we've been waiting for over 10s to make a deposit - something is SERIOUSLY wrong!"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,22 +3,12 @@
|
||||
|
||||
use crate::error::CredentialProxyError;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex as StdMutex, MutexGuard};
|
||||
use std::sync::{Mutex as StdMutex, MutexGuard};
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{debug, error};
|
||||
|
||||
pub(super) type RefillTaskResult = Result<(), CredentialProxyError>;
|
||||
|
||||
pub(super) struct InProgressGuard {
|
||||
in_progress: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Drop for InProgressGuard {
|
||||
fn drop(&mut self) {
|
||||
self.in_progress.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct RefillTask {
|
||||
// note that we can only have a single transaction in progress (or it'd mess up with our sequence numbers)
|
||||
@@ -26,7 +16,7 @@ pub(super) struct RefillTask {
|
||||
// we'll have to increase the number of deposits per transaction
|
||||
join_handle: StdMutex<Option<JoinHandle<RefillTaskResult>>>,
|
||||
|
||||
in_progress: Arc<AtomicBool>,
|
||||
in_progress: AtomicBool,
|
||||
}
|
||||
|
||||
impl RefillTask {
|
||||
@@ -38,15 +28,9 @@ impl RefillTask {
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Returns `None` if a refill is already in progress. On success, returns the
|
||||
/// join-handle guard (to store the new `JoinHandle` into) and an [`InProgressGuard`]
|
||||
/// that **must be moved into the spawned task** — it resets the flag when dropped.
|
||||
pub(super) fn try_get_new_task_guard(
|
||||
&self,
|
||||
) -> Option<(
|
||||
MutexGuard<'_, Option<JoinHandle<RefillTaskResult>>>,
|
||||
InProgressGuard,
|
||||
)> {
|
||||
) -> Option<MutexGuard<'_, Option<JoinHandle<RefillTaskResult>>>> {
|
||||
// sanity check for concurrent request
|
||||
if !self.try_set_in_progress() {
|
||||
debug!("another task has already started deposit refill request");
|
||||
@@ -64,11 +48,7 @@ impl RefillTask {
|
||||
}
|
||||
}
|
||||
|
||||
let completion_guard = InProgressGuard {
|
||||
in_progress: Arc::clone(&self.in_progress),
|
||||
};
|
||||
|
||||
Some((guard, completion_guard))
|
||||
Some(guard)
|
||||
}
|
||||
|
||||
pub(super) fn take_task_join_handle(&self) -> Option<JoinHandle<RefillTaskResult>> {
|
||||
@@ -76,34 +56,3 @@ impl RefillTask {
|
||||
self.join_handle.lock().expect("mutex got poisoned").take()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn in_progress_resets_after_guard_drop() {
|
||||
let task = RefillTask::default();
|
||||
|
||||
let (guard, completion_guard) = task.try_get_new_task_guard().unwrap();
|
||||
drop(guard);
|
||||
assert!(task.try_get_new_task_guard().is_none());
|
||||
|
||||
drop(completion_guard);
|
||||
assert!(task.try_get_new_task_guard().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_progress_resets_on_panic() {
|
||||
let task = RefillTask::default();
|
||||
|
||||
let (guard, completion_guard) = task.try_get_new_task_guard().unwrap();
|
||||
drop(guard);
|
||||
|
||||
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
let _g = completion_guard;
|
||||
panic!("simulated refill task panic");
|
||||
}));
|
||||
assert!(task.try_get_new_task_guard().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
inventory = { workspace = true }
|
||||
fastrand = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt", "macros", "time"] }
|
||||
rustls = { workspace=true }
|
||||
# used for decoding text responses (they were already implicitly included)
|
||||
|
||||
@@ -56,6 +56,7 @@ use std::{
|
||||
use hickory_resolver::{
|
||||
TokioResolver,
|
||||
config::{NameServerConfig, NameServerConfigGroup, ResolverConfig, ResolverOpts},
|
||||
lookup_ip::LookupIpIntoIter,
|
||||
name_server::TokioConnectionProvider,
|
||||
};
|
||||
use once_cell::sync::OnceCell;
|
||||
@@ -226,11 +227,9 @@ async fn resolve(
|
||||
let primary_err = match resolve_fut.await {
|
||||
Err(_) => ResolveError::Timeout,
|
||||
Ok(Ok(lookup)) => {
|
||||
// Shuffle so that successive connection attempts cycle through all
|
||||
// returned IPs rather than always hitting the same first address.
|
||||
let mut ips: Vec<IpAddr> = lookup.into_iter().collect();
|
||||
fastrand::shuffle(&mut ips);
|
||||
let addrs: Addrs = Box::new(ips.into_iter().map(|ip| SocketAddr::new(ip, 0)));
|
||||
let addrs: Addrs = Box::new(SocketAddrs {
|
||||
iter: lookup.into_iter(),
|
||||
});
|
||||
return Ok(addrs);
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
@@ -257,6 +256,18 @@ async fn resolve(
|
||||
Err(primary_err)
|
||||
}
|
||||
|
||||
struct SocketAddrs {
|
||||
iter: LookupIpIntoIter,
|
||||
}
|
||||
|
||||
impl Iterator for SocketAddrs {
|
||||
type Item = SocketAddr;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.iter.next().map(|ip_addr| SocketAddr::new(ip_addr, 0))
|
||||
}
|
||||
}
|
||||
|
||||
impl HickoryDnsResolver {
|
||||
/// Returns an instance of the shared resolver.
|
||||
pub fn shared() -> Self {
|
||||
|
||||
@@ -24,14 +24,11 @@ pub const SPHINX_STREAM_VERSION_THRESHOLD: u8 = v9::VERSION;
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const _: () = {
|
||||
assert!(SPHINX_STREAM_VERSION_THRESHOLD > MAX_NON_STREAM_VERSION);
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn stream_transport_threshold_is_consistent() {
|
||||
assert_eq!(MAX_NON_STREAM_VERSION, 8);
|
||||
assert_eq!(SPHINX_STREAM_VERSION_THRESHOLD, 9);
|
||||
assert!(SPHINX_STREAM_VERSION_THRESHOLD > MAX_NON_STREAM_VERSION);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
},
|
||||
"mixmining_reserve": {
|
||||
"denom": "unym",
|
||||
"amount": "166613455567357"
|
||||
"amount": "168575020719057"
|
||||
},
|
||||
"vesting_tokens": {
|
||||
"denom": "unym",
|
||||
@@ -13,6 +13,6 @@
|
||||
},
|
||||
"circulating_supply": {
|
||||
"denom": "unym",
|
||||
"amount": "833386544432643"
|
||||
"amount": "831424979280943"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"nodes": 744,
|
||||
"locations": 77,
|
||||
"mixnodes": 263,
|
||||
"exit_gateways": 474
|
||||
"nodes": 752,
|
||||
"locations": 76,
|
||||
"mixnodes": 273,
|
||||
"exit_gateways": 470
|
||||
}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
833_386_544
|
||||
831_424_979
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
4_628
|
||||
4_682
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.06%
|
||||
0.95%
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
27.103
|
||||
30.134
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
254_977
|
||||
254_377
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
61_194_673
|
||||
61_050_638
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
61_194_672
|
||||
61_050_637
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
| **Item** | **Description** | **Amount in NYM** |
|
||||
|:-------------------|:------------------------------------------------------|--------------------:|
|
||||
| Total Supply | Maximum amount of NYM token in existence | 1_000_000_000 |
|
||||
| Mixmining Reserve | Tokens releasing for operators rewards | 166_613_455 |
|
||||
| Mixmining Reserve | Tokens releasing for operators rewards | 168_575_020 |
|
||||
| Vesting Tokens | Tokens locked outside of circulation for future claim | 0 |
|
||||
| Circulating Supply | Amount of unlocked tokens | 833_386_544 |
|
||||
| Stake Saturation | Optimal size of node self-bond + delegation | 254_977 |
|
||||
| Circulating Supply | Amount of unlocked tokens | 831_424_979 |
|
||||
| Stake Saturation | Optimal size of node self-bond + delegation | 254_377 |
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"interval": {
|
||||
"reward_pool": "166613455567357.391753168447944888",
|
||||
"staking_supply": "61194672938727.325886595653401629",
|
||||
"reward_pool": "168575020719057.456153922699547282",
|
||||
"staking_supply": "61050637328128.353993717821521057",
|
||||
"staking_supply_scale_factor": "0.07342892",
|
||||
"epoch_reward_budget": "4628151543.537705326476901331",
|
||||
"stake_saturation_point": "254977803911.363857860815222506",
|
||||
"epoch_reward_budget": "4682639464.418262670942297209",
|
||||
"stake_saturation_point": "254377655533.868141640490923004",
|
||||
"sybil_resistance": "0.3",
|
||||
"active_set_work_factor": "10",
|
||||
"interval_pool_emission": "0.02"
|
||||
|
||||
@@ -1 +1 @@
|
||||
Thursday, April 30th 2026, 08:30:26 UTC
|
||||
Thursday, April 9th 2026, 15:33:24 UTC
|
||||
|
||||
@@ -6,7 +6,6 @@ usage: nym-node-cli install [-h] [-V] [-d BRANCH] [-v]
|
||||
[--email EMAIL] [--moniker MONIKER]
|
||||
[--description DESCRIPTION]
|
||||
[--public-ip PUBLIC_IP]
|
||||
[--host-ssh-port HOST_SSH_PORT]
|
||||
[--nym-node-binary NYM_NODE_BINARY]
|
||||
[--uplink-dev UPLINK_DEV] [--env KEY=VALUE]
|
||||
|
||||
@@ -28,8 +27,6 @@ options:
|
||||
Short public description of the node
|
||||
--public-ip PUBLIC_IP
|
||||
External IPv4 address (autodetected if omitted)
|
||||
--host-ssh-port HOST_SSH_PORT
|
||||
Host SSH port to allow in the firewall (default: 22)
|
||||
--nym-node-binary NYM_NODE_BINARY
|
||||
URL for nym-node binary (autodetected if omitted)
|
||||
--uplink-dev UPLINK_DEV
|
||||
|
||||
@@ -4,115 +4,56 @@ Start this nym-node
|
||||
Usage: nym-node run [OPTIONS]
|
||||
|
||||
Options:
|
||||
--id <ID>
|
||||
Id of the nym-node to use [env: NYMNODE_ID=] [default: default-nym-node]
|
||||
--config-file <CONFIG_FILE>
|
||||
Path to a configuration file of this node [env: NYMNODE_CONFIG=]
|
||||
--accept-operator-terms-and-conditions
|
||||
Explicitly specify whether you agree with the terms and conditions of a nym node operator as defined at <https://nymtech.net/terms-and-conditions/operators/v1.0.0> [env:
|
||||
NYMNODE_ACCEPT_OPERATOR_TERMS=]
|
||||
--deny-init
|
||||
Forbid a new node from being initialised if configuration file for the provided specification doesn't already exist [env: NYMNODE_DENY_INIT=]
|
||||
--init-only
|
||||
If this is a brand new nym-node, specify whether it should only be initialised without actually running the subprocesses [env: NYMNODE_INIT_ONLY=]
|
||||
--local
|
||||
Flag specifying this node will be running in a local setting [env: NYMNODE_LOCAL=]
|
||||
--mode [<MODE>...]
|
||||
Specifies the current mode(s) of this nym-node [env: NYMNODE_MODE=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only]
|
||||
--modes <MODES>
|
||||
Specifies the current mode(s) of this nym-node as a single flag [env: NYMNODE_MODES=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only]
|
||||
-w, --write-changes
|
||||
If this node has been initialised before, specify whether to write any new changes to the config file [env: NYMNODE_WRITE_CONFIG_CHANGES=]
|
||||
--bonding-information-output <BONDING_INFORMATION_OUTPUT>
|
||||
Specify output file for bonding information of this nym-node, i.e. its encoded keys. NOTE: the required bonding information is still a subject to change and this argument
|
||||
should be treated only as a preview of future features [env: NYMNODE_BONDING_INFORMATION_OUTPUT=]
|
||||
-o, --output <OUTPUT>
|
||||
Specify the output format of the bonding information (`text` or `json`) [env: NYMNODE_OUTPUT=] [default: text] [possible values: text, json]
|
||||
--public-ips <PUBLIC_IPS>
|
||||
Comma separated list of public ip addresses that will be announced to the nym-api and subsequently to the clients. In nearly all circumstances, it's going to be identical
|
||||
to the address you're going to use for bonding [env: NYMNODE_PUBLIC_IPS=]
|
||||
--hostname <HOSTNAME>
|
||||
Optional hostname associated with this gateway that will be announced to the nym-api and subsequently to the clients [env: NYMNODE_HOSTNAME=]
|
||||
--location <LOCATION>
|
||||
Optional **physical** location of this node's server. Either full country name (e.g. 'Poland'), two-letter alpha2 (e.g. 'PL'), three-letter alpha3 (e.g. 'POL') or
|
||||
three-digit numeric-3 (e.g. '616') can be provided [env: NYMNODE_LOCATION=]
|
||||
--http-bind-address <HTTP_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its http API. default: `[::]:8080` [env: NYMNODE_HTTP_BIND_ADDRESS=]
|
||||
--landing-page-assets-path <LANDING_PAGE_ASSETS_PATH>
|
||||
Path to assets directory of custom landing page of this node [env: NYMNODE_HTTP_LANDING_ASSETS=]
|
||||
--http-access-token <HTTP_ACCESS_TOKEN>
|
||||
An optional bearer token for accessing certain http endpoints. Currently only used for prometheus metrics [env: NYMNODE_HTTP_ACCESS_TOKEN=]
|
||||
--expose-system-info <EXPOSE_SYSTEM_INFO>
|
||||
Specify whether basic system information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_INFO=] [possible values: true, false]
|
||||
--expose-system-hardware <EXPOSE_SYSTEM_HARDWARE>
|
||||
Specify whether basic system hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_HARDWARE=] [possible values: true, false]
|
||||
--expose-crypto-hardware <EXPOSE_CRYPTO_HARDWARE>
|
||||
Specify whether detailed system crypto hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_CRYPTO_HARDWARE=] [possible values: true, false]
|
||||
--mixnet-bind-address <MIXNET_BIND_ADDRESS>
|
||||
Address this node will bind to for listening for mixnet packets default: `[::]:1789` [env: NYMNODE_MIXNET_BIND_ADDRESS=]
|
||||
--mixnet-announce-port <MIXNET_ANNOUNCE_PORT>
|
||||
If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env:
|
||||
NYMNODE_MIXNET_ANNOUNCE_PORT=]
|
||||
--nym-api-urls <NYM_API_URLS>
|
||||
Addresses to nym APIs from which the node gets the view of the network [env: NYMNODE_NYM_APIS=]
|
||||
--nyxd-urls <NYXD_URLS>
|
||||
Addresses to nyxd chain endpoint which the node will use for chain interactions [env: NYMNODE_NYXD=]
|
||||
--enable-console-logging <ENABLE_CONSOLE_LOGGING>
|
||||
Specify whether running statistics of this node should be logged to the console [env: NYMNODE_ENABLE_CONSOLE_LOGGING=] [possible values: true, false]
|
||||
--wireguard-enabled <WIREGUARD_ENABLED>
|
||||
Specifies whether the wireguard service is enabled on this node [env: NYMNODE_WG_ENABLED=] [possible values: true, false]
|
||||
--wireguard-bind-address <WIREGUARD_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its wireguard interface. default: `[::]:51822` [env: NYMNODE_WG_BIND_ADDRESS=]
|
||||
--wireguard-tunnel-announced-port <WIREGUARD_TUNNEL_ANNOUNCED_PORT>
|
||||
Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env:
|
||||
NYMNODE_WG_ANNOUNCED_PORT=]
|
||||
--wireguard-private-network-prefix <WIREGUARD_PRIVATE_NETWORK_PREFIX>
|
||||
The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4 is 32 and for IPv6 is 128 [env:
|
||||
NYMNODE_WG_PRIVATE_NETWORK_PREFIX=]
|
||||
--wireguard-userspace <WIREGUARD_USERSPACE>
|
||||
Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. Useful in containerized environments without kernel WireGuard support [env:
|
||||
NYMNODE_WG_USERSPACE=] [possible values: true, false]
|
||||
--verloc-bind-address <VERLOC_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its verloc API. default: `[::]:1790` [env: NYMNODE_VERLOC_BIND_ADDRESS=]
|
||||
--verloc-announce-port <VERLOC_ANNOUNCE_PORT>
|
||||
If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env:
|
||||
NYMNODE_VERLOC_ANNOUNCE_PORT=]
|
||||
--entry-bind-address <ENTRY_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its client websocket API. default: `[::]:9000` [env: NYMNODE_ENTRY_BIND_ADDRESS=]
|
||||
--announce-ws-port <ANNOUNCE_WS_PORT>
|
||||
Custom announced port for listening for websocket client traffic. If unspecified, the value from the `bind_address` will be used instead [env:
|
||||
NYMNODE_ENTRY_ANNOUNCE_WS_PORT=]
|
||||
--announce-wss-port <ANNOUNCE_WSS_PORT>
|
||||
If applicable, announced port for listening for secure websocket client traffic [env: NYMNODE_ENTRY_ANNOUNCE_WSS_PORT=]
|
||||
--enforce-zk-nyms <ENFORCE_ZK_NYMS>
|
||||
Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet or if it also accepts non-paying clients [env: NYMNODE_ENFORCE_ZK_NYMS=]
|
||||
[possible values: true, false]
|
||||
--mnemonic <MNEMONIC>
|
||||
Custom cosmos wallet mnemonic used for zk-nym redemption. If no value is provided, a fresh mnemonic is going to be generated [env: NYMNODE_MNEMONIC=]
|
||||
--upgrade-mode-attestation-url <UPGRADE_MODE_ATTESTATION_URL>
|
||||
Endpoint to query to retrieve current upgrade mode attestation. This argument should never be set outside testnets and local networks [env:
|
||||
NYMNODE_UPGRADE_MODE_ATTESTATION_URL=]
|
||||
--upgrade-mode-attester-public-key <UPGRADE_MODE_ATTESTER_PUBLIC_KEY>
|
||||
Expected public key of the entity signing the published attestation. This argument should never be set outside testnets and local networks [env:
|
||||
NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY=]
|
||||
--upstream-exit-policy-url <UPSTREAM_EXIT_POLICY_URL>
|
||||
Specifies the url for an upstream source of the exit policy used by this node [env: NYMNODE_UPSTREAM_EXIT_POLICY=]
|
||||
--open-proxy <OPEN_PROXY>
|
||||
Specifies whether this exit node should run in 'open-proxy' mode and thus would attempt to resolve **ANY** request it receives [env: NYMNODE_OPEN_PROXY=] [possible values:
|
||||
true, false]
|
||||
--lp-control-bind-address <LP_CONTROL_BIND_ADDRESS>
|
||||
Bind address for the TCP LP control traffic. default: `[::]:41264` [env: NYMNODE_LP_CONTROL_BIND_ADDRESS=]
|
||||
--lp-control-announce-port <LP_CONTROL_ANNOUNCE_PORT>
|
||||
Custom announced port for listening for the TCP LP control traffic. If unspecified, the value from the `lp_control_bind_address` will be used instead [env:
|
||||
NYMNODE_LP_CONTROL_ANNOUNCE_PORT=]
|
||||
--lp-data-bind-address <LP_DATA_BIND_ADDRESS>
|
||||
Bind address for the UDP LP data traffic. default: `[::]:51264` [env: NYMNODE_LP_DATA_BIND_ADDRESS=]
|
||||
--lp-data-announce-port <LP_DATA_ANNOUNCE_PORT>
|
||||
Custom announced port for listening for the UDP LP data traffic. If unspecified, the value from the `lp_data_bind_address` will be used instead [env:
|
||||
NYMNODE_LP_DATA_ANNOUNCE_PORT=]
|
||||
--lp-use-mock-ecash <LP_USE_MOCK_ECASH>
|
||||
Use mock ecash manager for LP testing. WARNING: Only use this for local testing! Never enable in production. When enabled, the LP listener will accept any credential
|
||||
without blockchain verification [env: NYMNODE_LP_USE_MOCK_ECASH=] [possible values: true, false]
|
||||
-h, --help
|
||||
Print help
|
||||
--id <ID> Id of the nym-node to use [env: NYMNODE_ID=] [default: default-nym-node]
|
||||
--config-file <CONFIG_FILE> Path to a configuration file of this node [env: NYMNODE_CONFIG=]
|
||||
--accept-operator-terms-and-conditions Explicitly specify whether you agree with the terms and conditions of a nym node operator as defined at <https://nymtech.net/terms-and-conditions/operators/v1.0.0> [env: NYMNODE_ACCEPT_OPERATOR_TERMS=]
|
||||
--deny-init Forbid a new node from being initialised if configuration file for the provided specification doesn't already exist [env: NYMNODE_DENY_INIT=]
|
||||
--init-only If this is a brand new nym-node, specify whether it should only be initialised without actually running the subprocesses [env: NYMNODE_INIT_ONLY=]
|
||||
--local Flag specifying this node will be running in a local setting [env: NYMNODE_LOCAL=]
|
||||
--mode [<MODE>...] Specifies the current mode(s) of this nym-node [env: NYMNODE_MODE=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only]
|
||||
--modes <MODES> Specifies the current mode(s) of this nym-node as a single flag [env: NYMNODE_MODES=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only]
|
||||
-w, --write-changes If this node has been initialised before, specify whether to write any new changes to the config file [env: NYMNODE_WRITE_CONFIG_CHANGES=]
|
||||
--bonding-information-output <BONDING_INFORMATION_OUTPUT> Specify output file for bonding information of this nym-node, i.e. its encoded keys. NOTE: the required bonding information is still a subject to change and this argument should be treated only as a preview of
|
||||
future features [env: NYMNODE_BONDING_INFORMATION_OUTPUT=]
|
||||
-o, --output <OUTPUT> Specify the output format of the bonding information (`text` or `json`) [env: NYMNODE_OUTPUT=] [default: text] [possible values: text, json]
|
||||
--public-ips <PUBLIC_IPS> Comma separated list of public ip addresses that will be announced to the nym-api and subsequently to the clients. In nearly all circumstances, it's going to be identical to the address you're going to use for
|
||||
bonding [env: NYMNODE_PUBLIC_IPS=]
|
||||
--hostname <HOSTNAME> Optional hostname associated with this gateway that will be announced to the nym-api and subsequently to the clients [env: NYMNODE_HOSTNAME=]
|
||||
--location <LOCATION> Optional **physical** location of this node's server. Either full country name (e.g. 'Poland'), two-letter alpha2 (e.g. 'PL'), three-letter alpha3 (e.g. 'POL') or three-digit numeric-3 (e.g. '616') can be
|
||||
provided [env: NYMNODE_LOCATION=]
|
||||
--http-bind-address <HTTP_BIND_ADDRESS> Socket address this node will use for binding its http API. default: `[::]:8080` [env: NYMNODE_HTTP_BIND_ADDRESS=]
|
||||
--landing-page-assets-path <LANDING_PAGE_ASSETS_PATH> Path to assets directory of custom landing page of this node [env: NYMNODE_HTTP_LANDING_ASSETS=]
|
||||
--http-access-token <HTTP_ACCESS_TOKEN> An optional bearer token for accessing certain http endpoints. Currently only used for prometheus metrics [env: NYMNODE_HTTP_ACCESS_TOKEN=]
|
||||
--expose-system-info <EXPOSE_SYSTEM_INFO> Specify whether basic system information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_INFO=] [possible values: true, false]
|
||||
--expose-system-hardware <EXPOSE_SYSTEM_HARDWARE> Specify whether basic system hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_HARDWARE=] [possible values: true, false]
|
||||
--expose-crypto-hardware <EXPOSE_CRYPTO_HARDWARE> Specify whether detailed system crypto hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_CRYPTO_HARDWARE=] [possible values: true, false]
|
||||
--mixnet-bind-address <MIXNET_BIND_ADDRESS> Address this node will bind to for listening for mixnet packets default: `[::]:1789` [env: NYMNODE_MIXNET_BIND_ADDRESS=]
|
||||
--mixnet-announce-port <MIXNET_ANNOUNCE_PORT> If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env: NYMNODE_MIXNET_ANNOUNCE_PORT=]
|
||||
--nym-api-urls <NYM_API_URLS> Addresses to nym APIs from which the node gets the view of the network [env: NYMNODE_NYM_APIS=]
|
||||
--nyxd-urls <NYXD_URLS> Addresses to nyxd chain endpoint which the node will use for chain interactions [env: NYMNODE_NYXD=]
|
||||
--enable-console-logging <ENABLE_CONSOLE_LOGGING> Specify whether running statistics of this node should be logged to the console [env: NYMNODE_ENABLE_CONSOLE_LOGGING=] [possible values: true, false]
|
||||
--wireguard-enabled <WIREGUARD_ENABLED> Specifies whether the wireguard service is enabled on this node [env: NYMNODE_WG_ENABLED=] [possible values: true, false]
|
||||
--wireguard-bind-address <WIREGUARD_BIND_ADDRESS> Socket address this node will use for binding its wireguard interface. default: `[::]:51822` [env: NYMNODE_WG_BIND_ADDRESS=]
|
||||
--wireguard-tunnel-announced-port <WIREGUARD_TUNNEL_ANNOUNCED_PORT> Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
|
||||
--wireguard-private-network-prefix <WIREGUARD_PRIVATE_NETWORK_PREFIX> The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4 is 32 and for IPv6 is 128 [env: NYMNODE_WG_PRIVATE_NETWORK_PREFIX=]
|
||||
--wireguard-userspace <WIREGUARD_USERSPACE> Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. Useful in containerized environments without kernel WireGuard support [env: NYMNODE_WG_USERSPACE=] [possible values: true,
|
||||
false]
|
||||
--verloc-bind-address <VERLOC_BIND_ADDRESS> Socket address this node will use for binding its verloc API. default: `[::]:1790` [env: NYMNODE_VERLOC_BIND_ADDRESS=]
|
||||
--verloc-announce-port <VERLOC_ANNOUNCE_PORT> If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env: NYMNODE_VERLOC_ANNOUNCE_PORT=]
|
||||
--entry-bind-address <ENTRY_BIND_ADDRESS> Socket address this node will use for binding its client websocket API. default: `[::]:9000` [env: NYMNODE_ENTRY_BIND_ADDRESS=]
|
||||
--announce-ws-port <ANNOUNCE_WS_PORT> Custom announced port for listening for websocket client traffic. If unspecified, the value from the `bind_address` will be used instead [env: NYMNODE_ENTRY_ANNOUNCE_WS_PORT=]
|
||||
--announce-wss-port <ANNOUNCE_WSS_PORT> If applicable, announced port for listening for secure websocket client traffic [env: NYMNODE_ENTRY_ANNOUNCE_WSS_PORT=]
|
||||
--enforce-zk-nyms <ENFORCE_ZK_NYMS> Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet or if it also accepts non-paying clients [env: NYMNODE_ENFORCE_ZK_NYMS=] [possible values: true, false]
|
||||
--mnemonic <MNEMONIC> Custom cosmos wallet mnemonic used for zk-nym redemption. If no value is provided, a fresh mnemonic is going to be generated [env: NYMNODE_MNEMONIC=]
|
||||
--upgrade-mode-attestation-url <UPGRADE_MODE_ATTESTATION_URL> Endpoint to query to retrieve current upgrade mode attestation. This argument should never be set outside testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTATION_URL=]
|
||||
--upgrade-mode-attester-public-key <UPGRADE_MODE_ATTESTER_PUBLIC_KEY> Expected public key of the entity signing the published attestation. This argument should never be set outside testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY=]
|
||||
--upstream-exit-policy-url <UPSTREAM_EXIT_POLICY_URL> Specifies the url for an upstream source of the exit policy used by this node [env: NYMNODE_UPSTREAM_EXIT_POLICY=]
|
||||
--open-proxy <OPEN_PROXY> Specifies whether this exit node should run in 'open-proxy' mode and thus would attempt to resolve **ANY** request it receives [env: NYMNODE_OPEN_PROXY=] [possible values: true, false]
|
||||
--lp-control-bind-address <LP_CONTROL_BIND_ADDRESS> Bind address for the TCP LP control traffic. default: `[::]:41264` [env: NYMNODE_LP_CONTROL_BIND_ADDRESS=]
|
||||
--lp-control-announce-port <LP_CONTROL_ANNOUNCE_PORT> Custom announced port for listening for the TCP LP control traffic. If unspecified, the value from the `lp_control_bind_address` will be used instead [env: NYMNODE_LP_CONTROL_ANNOUNCE_PORT=]
|
||||
--lp-data-bind-address <LP_DATA_BIND_ADDRESS> Bind address for the UDP LP data traffic. default: `[::]:51264` [env: NYMNODE_LP_DATA_BIND_ADDRESS=]
|
||||
--lp-data-announce-port <LP_DATA_ANNOUNCE_PORT> Custom announced port for listening for the UDP LP data traffic. If unspecified, the value from the `lp_data_bind_address` will be used instead [env: NYMNODE_LP_DATA_ANNOUNCE_PORT=]
|
||||
--lp-use-mock-ecash <LP_USE_MOCK_ECASH> Use mock ecash manager for LP testing. WARNING: Only use this for local testing! Never enable in production. When enabled, the LP listener will accept any credential without blockchain verification [env:
|
||||
NYMNODE_LP_USE_MOCK_ECASH=] [possible values: true, false]
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
@@ -17,9 +17,9 @@ Download and run instructions for the GUIs can be found [here](https://nymvpn.co
|
||||
## Download & Extract Binary
|
||||
Check the [release page](https://github.com/nymtech/nym-vpn-client/releases/) page for the latest release version and modify the instructions accordingly. These instructions use the latest as of the time of writing.
|
||||
```sh
|
||||
wget -q https://github.com/nymtech/nym-vpn-client/releases/download/nym-vpn-core-v1.29.0/nym-vpn-core-v1.29.0_<YOUR_OPERATING_SYSTEM>.tar.gz &&
|
||||
tar -xzf nym-vpn-core-v1.29.0_<YOUR_OPERATING_SYSTEM>.tar.gz &&
|
||||
cd nym-vpn-core-v1.29.0_<YOUR_OPERATING_SYSTEM>/ &&
|
||||
wget -q https://github.com/nymtech/nym-vpn-client/releases/download/nym-vpn-core-v1.27.0-beta/nym-vpn-core-v1.27.0-beta_<YOUR_OPERATING_SYSTEM>.tar.gz &&
|
||||
tar -xzf nym-vpn-core-v1.27.0-beta_<YOUR_OPERATING_SYSTEM>.tar.gz &&
|
||||
cd nym-vpn-core-v1.27.0-beta_<YOUR_OPERATING_SYSTEM>/ &&
|
||||
chmod u+x *
|
||||
```
|
||||
|
||||
@@ -165,66 +165,6 @@ View the current device identity:
|
||||
nym-vpnc device get
|
||||
```
|
||||
|
||||
## Pay as You Go: Decentralized Access to Nym
|
||||
|
||||
You can fund your VPN usage directly from your own wallet instead of going through the NymVPN account system. You deposit `$NYM` into the ticketbook smart contract and receive zk-nym ticketbooks that authenticate you on the network.
|
||||
|
||||
<Callout type="warning" emoji="⚠️">
|
||||
If you already have an account stored in `nym-vpnc`, you must remove it first with `nym-vpnc account forget` before setting a new mnemonic.
|
||||
</Callout>
|
||||
|
||||
### Set Your Mnemonic
|
||||
|
||||
Store the recovery phrase for your on-chain wallet address (`n1...`) that holds your `$NYM` tokens:
|
||||
|
||||
```sh
|
||||
nym-vpnc account set "<MNEMONIC>" --location blockchain
|
||||
```
|
||||
|
||||
You must fund this address yourself, for example by transferring `$NYM` from an exchange or another wallet. The `--location blockchain` flag tells `nym-vpnc` to use the on-chain wallet directly rather than the NymVPN account system.
|
||||
|
||||
### Obtain Ticketbooks
|
||||
|
||||
Deposit `$NYM` into the ticketbook smart contract and receive zk-nym credentials:
|
||||
|
||||
```sh
|
||||
nym-vpnc account obtain-ticketbooks --amount 1 --source smartcontract
|
||||
```
|
||||
|
||||
You can omit `--source` to use the default:
|
||||
|
||||
```sh
|
||||
nym-vpnc account obtain-ticketbooks --amount 1
|
||||
```
|
||||
|
||||
The `--amount` flag specifies how many ticketbooks to obtain **per ticket type**. Each request issues one ticketbook for each of the three types:
|
||||
|
||||
- **Mixnet Entry** — used for 5-hop mixnet mode
|
||||
- **WireGuard Entry** — used for 2-hop WireGuard mode (entry side)
|
||||
- **WireGuard Exit** — used for 2-hop WireGuard mode (exit side)
|
||||
|
||||
So `--amount 1` produces 3 ticketbooks total (one of each type), and `--amount 2` produces 6.
|
||||
|
||||
<Callout type="warning" emoji="⚠️">
|
||||
Each ticketbook costs 75 NYM. Because `--amount` applies per type, `--amount 1` deposits **225 NYM** (75 × 3 types), and `--amount 2` deposits **450 NYM**. If you only use two-hop (WireGuard) mode, the Mixnet Entry ticketbooks will go unused. There is currently no way to obtain ticketbooks for a single type.
|
||||
</Callout>
|
||||
|
||||
This command:
|
||||
|
||||
- Deposits `$NYM` into the ticketbook smart contract (plus a small fee buffer per deposit)
|
||||
- Requests credential issuance from the decentralised Nym API validators
|
||||
- Stores the resulting zk-nym ticketbooks locally on the device
|
||||
|
||||
### Connect
|
||||
|
||||
Connect using the locally stored ticketbooks:
|
||||
|
||||
```sh
|
||||
nym-vpnc connect-v2
|
||||
```
|
||||
|
||||
The CLI uses the ticketbooks to authenticate with entry nodes (gateways) and connect to the Nym network. You will see the connection happening in the `nym-vpnd` logs.
|
||||
|
||||
## Tunnel Configuration
|
||||
|
||||
Print current tunnel configuration:
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-api"
|
||||
version = "1.1.78"
|
||||
version = "1.1.79"
|
||||
authors.workspace = true
|
||||
edition = "2021"
|
||||
license = "GPL-3.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-credential-proxy"
|
||||
version = "0.3.2-rc"
|
||||
version = "0.3.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -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,96 @@ 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.
|
||||
fn generate_exit_policy_ports() -> anyhow::Result<()> {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
let script_path = PathBuf::from("../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 {
|
||||
if 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";
|
||||
|
||||
|
||||
@@ -62,21 +62,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 +207,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 +445,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 +469,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 +588,92 @@ func ping(req NetstackRequestGo) (NetstackResponse, error) {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// checkPorts tests TCP connectivity to a target on each port through the WG
|
||||
// tunnel. blocked ports (NYM-EXIT iptables) will timeout or get reset.
|
||||
// runs concurrently.
|
||||
func checkPorts(target string, ports []uint16, timeoutSec uint64, tnet *netstack.Net) map[string]bool {
|
||||
if target == "" {
|
||||
target = "portquiz.net"
|
||||
}
|
||||
if timeoutSec == 0 {
|
||||
timeoutSec = 5
|
||||
}
|
||||
|
||||
// resolve target once via host DNS (we only care about TCP reachability
|
||||
// through the tunnel, not DNS-through-tunnel behaviour)
|
||||
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 {
|
||||
targetIP = addrs[0]
|
||||
log.Printf("Port check: resolved %s -> %s", target, targetIP)
|
||||
}
|
||||
}
|
||||
|
||||
timeout := time.Duration(timeoutSec) * time.Second
|
||||
|
||||
// batching: portquiz.net enforces a per-source-IP connection limit.
|
||||
// testing all 114 ports in one burst trips it after ~29 connections.
|
||||
// small batches with a cooldown in between keep us under the limit.
|
||||
const batchSize = 20
|
||||
const batchDelay = 25 * time.Second
|
||||
const dialGap = 200 * time.Millisecond
|
||||
|
||||
results := make(map[string]bool)
|
||||
totalBatches := (len(ports) + batchSize - 1) / batchSize
|
||||
|
||||
for batchIdx := 0; batchIdx < totalBatches; batchIdx++ {
|
||||
start := batchIdx * batchSize
|
||||
end := start + batchSize
|
||||
if end > len(ports) {
|
||||
end = len(ports)
|
||||
}
|
||||
batch := ports[start:end]
|
||||
|
||||
if batchIdx > 0 {
|
||||
log.Printf("Batch %d/%d: cooldown %v before next batch...",
|
||||
batchIdx+1, totalBatches, batchDelay)
|
||||
time.Sleep(batchDelay)
|
||||
}
|
||||
|
||||
log.Printf("Batch %d/%d: testing %d ports (%d–%d)...",
|
||||
batchIdx+1, totalBatches, len(batch), batch[0], batch[len(batch)-1])
|
||||
|
||||
for i, p := range batch {
|
||||
if i > 0 {
|
||||
time.Sleep(dialGap)
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", targetIP, p)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
conn, err := tnet.DialContext(ctx, "tcp", addr)
|
||||
cancel()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Port %d: CLOSED (%v)", p, err)
|
||||
results[fmt.Sprintf("%d", p)] = false
|
||||
} else {
|
||||
conn.Close()
|
||||
log.Printf("Port %d: OPEN", p)
|
||||
results[fmt.Sprintf("%d", p)] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -25,6 +26,11 @@ pub struct NetstackRequest {
|
||||
v6_ping_config: PingConfig,
|
||||
download_timeout_sec: u64,
|
||||
awg_args: String,
|
||||
// exit policy port check
|
||||
port_check_target: String,
|
||||
port_check_ports: Vec<u16>,
|
||||
port_check_only: bool,
|
||||
port_check_timeout_sec: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
@@ -76,6 +82,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 +93,10 @@ 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_target: netstack_args.port_check_target.clone(),
|
||||
port_check_ports: netstack_args.port_check_ports.clone(),
|
||||
port_check_only,
|
||||
port_check_timeout_sec: netstack_args.port_check_timeout_sec,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +127,11 @@ pub struct NetstackRequestGo {
|
||||
recv_timeout_sec: u64,
|
||||
download_timeout_sec: u64,
|
||||
awg_args: String,
|
||||
// exit policy port check
|
||||
port_check_target: String,
|
||||
port_check_ports: Vec<u16>,
|
||||
port_check_only: bool,
|
||||
port_check_timeout_sec: u64,
|
||||
}
|
||||
|
||||
impl NetstackRequestGo {
|
||||
@@ -135,6 +151,10 @@ 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_target: req.port_check_target.clone(),
|
||||
port_check_ports: req.port_check_ports.clone(),
|
||||
port_check_only: req.port_check_only,
|
||||
port_check_timeout_sec: req.port_check_timeout_sec,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +175,10 @@ 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_target: req.port_check_target.clone(),
|
||||
port_check_ports: req.port_check_ports.clone(),
|
||||
port_check_only: req.port_check_only,
|
||||
port_check_timeout_sec: req.port_check_timeout_sec,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,6 +197,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> {
|
||||
@@ -149,6 +150,7 @@ pub async fn wg_probe(
|
||||
&tunnel_config,
|
||||
&netstack_args,
|
||||
&awg_args.unwrap_or_default(),
|
||||
port_check_only,
|
||||
&mut wg_outcome,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,15 +1,57 @@
|
||||
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,
|
||||
pub used_entry: String,
|
||||
pub outcome: ProbeOutcome,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ports_check: Option<PortsCheckSummary>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
@@ -50,6 +92,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 +241,19 @@ 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>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct IpPingReplies {
|
||||
pub ipr_tun_ip_v4: bool,
|
||||
|
||||
@@ -63,6 +63,7 @@ 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.
|
||||
@@ -71,6 +72,7 @@ impl WgTunnelConfig {
|
||||
/// * `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)
|
||||
/// * `port_check_only` - If true, skip pings/download and only run TCP port checks
|
||||
/// * `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.
|
||||
@@ -78,6 +80,7 @@ pub fn run_tunnel_tests(
|
||||
config: &WgTunnelConfig,
|
||||
netstack_args: &NetstackArgs,
|
||||
awg_args: &str,
|
||||
port_check_only: bool,
|
||||
wg_outcome: &mut WgProbeResults,
|
||||
) {
|
||||
// Build the netstack request
|
||||
@@ -91,9 +94,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 +126,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 +140,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;
|
||||
}
|
||||
|
||||
// Perform IPv6 ping test
|
||||
info!("Testing IPv6 tunnel connectivity...");
|
||||
let ipv6_request = NetstackRequestGo::from_rust_v6(&netstack_request);
|
||||
|
||||
@@ -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,280 @@ 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>,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
authenticator: nym_sdk::mixnet::Recipient,
|
||||
authenticator_version: nym_authenticator_requests::AuthenticatorVersion,
|
||||
ip_address: IpAddr,
|
||||
bandwidth_provider: &dyn BandwidthTicketProvider,
|
||||
wg_ticket_type: TicketType,
|
||||
credential_provider: nym_sdk::mixnet::NodeIdentity,
|
||||
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(wg_ticket_type, 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,
|
||||
authenticator,
|
||||
authenticator_version,
|
||||
Arc::new(x25519::KeyPair::new(&mut rng)),
|
||||
ip_address,
|
||||
);
|
||||
|
||||
match wg_probe(
|
||||
auth_client,
|
||||
ip_address,
|
||||
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.first().unwrap() {
|
||||
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,
|
||||
setup.authenticator,
|
||||
setup.exit_node.authenticator_version,
|
||||
setup.ip_address,
|
||||
bandwidth_provider,
|
||||
TicketType::V1WireguardExit,
|
||||
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 +368,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,
|
||||
@@ -256,6 +533,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,
|
||||
@@ -276,6 +721,7 @@ impl Probe {
|
||||
lp: None,
|
||||
socks5: None,
|
||||
},
|
||||
ports_check: None,
|
||||
};
|
||||
|
||||
let mixnet_client = match mixnet_client {
|
||||
@@ -356,6 +802,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(),
|
||||
@@ -384,6 +831,7 @@ impl Probe {
|
||||
exit_node.authenticator_version,
|
||||
self.config.amnezia_args.clone(),
|
||||
self.config.netstack_args.clone(),
|
||||
false,
|
||||
credential,
|
||||
)
|
||||
.await
|
||||
@@ -391,6 +839,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(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(ProbeOutput::Standard)
|
||||
}
|
||||
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(ProbeOutput::Standard)
|
||||
}
|
||||
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(ProbeOutput::Standard)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-node-status-agent"
|
||||
version = "2.0.0"
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,9 @@ use nym_bin_common::bin_info;
|
||||
use nym_crypto::asymmetric::ed25519::PrivateKey;
|
||||
use std::{env, sync::OnceLock};
|
||||
|
||||
pub(crate) mod common;
|
||||
pub(crate) mod generate_keypair;
|
||||
pub(crate) mod run_ports_check;
|
||||
pub(crate) mod run_probe;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -20,7 +22,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());
|
||||
@@ -51,6 +53,7 @@ pub(crate) struct Args {
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub(crate) enum Command {
|
||||
RunProbe(RunProbeArgs),
|
||||
RunPortsCheck(RunPortsCheckArgs),
|
||||
|
||||
GenerateKeypair {
|
||||
#[arg(long)]
|
||||
@@ -71,21 +74,31 @@ 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 {
|
||||
Command::RunProbe(args) => {
|
||||
// Parse server configs
|
||||
let mut servers = Vec::new();
|
||||
for s in &args.server {
|
||||
match parse_server_config(s) {
|
||||
Ok(config) => servers.push(config),
|
||||
Err(e) => {
|
||||
tracing::error!("Invalid server config '{}': {}", s, e);
|
||||
anyhow::bail!("Invalid server config '{}': {}", s, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
let servers = common::parse_servers(&args.server)?;
|
||||
|
||||
run_probe::run_probe(&servers, args.probe_config, log_capture)
|
||||
.await
|
||||
@@ -93,6 +106,21 @@ impl Args {
|
||||
tracing::error!("{err}");
|
||||
})?
|
||||
}
|
||||
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,131 @@
|
||||
use crate::cli::ServerConfig;
|
||||
use crate::cli::common;
|
||||
use crate::log_capture::LogCapture;
|
||||
use nym_gateway_probe::RunPortsConfig;
|
||||
use tracing::instrument;
|
||||
|
||||
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 port_check_result = nym_gateway_probe::Probe::run_ports_for_agent(
|
||||
gateway_identity_pubkey,
|
||||
network,
|
||||
&run_ports_config,
|
||||
credentials_args,
|
||||
)
|
||||
.await?;
|
||||
let probe_log = log_capture.stop_and_drain();
|
||||
|
||||
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,12 +44,7 @@ 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();
|
||||
@@ -110,15 +88,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
|
||||
|
||||
+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"
|
||||
}
|
||||
+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"
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n WITH oldest_queued AS (\n SELECT id\n FROM testruns\n WHERE status = $1\n ORDER BY created_utc asc\n LIMIT 1\n FOR UPDATE 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": "38439a6c33bf21b90032659797105b6af747c3ae6f48bc41e5ec509a2d87abcc"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE gateways SET last_ports_check_utc = $1, last_updated_utc = $1 WHERE id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3c6da5c1faace026222bbb28553d8ea15b957dd19b097101dcda2c05aee21d20"
|
||||
}
|
||||
+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"
|
||||
}
|
||||
+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"
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n WITH oldest_queued AS (\n SELECT id\n FROM testruns\n WHERE status = $1 AND kind = $4\n ORDER BY created_utc asc\n LIMIT 1\n FOR UPDATE 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": "ad78bf2f7ed4666b4e5f60811f04cb8bf8ebb1c26839f915780d0fc0d1939973"
|
||||
}
|
||||
+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"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT last_probe_result FROM gateways WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "last_probe_result",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "cc84f8a284c9aba4494152ff4e6087af53e9504a8bb13280dde5c37b9b418370"
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-node-status-api"
|
||||
version = "4.6.1"
|
||||
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);
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
-- Persistent port-scan results (e.g. exit policy) live beside last_probe_result in the API,
|
||||
-- not embedded in the probe JSON blob, so frequent probe runs do not wipe 14-day port checks.
|
||||
ALTER TABLE gateways
|
||||
ADD COLUMN IF NOT EXISTS ports_check JSONB;
|
||||
|
||||
ALTER TABLE gateways
|
||||
ADD COLUMN IF NOT EXISTS last_ports_check_utc BIGINT;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
-- Exit port check summary lives in `ports_check`, not inside `last_probe_result` JSON.
|
||||
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::jsonb ? 'ports_check';
|
||||
|
||||
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::jsonb ? 'ports_check';
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- `20260416000000` may have copied `ports_check` from legacy JSON without setting `last_ports_check_utc`.
|
||||
UPDATE gateways
|
||||
SET last_ports_check_utc = last_updated_utc
|
||||
WHERE ports_check IS NOT NULL
|
||||
AND jsonb_typeof(ports_check) = 'object'
|
||||
AND last_ports_check_utc IS NULL;
|
||||
@@ -82,6 +82,15 @@ pub(crate) struct Cli {
|
||||
#[arg(value_parser = parse_duration_std)]
|
||||
pub(crate) testruns_refresh_interval: Duration,
|
||||
|
||||
/// 2 hour safety net for test runs which include a port check
|
||||
#[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,123 @@ use strum_macros::{EnumString, FromRepr};
|
||||
use time::{Date, OffsetDateTime, UtcDateTime};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub(crate) fn detach_ports_check_from_probe_json(
|
||||
mut probe: serde_json::Value,
|
||||
) -> (serde_json::Value, Option<serde_json::Value>) {
|
||||
let nested = match &mut probe {
|
||||
serde_json::Value::Object(map) => map.remove("ports_check").filter(|v| !v.is_null()),
|
||||
_ => None,
|
||||
};
|
||||
(probe, nested)
|
||||
}
|
||||
|
||||
fn strip_wg_port_check_results_from_last_probe(value: &mut serde_json::Value) {
|
||||
let Some(outcome) = value.get_mut("outcome").and_then(|o| o.as_object_mut()) else {
|
||||
return;
|
||||
};
|
||||
let Some(wg) = outcome.get_mut("wg").and_then(|w| w.as_object_mut()) else {
|
||||
return;
|
||||
};
|
||||
wg.remove("port_check_results");
|
||||
}
|
||||
|
||||
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 +165,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 +192,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 +200,27 @@ 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_parsed =
|
||||
serde_json::from_str::<serde_json::Value>(&last_probe_result_raw).ok();
|
||||
let (last_probe_result, nested_ports) = last_probe_parsed
|
||||
.map(detach_ports_check_from_probe_json)
|
||||
.map(|(v, n)| {
|
||||
let mut v = v;
|
||||
strip_wg_port_check_results_from_last_probe(&mut v);
|
||||
let v = (!v.is_null()).then_some(v);
|
||||
(v, n)
|
||||
})
|
||||
.unwrap_or((None, None));
|
||||
|
||||
let ports_check = value
|
||||
.ports_check
|
||||
.clone()
|
||||
.or(nested_ports)
|
||||
.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 +243,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 +433,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 +448,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,
|
||||
|
||||
@@ -77,23 +77,24 @@ pub(crate) async fn update_bonded_gateways(
|
||||
|
||||
pub(crate) async fn get_all_gateways(pool: &DbPool) -> anyhow::Result<Vec<Gateway>> {
|
||||
let mut conn = pool.acquire().await?;
|
||||
let items = sqlx::query_as!(
|
||||
GatewayDto,
|
||||
let items = sqlx::query_as::<_, GatewayDto>(
|
||||
r#"SELECT
|
||||
gw.gateway_identity_key as "gateway_identity_key!",
|
||||
gw.bonded as "bonded: bool",
|
||||
gw.performance as "performance!",
|
||||
gw.self_described as "self_described?",
|
||||
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.last_testrun_utc as "last_testrun_utc?",
|
||||
gw.last_updated_utc as "last_updated_utc!",
|
||||
gw.bridges as "bridges?: serde_json::Value",
|
||||
COALESCE(gd.moniker, 'NA') as "moniker!",
|
||||
COALESCE(gd.website, 'NA') as "website!",
|
||||
COALESCE(gd.security_contact, 'NA') as "security_contact!",
|
||||
COALESCE(gd.details, 'NA') as "details!"
|
||||
gw.gateway_identity_key,
|
||||
gw.bonded,
|
||||
gw.performance,
|
||||
gw.self_described,
|
||||
gw.explorer_pretty_bond,
|
||||
gw.last_probe_result,
|
||||
gw.last_probe_log,
|
||||
gw.ports_check,
|
||||
gw.last_ports_check_utc,
|
||||
gw.last_testrun_utc,
|
||||
gw.last_updated_utc,
|
||||
COALESCE(gd.moniker, 'NA') AS moniker,
|
||||
COALESCE(gd.security_contact, 'NA') AS security_contact,
|
||||
COALESCE(gd.details, 'NA') AS details,
|
||||
COALESCE(gd.website, 'NA') AS website,
|
||||
gw.bridges
|
||||
FROM gateways gw
|
||||
LEFT JOIN gateway_description gd
|
||||
ON gw.gateway_identity_key = gd.gateway_identity_key
|
||||
|
||||
@@ -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(
|
||||
@@ -32,6 +34,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 +93,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
|
||||
@@ -99,7 +115,7 @@ pub(crate) async fn assign_oldest_testrun(
|
||||
WITH oldest_queued AS (
|
||||
SELECT id
|
||||
FROM testruns
|
||||
WHERE status = $1
|
||||
WHERE status = $1 AND kind = $4
|
||||
ORDER BY created_utc asc
|
||||
LIMIT 1
|
||||
FOR UPDATE SKIP LOCKED
|
||||
@@ -117,28 +133,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)
|
||||
@@ -183,17 +198,138 @@ 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,
|
||||
use crate::db::models::detach_ports_check_from_probe_json;
|
||||
|
||||
let value: serde_json::Value = serde_json::from_str(result)
|
||||
.map_err(|e| anyhow::anyhow!("Invalid probe result JSON: {e}"))?;
|
||||
let (stripped_json, ports_check) = detach_ports_check_from_probe_json(value);
|
||||
let stripped = serde_json::to_string(&stripped_json)?;
|
||||
let now_ts = crate::utils::now_utc().unix_timestamp();
|
||||
let ports_check_ts = ports_check.as_ref().map(|_| now_ts);
|
||||
|
||||
sqlx::query(
|
||||
r#"UPDATE gateways SET
|
||||
last_probe_result = $1,
|
||||
ports_check = COALESCE($2, ports_check),
|
||||
last_ports_check_utc = CASE WHEN $2 IS NOT NULL THEN $3 ELSE last_ports_check_utc END
|
||||
WHERE id = $4"#,
|
||||
)
|
||||
.bind(&stripped)
|
||||
.bind(ports_check.map(Json))
|
||||
.bind(ports_check_ts)
|
||||
.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,
|
||||
@@ -221,6 +357,7 @@ pub(crate) async fn get_testrun_by_id(
|
||||
id,
|
||||
gateway_id,
|
||||
status,
|
||||
kind,
|
||||
created_utc,
|
||||
ip_address,
|
||||
log,
|
||||
@@ -247,14 +384,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,138 @@ mod db_tests {
|
||||
assert_eq!(http_gateway.description.details, "details");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gateway_dto_nested_ports_check_detached_to_top_level() {
|
||||
let ports_payload = serde_json::json!({"https": 443, "wg": 51822});
|
||||
let mut probe = serde_json::json!({
|
||||
"node": "n1",
|
||||
"used_entry": "e1",
|
||||
"outcome": {"as_entry": "NotTested"},
|
||||
"ports_check": ports_payload
|
||||
});
|
||||
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: Some(probe.to_string()),
|
||||
last_probe_log: None,
|
||||
ports_check: None,
|
||||
last_ports_check_utc: None,
|
||||
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();
|
||||
|
||||
assert_eq!(http_gateway.ports_check, Some(ports_payload));
|
||||
assert!(http_gateway.last_ports_check_utc.is_none());
|
||||
|
||||
probe.as_object_mut().unwrap().remove("ports_check");
|
||||
assert_eq!(http_gateway.last_probe_result, Some(probe));
|
||||
}
|
||||
|
||||
#[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 +401,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(),
|
||||
|
||||
@@ -85,14 +85,14 @@ async fn get_gateway(
|
||||
) -> HttpResult<Json<Gateway>> {
|
||||
let db = state.db_pool();
|
||||
let res = state.cache().get_gateway_list(db).await;
|
||||
|
||||
match res
|
||||
.iter()
|
||||
.find(|item| item.gateway_identity_key == identity_key)
|
||||
{
|
||||
Some(res) => Ok(Json(res.clone())),
|
||||
None => Err(HttpError::invalid_input(identity_key)),
|
||||
}
|
||||
res.into_iter()
|
||||
.find(|g| g.gateway_identity_key == identity_key)
|
||||
.map(Json)
|
||||
.ok_or_else(|| {
|
||||
HttpError::invalid_input(format!(
|
||||
"gateway not found in local gateway cache: {identity_key}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
// Extract filtering logic for testing
|
||||
@@ -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,
|
||||
|
||||
@@ -79,6 +79,8 @@ mod tests {
|
||||
},
|
||||
last_probe_result: None,
|
||||
last_probe_log: None,
|
||||
ports_check: None,
|
||||
last_ports_check_utc: None,
|
||||
last_testrun_utc: None,
|
||||
last_updated_utc: "2025-01-01T12:00:00Z".to_string(),
|
||||
routing_score: 1.0,
|
||||
|
||||
@@ -191,6 +191,8 @@ mod tests {
|
||||
},
|
||||
last_probe_result: None,
|
||||
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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::db::DbConnection;
|
||||
use crate::db::models::{TestRunDto, TestRunStatus};
|
||||
use crate::db::models::{TestRunDto, TestRunKind, TestRunStatus};
|
||||
use crate::db::queries;
|
||||
use crate::utils::{now_utc, unix_timestamp_to_utc_rfc3339};
|
||||
use crate::{
|
||||
@@ -16,7 +16,8 @@ use axum::{
|
||||
extract::{Path, State},
|
||||
};
|
||||
use nym_node_status_client::models::{
|
||||
TestrunAssignmentWithTickets, get_testrun, submit_results, submit_results_v2,
|
||||
TestrunAssignmentWithTickets, get_testrun, submit_ports_check_results_v2, submit_results,
|
||||
submit_results_v2,
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use tracing::error;
|
||||
@@ -26,8 +27,16 @@ use tracing::error;
|
||||
pub(crate) fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", axum::routing::get(request_testrun))
|
||||
.route(
|
||||
"/ports-check",
|
||||
axum::routing::get(request_ports_check_testrun),
|
||||
)
|
||||
.route("/:testrun_id", axum::routing::post(submit_testrun))
|
||||
.route("/:testrun_id/v2", axum::routing::post(submit_testrun_v2))
|
||||
.route(
|
||||
"/:testrun_id/ports-check/v2",
|
||||
axum::routing::post(submit_ports_check_testrun_v2),
|
||||
)
|
||||
.layer(DefaultBodyLimit::max(1024 * 1024 * 5))
|
||||
}
|
||||
|
||||
@@ -90,6 +99,64 @@ async fn request_testrun(
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn request_ports_check_testrun(
|
||||
State(state): State<AppState>,
|
||||
Json(request): Json<get_testrun::GetTestrunRequest>,
|
||||
) -> HttpResult<Json<TestrunAssignmentWithTickets>> {
|
||||
state.authenticate_agent_submission(&request)?;
|
||||
state.is_fresh(&request.payload.timestamp)?;
|
||||
|
||||
tracing::debug!("Agent requested ports-check testrun");
|
||||
|
||||
let db = state.db_pool();
|
||||
let mut conn = db
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(HttpError::internal_with_logging)?;
|
||||
|
||||
let active_testruns = db::queries::testruns::count_testruns_in_progress(&mut conn)
|
||||
.await
|
||||
.map_err(HttpError::internal_with_logging)?
|
||||
.unwrap_or_default();
|
||||
let max_count = state.agent_max_count();
|
||||
if active_testruns >= max_count {
|
||||
tracing::warn!("{active_testruns}/{max_count} testruns in progress, rejecting",);
|
||||
return Err(HttpError::no_testruns_available());
|
||||
}
|
||||
|
||||
match db::queries::testruns::assign_oldest_ports_check_testrun(&mut conn).await {
|
||||
Ok(res) => {
|
||||
let Some(assignment) = res else {
|
||||
tracing::debug!("No ports-check testruns available");
|
||||
return Err(HttpError::no_testruns_available());
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
"🏃 Assigned ports-check testrun row_id {} gateway {} to agent",
|
||||
&assignment.testrun_id,
|
||||
assignment.gateway_identity_key,
|
||||
);
|
||||
|
||||
let materials = state
|
||||
.ticketbook_manager_state()
|
||||
.attempt_assign_ticket_materials(assignment.testrun_id)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!(
|
||||
"failed to get ticket materials for runner {}: {err}",
|
||||
assignment.testrun_id
|
||||
);
|
||||
HttpError::internal_with_logging(format!(
|
||||
"could not retrieve needed tickets: {err}"
|
||||
))
|
||||
})?;
|
||||
Ok(Json(assignment.with_ticket_materials(materials)))
|
||||
}
|
||||
Err(err) => Err(HttpError::internal_with_logging(err)),
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn submit_testrun(
|
||||
Path(submitted_testrun_id): Path<i32>,
|
||||
@@ -338,3 +405,148 @@ async fn process_testrun_submission_by_gateway(
|
||||
|
||||
Ok(StatusCode::CREATED)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn submit_ports_check_testrun_v2(
|
||||
Path(submitted_testrun_id): Path<i32>,
|
||||
State(state): State<AppState>,
|
||||
Json(submission): Json<submit_ports_check_results_v2::SubmitPortsCheckResultsV2>,
|
||||
) -> HttpResult<StatusCode> {
|
||||
state.authenticate_agent_submission(&submission)?;
|
||||
|
||||
let db = state.db_pool();
|
||||
let mut conn = db
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(HttpError::internal_with_logging)?;
|
||||
|
||||
match queries::testruns::get_testrun_by_id(&mut conn, submitted_testrun_id).await {
|
||||
Ok(testrun) => {
|
||||
if testrun.kind != TestRunKind::PortsCheck as i16 {
|
||||
tracing::warn!(
|
||||
"Testrun {} has wrong kind for ports-check submit: {}",
|
||||
submitted_testrun_id,
|
||||
testrun.kind
|
||||
);
|
||||
return Err(HttpError::invalid_input(format!(
|
||||
"Testrun {} is not a ports-check run",
|
||||
submitted_testrun_id
|
||||
)));
|
||||
}
|
||||
|
||||
let gw_identity = queries::select_gateway_identity(&mut conn, testrun.gateway_id)
|
||||
.await
|
||||
.map_err(HttpError::internal_with_logging)?;
|
||||
|
||||
if gw_identity != submission.payload.gateway_identity_key {
|
||||
tracing::warn!(
|
||||
"Gateway mismatch for ports-check testrun {}: expected {}, got {}",
|
||||
submitted_testrun_id,
|
||||
gw_identity,
|
||||
submission.payload.gateway_identity_key
|
||||
);
|
||||
return Err(HttpError::invalid_input("Gateway identity mismatch"));
|
||||
}
|
||||
|
||||
if Some(submission.payload.assigned_at_utc) != testrun.last_assigned_utc {
|
||||
tracing::warn!(
|
||||
"Submitted ports-check testrun timestamp mismatch: {} != {:?}, rejecting",
|
||||
submission.payload.assigned_at_utc,
|
||||
testrun.last_assigned_utc
|
||||
);
|
||||
return Err(HttpError::invalid_input(format!(
|
||||
"Ports-check testrun {} timestamp mismatch: expected {:?}, got {}",
|
||||
submitted_testrun_id,
|
||||
testrun.last_assigned_utc,
|
||||
submission.payload.assigned_at_utc
|
||||
)));
|
||||
}
|
||||
|
||||
process_ports_check_submission(
|
||||
submitted_testrun_id,
|
||||
testrun.gateway_id,
|
||||
submission.payload,
|
||||
&mut conn,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::info!(
|
||||
"Creating external ports-check testrun {} for gateway {}",
|
||||
submitted_testrun_id,
|
||||
submission.payload.gateway_identity_key
|
||||
);
|
||||
|
||||
let gateway_id =
|
||||
queries::get_or_create_gateway(&mut conn, &submission.payload.gateway_identity_key)
|
||||
.await
|
||||
.map_err(HttpError::internal_with_logging)?;
|
||||
|
||||
queries::testruns::insert_external_ports_check_testrun(
|
||||
&mut conn,
|
||||
submitted_testrun_id,
|
||||
gateway_id,
|
||||
submission.payload.assigned_at_utc,
|
||||
)
|
||||
.await
|
||||
.map_err(HttpError::internal_with_logging)?;
|
||||
|
||||
process_ports_check_submission(
|
||||
submitted_testrun_id,
|
||||
gateway_id,
|
||||
submission.payload,
|
||||
&mut conn,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_ports_check_submission(
|
||||
testrun_id: i32,
|
||||
gateway_id: i32,
|
||||
payload: submit_ports_check_results_v2::Payload,
|
||||
conn: &mut DbConnection,
|
||||
) -> HttpResult<StatusCode> {
|
||||
let gw_identity = &payload.gateway_identity_key;
|
||||
let open_ports = payload
|
||||
.port_check_result
|
||||
.ports
|
||||
.values()
|
||||
.filter(|&&v| v)
|
||||
.count();
|
||||
let total_ports = payload.port_check_result.ports.len();
|
||||
|
||||
tracing::debug!(
|
||||
"Processing ports-check submission for gateway {} ({} bytes, {}/{} ports open, can_register={})",
|
||||
gw_identity,
|
||||
payload.probe_log.len(),
|
||||
open_ports,
|
||||
total_ports,
|
||||
payload.port_check_result.can_register,
|
||||
);
|
||||
|
||||
queries::testruns::update_gateway_ports_check_only(
|
||||
conn,
|
||||
gateway_id,
|
||||
&payload.port_check_result,
|
||||
)
|
||||
.await
|
||||
.map_err(HttpError::internal_with_logging)?;
|
||||
|
||||
queries::testruns::update_testrun_status(conn, testrun_id, TestRunStatus::Complete)
|
||||
.await
|
||||
.map_err(HttpError::internal_with_logging)?;
|
||||
|
||||
let assigned_at = unix_timestamp_to_utc_rfc3339(payload.assigned_at_utc);
|
||||
let now = now_utc();
|
||||
tracing::info!(
|
||||
"✅ Ports-check testrun {} for gateway {} complete (assigned at {}, current time {})",
|
||||
testrun_id,
|
||||
gw_identity,
|
||||
assigned_at,
|
||||
now
|
||||
);
|
||||
|
||||
Ok(StatusCode::CREATED)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ fn test_weighted_score_calculation() {
|
||||
},
|
||||
last_probe_result: None,
|
||||
last_probe_log: None,
|
||||
ports_check: None,
|
||||
last_ports_check_utc: None,
|
||||
last_testrun_utc: None,
|
||||
last_updated_utc: "2025-10-10T00:00:00Z".to_string(),
|
||||
routing_score: 0.0,
|
||||
@@ -163,10 +165,12 @@ fn conversion_from_gw_probe_latest() {
|
||||
downloaded_file_size_bytes_v6: 20971520,
|
||||
downloaded_file_v6: "test-file-v6.bin".to_string(),
|
||||
download_error_v6: "none-v6".to_string(),
|
||||
port_check_results: None,
|
||||
};
|
||||
let probe_latest = ProbeResultLatest {
|
||||
node: "test-node-identity-key".to_string(),
|
||||
used_entry: "test-entry-node".to_string(),
|
||||
ports_check: None,
|
||||
outcome: ProbeOutcomeLatest {
|
||||
as_entry: EntryLatest::Tested(EntryTestResultLatest {
|
||||
can_connect: true,
|
||||
|
||||
@@ -43,6 +43,8 @@ pub struct Gateway {
|
||||
pub description: NodeDescription,
|
||||
pub last_probe_result: Option<serde_json::Value>,
|
||||
pub last_probe_log: Option<String>,
|
||||
pub ports_check: Option<serde_json::Value>,
|
||||
pub last_ports_check_utc: Option<String>,
|
||||
pub last_testrun_utc: Option<String>,
|
||||
pub last_updated_utc: String,
|
||||
pub routing_score: f32,
|
||||
@@ -566,6 +568,8 @@ mod test {
|
||||
self_described: Some(serde_json::json!({"test": "value"})),
|
||||
explorer_pretty_bond: None,
|
||||
last_probe_result: Some(serde_json::json!({"status": "ok"})),
|
||||
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,
|
||||
@@ -628,6 +632,8 @@ pub struct GatewaySkinny {
|
||||
pub self_described: Option<serde_json::Value>,
|
||||
pub explorer_pretty_bond: Option<serde_json::Value>,
|
||||
pub last_probe_result: Option<serde_json::Value>,
|
||||
pub ports_check: Option<serde_json::Value>,
|
||||
pub last_ports_check_utc: Option<String>,
|
||||
pub last_testrun_utc: Option<String>,
|
||||
pub last_updated_utc: String,
|
||||
pub routing_score: f32,
|
||||
|
||||
@@ -9,6 +9,7 @@ use core::net::SocketAddr;
|
||||
use nym_crypto::asymmetric::ed25519::PublicKey;
|
||||
use nym_task::ShutdownTracker;
|
||||
use std::sync::Arc;
|
||||
use tokio::time::MissedTickBehavior;
|
||||
use tokio::{net::TcpListener, sync::RwLock};
|
||||
|
||||
/// Return handles that allow for graceful shutdown of server + awaiting its
|
||||
@@ -27,6 +28,7 @@ pub(crate) async fn start_http_api(
|
||||
shutdown_tracker: &ShutdownTracker,
|
||||
) -> anyhow::Result<()> {
|
||||
let router_builder = RouterBuilder::with_default_routes();
|
||||
let db_pool_for_scheduler = db_pool.clone();
|
||||
|
||||
let state = AppState::new(
|
||||
db_pool,
|
||||
@@ -46,6 +48,34 @@ pub(crate) async fn start_http_api(
|
||||
let server = router.build_server(bind_addr).await?;
|
||||
let shutdown = shutdown_tracker.clone_shutdown_token().cancelled_owned();
|
||||
|
||||
let ports_check_scheduler_enabled = std::env::var("PORTS_CHECK_SCHEDULER_ENABLED")
|
||||
.ok()
|
||||
.map(|v| matches!(v.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"))
|
||||
.unwrap_or(true);
|
||||
|
||||
if ports_check_scheduler_enabled {
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(60 * 10));
|
||||
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
let scheduler_shutdown = shutdown_tracker.clone_shutdown_token().cancelled_owned();
|
||||
shutdown_tracker.spawn(async move {
|
||||
tokio::select! {
|
||||
_ = async {
|
||||
loop {
|
||||
interval.tick().await;
|
||||
match crate::db::queries::testruns::enqueue_due_ports_check_testruns(&db_pool_for_scheduler).await {
|
||||
Ok(enqueued) if enqueued > 0 => tracing::info!("Enqueued {enqueued} due ports-check testruns"),
|
||||
Ok(_) => {}
|
||||
Err(e) => tracing::warn!("Failed to enqueue due ports-check testruns: {e}"),
|
||||
}
|
||||
}
|
||||
} => {},
|
||||
_ = scheduler_shutdown => {}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
tracing::info!("Ports-check scheduler disabled (PORTS_CHECK_SCHEDULER_ENABLED=false)");
|
||||
}
|
||||
|
||||
shutdown_tracker.spawn(async move {
|
||||
axum::serve(
|
||||
server.listener,
|
||||
|
||||
@@ -122,10 +122,10 @@ impl AppState {
|
||||
return Err(HttpError::unauthorized());
|
||||
};
|
||||
|
||||
request.verify_signature().map_err(|_| {
|
||||
if request.verify_signature().is_err() {
|
||||
tracing::warn!("Signature verification failed, rejecting");
|
||||
HttpError::unauthorized()
|
||||
})?;
|
||||
return Err(HttpError::unauthorized());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -125,7 +125,12 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
let pool = storage.pool_owned();
|
||||
shutdown_manager.spawn_with_shutdown(async move {
|
||||
testruns::start(pool, args.testruns_refresh_interval).await
|
||||
testruns::start(
|
||||
pool,
|
||||
args.testruns_refresh_interval,
|
||||
args.testruns_stale_in_progress,
|
||||
)
|
||||
.await
|
||||
});
|
||||
|
||||
let db_pool_scraper = storage.pool_owned();
|
||||
|
||||
@@ -29,10 +29,13 @@ pub mod builders {
|
||||
},
|
||||
last_probe_result: None,
|
||||
last_probe_log: None,
|
||||
ports_check: None,
|
||||
last_ports_check_utc: None,
|
||||
last_testrun_utc: None,
|
||||
last_updated_utc: "2024-01-01T00:00:00Z".to_string(),
|
||||
routing_score: 0.95,
|
||||
config_score: 100,
|
||||
bridges: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,13 @@ use tracing::instrument;
|
||||
pub(crate) mod models;
|
||||
mod queue;
|
||||
|
||||
pub(crate) async fn start(pool: DbPool, refresh_interval: Duration) {
|
||||
pub(crate) async fn start(
|
||||
pool: DbPool,
|
||||
refresh_interval: Duration,
|
||||
stale_in_progress_after: Duration,
|
||||
) {
|
||||
loop {
|
||||
if let Err(e) = refresh_stale_testruns(&pool, refresh_interval).await {
|
||||
if let Err(e) = refresh_stale_testruns(&pool, stale_in_progress_after).await {
|
||||
tracing::error!("{e}");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::db::DbConnection;
|
||||
use crate::db::models::{GatewayInfoDto, TestRunDto, TestRunStatus};
|
||||
use crate::db::models::{GatewayInfoDto, TestRunDto, TestRunKind, TestRunStatus};
|
||||
use crate::testruns::models::TestRun;
|
||||
use crate::utils::now_utc;
|
||||
use anyhow::anyhow;
|
||||
@@ -53,15 +53,17 @@ pub(crate) async fn try_queue_testrun(
|
||||
id,
|
||||
gateway_id,
|
||||
status,
|
||||
kind,
|
||||
created_utc,
|
||||
ip_address,
|
||||
log,
|
||||
last_assigned_utc
|
||||
FROM testruns
|
||||
WHERE gateway_id = $1 AND status != 2
|
||||
WHERE gateway_id = $1 AND status != 2 AND kind = $2
|
||||
ORDER BY id DESC
|
||||
LIMIT 1"#,
|
||||
gateway_id
|
||||
gateway_id,
|
||||
TestRunKind::Probe as i16
|
||||
)
|
||||
.fetch(conn.as_mut())
|
||||
.try_collect::<Vec<_>>()
|
||||
|
||||
@@ -12,6 +12,10 @@ impl ApiPaths {
|
||||
format!("{}/internal/testruns", self.server_address)
|
||||
}
|
||||
|
||||
pub(super) fn request_ports_check_testrun(&self) -> String {
|
||||
format!("{}/internal/testruns/ports-check", self.server_address)
|
||||
}
|
||||
|
||||
pub(super) fn submit_results(&self, testrun_id: impl Display) -> String {
|
||||
format!("{}/internal/testruns/{}", self.server_address, testrun_id)
|
||||
}
|
||||
@@ -22,4 +26,11 @@ impl ApiPaths {
|
||||
self.server_address, testrun_id
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn submit_ports_check_results_v2(&self, testrun_id: impl Display) -> String {
|
||||
format!(
|
||||
"{}/internal/testruns/{}/ports-check/v2",
|
||||
self.server_address, testrun_id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use crate::models::{TestrunAssignmentWithTickets, get_testrun, submit_results, submit_results_v2};
|
||||
use crate::models::{
|
||||
TestrunAssignmentWithTickets, get_testrun, submit_ports_check_results_v2, submit_results,
|
||||
submit_results_v2,
|
||||
};
|
||||
use anyhow::bail;
|
||||
use api::ApiPaths;
|
||||
use nym_crypto::asymmetric::ed25519::{PrivateKey, Signature};
|
||||
@@ -76,6 +79,52 @@ impl NsApiClient {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all)]
|
||||
pub async fn request_ports_check_testrun(
|
||||
&self,
|
||||
) -> anyhow::Result<Option<TestrunAssignmentWithTickets>> {
|
||||
let target_url = self.api.request_ports_check_testrun();
|
||||
|
||||
let payload = get_testrun::Payload {
|
||||
agent_public_key: self.auth_key.public_key(),
|
||||
timestamp: time::UtcDateTime::now().unix_timestamp(),
|
||||
};
|
||||
let signature = self.sign_message(&payload)?;
|
||||
let request = get_testrun::GetTestrunRequest { payload, signature };
|
||||
|
||||
let res = self.client.get(target_url).json(&request).send().await?;
|
||||
let status = res.status();
|
||||
let response_text = res.text().await?;
|
||||
|
||||
if status.is_client_error() {
|
||||
if matches!(status, reqwest::StatusCode::NOT_FOUND) {
|
||||
tracing::warn!(
|
||||
"Ports-check request endpoint not available on server (404), skipping iteration"
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
bail!("{}: {}", status, response_text);
|
||||
} else if status.is_server_error() {
|
||||
if matches!(status, reqwest::StatusCode::SERVICE_UNAVAILABLE)
|
||||
&& response_text.contains("No testruns available")
|
||||
{
|
||||
return Ok(None);
|
||||
} else {
|
||||
bail!("{}: {}", status, response_text);
|
||||
}
|
||||
}
|
||||
|
||||
serde_json::from_str(&response_text)
|
||||
.map(|testrun| {
|
||||
tracing::info!("Received ports-check testrun assignment: {:?}", testrun);
|
||||
testrun
|
||||
})
|
||||
.map_err(|err| {
|
||||
tracing::error!("err");
|
||||
err.into()
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "info", fields(testrun_id, assigned_at_utc), skip_all)]
|
||||
pub async fn submit_results(
|
||||
&self,
|
||||
@@ -140,6 +189,40 @@ impl NsApiClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip(self, port_check_result))]
|
||||
pub async fn submit_ports_check_results_with_context(
|
||||
&self,
|
||||
testrun_id: i32,
|
||||
port_check_result: nym_gateway_probe::PortCheckResult,
|
||||
probe_log: String,
|
||||
assigned_at_utc: i64,
|
||||
gateway_identity_key: String,
|
||||
) -> anyhow::Result<()> {
|
||||
let target_url = self.api.submit_ports_check_results_v2(testrun_id);
|
||||
|
||||
let payload = submit_ports_check_results_v2::Payload {
|
||||
port_check_result,
|
||||
probe_log,
|
||||
agent_public_key: self.auth_key.public_key(),
|
||||
assigned_at_utc,
|
||||
gateway_identity_key,
|
||||
};
|
||||
let signature = self.sign_message(&payload)?;
|
||||
let submit =
|
||||
submit_ports_check_results_v2::SubmitPortsCheckResultsV2 { payload, signature };
|
||||
|
||||
let res = self
|
||||
.client
|
||||
.post(target_url)
|
||||
.json(&submit)
|
||||
.send()
|
||||
.await
|
||||
.and_then(|response| response.error_for_status())?;
|
||||
|
||||
tracing::debug!("Submitted ports-check results: {})", res.status());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sign_message<T>(&self, message: &T) -> anyhow::Result<Signature>
|
||||
where
|
||||
T: serde::Serialize,
|
||||
|
||||
@@ -41,6 +41,9 @@ pub struct TestrunAssignment {
|
||||
pub testrun_id: i32,
|
||||
pub assigned_at_utc: i64,
|
||||
pub gateway_identity_key: String,
|
||||
/// Unix timestamp (seconds) of the last persisted exit-policy port check for this gateway.
|
||||
#[serde(default)]
|
||||
pub last_ports_check_utc: Option<i64>,
|
||||
}
|
||||
|
||||
impl TestrunAssignment {
|
||||
@@ -143,3 +146,39 @@ pub mod submit_results_v2 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod submit_ports_check_results_v2 {
|
||||
use crate::auth::SignedRequest;
|
||||
|
||||
use super::*;
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Payload {
|
||||
pub port_check_result: nym_gateway_probe::PortCheckResult,
|
||||
pub probe_log: String,
|
||||
pub agent_public_key: PublicKey,
|
||||
pub assigned_at_utc: i64,
|
||||
pub gateway_identity_key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct SubmitPortsCheckResultsV2 {
|
||||
pub payload: Payload,
|
||||
pub signature: Signature,
|
||||
}
|
||||
|
||||
impl SignedRequest for SubmitPortsCheckResultsV2 {
|
||||
type Payload = Payload;
|
||||
|
||||
fn public_key(&self) -> &PublicKey {
|
||||
&self.payload.agent_public_key
|
||||
}
|
||||
|
||||
fn signature(&self) -> &Signature {
|
||||
&self.signature
|
||||
}
|
||||
|
||||
fn payload(&self) -> &Self::Payload {
|
||||
&self.payload
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-node"
|
||||
version = "1.30.0"
|
||||
version = "1.31.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0"
|
||||
|
||||
Generated
+30
-53
@@ -1258,8 +1258,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "core-models"
|
||||
version = "0.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "657f625ff361906f779745d08375ae3cc9fef87a35fba5f22874cf773010daf4"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"hax-lib",
|
||||
"pastey",
|
||||
@@ -3855,8 +3854,7 @@ checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
[[package]]
|
||||
name = "libcrux-aesgcm"
|
||||
version = "0.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99f2a019dab4097585a7d4f5b9deebe46cd1e628b16a5bc4cb0ce35e1da334e6"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-intrinsics",
|
||||
"libcrux-platform",
|
||||
@@ -3866,9 +3864,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-chacha20poly1305"
|
||||
version = "0.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc08d044676af21343b32b988411fa98dbb5cf65a03c9df478ced221bbdfdb1b"
|
||||
version = "0.0.6"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-hacl-rs",
|
||||
"libcrux-macros",
|
||||
@@ -3880,8 +3877,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "libcrux-curve25519"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb1e5fd8476a6ed609d24ef42aee5ab6f99f7c65d054f92412da9f499e423299"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-hacl-rs",
|
||||
"libcrux-macros",
|
||||
@@ -3892,8 +3888,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "libcrux-ecdh"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b65f73ce79337c762eb38bbac91e4c9b9e60cf318e8501b812750c640814d45e"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-curve25519",
|
||||
"libcrux-p256",
|
||||
@@ -3903,9 +3898,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-ed25519"
|
||||
version = "0.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "835919315b7042fe9e03b6458efe0db94bf2aa7b873934dbee5b5463a8124b43"
|
||||
version = "0.0.6"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-hacl-rs",
|
||||
"libcrux-macros",
|
||||
@@ -3917,8 +3911,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "libcrux-hacl-rs"
|
||||
version = "0.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2637dc87d158e1f1b550fd9b226443e84153fded4de69028d897b534d16d22e6"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-macros",
|
||||
]
|
||||
@@ -3926,8 +3919,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "libcrux-hkdf"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c1a89ca0c89be3a268a921e47105fb7873badf7267f5e3ebf4ea46baedd73ef"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-hacl-rs",
|
||||
"libcrux-hmac",
|
||||
@@ -3937,8 +3929,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "libcrux-hmac"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7a242707d65960770bd7e14e4f18a92bdf0b967777dd404887db8d087a643b"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-hacl-rs",
|
||||
"libcrux-macros",
|
||||
@@ -3948,8 +3939,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "libcrux-intrinsics"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1b5db005ff8001e026b73a6842ee81bbef8ec5ff0e1915a67ae65fd2a9fafa5"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"core-models",
|
||||
"hax-lib",
|
||||
@@ -3957,9 +3947,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-kem"
|
||||
version = "0.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12631592f491d22fd1a176d32b2c6edfb673998fd3987e9d95f8fa79ad2a737b"
|
||||
version = "0.0.6"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-curve25519",
|
||||
"libcrux-ecdh",
|
||||
@@ -3974,8 +3963,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "libcrux-macros"
|
||||
version = "0.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffd6aa2dcd5be681662001b81d493f1569c6d49a32361f470b0c955465cd0338"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
@@ -3983,9 +3971,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-ml-dsa"
|
||||
version = "0.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a72929ed421cc3bf16a946b3e7d2a58d215b0b5c2a12be26b53629f081bf49b2"
|
||||
version = "0.0.7"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"core-models",
|
||||
"hax-lib",
|
||||
@@ -3998,9 +3985,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-ml-kem"
|
||||
version = "0.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a14ab3e477de9df6ee1273a114018ff62c4996ca9220070c4e5cb1743f94a67d"
|
||||
version = "0.0.7"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"hax-lib",
|
||||
"libcrux-intrinsics",
|
||||
@@ -4015,8 +4001,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "libcrux-p256"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4778ba25cb08bb8a96bd100e19ed9aecf78337198fd176036e21042b2dd99bc"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-hacl-rs",
|
||||
"libcrux-macros",
|
||||
@@ -4028,17 +4013,15 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "libcrux-platform"
|
||||
version = "0.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d9e21d7ed31a92ac539bd69a8c970b183ee883872d2d19ce27036e24cb8ecc4"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-poly1305"
|
||||
version = "0.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02491808ee5b9db8cb65fad64ae0be812db64beef179d945c00c7787dc7dfcf9"
|
||||
version = "0.0.4"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-hacl-rs",
|
||||
"libcrux-macros",
|
||||
@@ -4046,9 +4029,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-psq"
|
||||
version = "0.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "779ade7aa5e1b4b400c716b313cbf69070988dd005f92e961c2da4c3c42fbea4"
|
||||
version = "0.0.7"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-aesgcm",
|
||||
"libcrux-chacha20poly1305",
|
||||
@@ -4068,8 +4050,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "libcrux-secrets"
|
||||
version = "0.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ce650f3041b44ba40d4263852347d007cd2cd9d1cc856a6f6c8b2e10c3fd40b"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"hax-lib",
|
||||
]
|
||||
@@ -4077,8 +4058,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "libcrux-sha2"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9d253473f259fc74a280c43f29c464f7e374abdf28b4942234dc707f529d4b7"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-hacl-rs",
|
||||
"libcrux-macros",
|
||||
@@ -4087,9 +4067,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-sha3"
|
||||
version = "0.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1ae0b7d0e1cc4793a609fd0ff2ca3b3a3fabae523770c619a3d4bc86417b0d7"
|
||||
version = "0.0.7"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"hax-lib",
|
||||
"libcrux-intrinsics",
|
||||
@@ -4100,8 +4079,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "libcrux-traits"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e4fa89f3f5e34b47f928b22b1b78395a0d4ec23b1f583db635f128159d65f"
|
||||
source = "git+https://github.com/cryspen/libcrux?rev=b17f8687b67cdcfc10b55aeecc998bbbca28f775#b17f8687b67cdcfc10b55aeecc998bbbca28f775"
|
||||
dependencies = [
|
||||
"libcrux-secrets",
|
||||
"rand 0.9.2",
|
||||
@@ -5092,7 +5070,6 @@ dependencies = [
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"x25519-dalek",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Regular → Executable
+100
-242
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# nym host network firewall, tunnel and wireguard exit policy manager
|
||||
# nym tunnel and wireguard exit policy manager
|
||||
# run this script as root
|
||||
|
||||
set -euo pipefail
|
||||
@@ -20,10 +20,6 @@ info() {
|
||||
printf "%b\n" "${YELLOW}[INFO] $*${NC}"
|
||||
}
|
||||
|
||||
warn() {
|
||||
printf "%b\n" "${YELLOW}[WARN] $*${NC}"
|
||||
}
|
||||
|
||||
ok() {
|
||||
printf "%b\n" "${GREEN}[OK] $*${NC}"
|
||||
}
|
||||
@@ -106,44 +102,18 @@ detect_uplink_interface() {
|
||||
}
|
||||
|
||||
# uplink device detection, can be overridden
|
||||
# Backward compatibility:
|
||||
# - NETWORK_DEVICE sets both IPv4 and IPv6 uplinks.
|
||||
# Preferred overrides:
|
||||
# - NETWORK_DEVICE_V4
|
||||
# - NETWORK_DEVICE_V6
|
||||
NETWORK_DEVICE="${NETWORK_DEVICE:-}"
|
||||
NETWORK_DEVICE_V4="${NETWORK_DEVICE_V4:-${NETWORK_DEVICE:-}}"
|
||||
NETWORK_DEVICE_V6="${NETWORK_DEVICE_V6:-${NETWORK_DEVICE:-}}"
|
||||
|
||||
if [[ -z "$NETWORK_DEVICE_V4" ]]; then
|
||||
NETWORK_DEVICE_V4="$(detect_uplink_interface "ip -o route show default")"
|
||||
if [[ -z "$NETWORK_DEVICE" ]]; then
|
||||
NETWORK_DEVICE="$(detect_uplink_interface "ip -o route show default")"
|
||||
fi
|
||||
if [[ -z "$NETWORK_DEVICE_V4" ]]; then
|
||||
NETWORK_DEVICE_V4="$(detect_uplink_interface "ip -o route show default table all")"
|
||||
if [[ -z "$NETWORK_DEVICE" ]]; then
|
||||
NETWORK_DEVICE="$(detect_uplink_interface "ip -o route show default table all")"
|
||||
fi
|
||||
if [[ -z "$NETWORK_DEVICE_V4" ]]; then
|
||||
error "cannot determine ipv4 uplink interface. set NETWORK_DEVICE_V4 or NETWORK_DEVICE"
|
||||
if [[ -z "$NETWORK_DEVICE" ]]; then
|
||||
error "cannot determine uplink interface. set NETWORK_DEVICE or UPLINK_DEV"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$NETWORK_DEVICE_V6" ]]; then
|
||||
NETWORK_DEVICE_V6="$(detect_uplink_interface "ip -6 -o route show default")"
|
||||
fi
|
||||
if [[ -z "$NETWORK_DEVICE_V6" ]]; then
|
||||
NETWORK_DEVICE_V6="$(detect_uplink_interface "ip -6 -o route show default table all")"
|
||||
fi
|
||||
|
||||
has_ipv6_uplink() {
|
||||
[[ -n "${NETWORK_DEVICE_V6:-}" ]]
|
||||
}
|
||||
|
||||
info "detected ipv4 uplink: $NETWORK_DEVICE_V4"
|
||||
if has_ipv6_uplink; then
|
||||
info "detected ipv6 uplink: $NETWORK_DEVICE_V6"
|
||||
else
|
||||
warn "could not determine ipv6 uplink interface. continuing with ipv4-only setup; ipv6-specific setup will be skipped and ipv6 tests may fail"
|
||||
fi
|
||||
|
||||
###############################################################################
|
||||
# shared helpers
|
||||
###############################################################################
|
||||
@@ -198,7 +168,7 @@ EOF
|
||||
}
|
||||
|
||||
save_iptables_rules() {
|
||||
info "saving iptables rules to /etc/iptables"
|
||||
info "saving iptables rules to /etc/iptables$"
|
||||
mkdir -p /etc/iptables
|
||||
iptables-save > /etc/iptables/rules.v4
|
||||
ip6tables-save > /etc/iptables/rules.v6
|
||||
@@ -224,16 +194,11 @@ fetch_ipv6_address() {
|
||||
|
||||
fetch_and_display_ipv6() {
|
||||
local ipv6_address
|
||||
if ! has_ipv6_uplink; then
|
||||
warn "no ipv6 uplink detected; skipping ipv6 uplink address display"
|
||||
return 0
|
||||
fi
|
||||
|
||||
ipv6_address=$(ip -6 addr show "$NETWORK_DEVICE_V6" scope global | awk '/inet6/ {print $2}')
|
||||
ipv6_address=$(ip -6 addr show "$NETWORK_DEVICE" scope global | awk '/inet6/ {print $2}')
|
||||
if [[ -z "$ipv6_address" ]]; then
|
||||
error "no global ipv6 address found on $NETWORK_DEVICE_V6"
|
||||
error "no global ipv6 address found on $NETWORK_DEVICE"
|
||||
else
|
||||
ok "ipv6 address on $NETWORK_DEVICE_V6: $ipv6_address"
|
||||
ok "ipv6 address on $NETWORK_DEVICE: $ipv6_address"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -378,39 +343,35 @@ remove_duplicate_rules() {
|
||||
|
||||
apply_iptables_rules() {
|
||||
local interface=$1
|
||||
info "applying iptables rules for $interface using ipv4 uplink $NETWORK_DEVICE_V4${NETWORK_DEVICE_V6:+ and ipv6 uplink $NETWORK_DEVICE_V6}"
|
||||
info "applying iptables rules for $interface using uplink $NETWORK_DEVICE"
|
||||
sleep 1
|
||||
|
||||
# ipv4 nat and forwarding
|
||||
iptables -t nat -C POSTROUTING -o "$NETWORK_DEVICE_V4" -j MASQUERADE 2>/dev/null || \
|
||||
iptables -t nat -A POSTROUTING -o "$NETWORK_DEVICE_V4" -j MASQUERADE
|
||||
iptables -t nat -C POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE 2>/dev/null || \
|
||||
iptables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE
|
||||
|
||||
# governed by NYM-EXIT, do not add a broad FORWARD ACCEPT
|
||||
if ! iptables -C FORWARD -i "$interface" -o "$NETWORK_DEVICE_V4" -j "$NYM_CHAIN" 2>/dev/null; then
|
||||
iptables -C FORWARD -i "$interface" -o "$NETWORK_DEVICE_V4" -j ACCEPT 2>/dev/null || \
|
||||
iptables -I FORWARD 1 -i "$interface" -o "$NETWORK_DEVICE_V4" -j ACCEPT
|
||||
if ! iptables -C FORWARD -i "$interface" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then
|
||||
iptables -C FORWARD -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null || \
|
||||
iptables -I FORWARD 1 -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT
|
||||
fi
|
||||
|
||||
iptables -C FORWARD -i "$NETWORK_DEVICE_V4" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \
|
||||
iptables -I FORWARD 2 -i "$NETWORK_DEVICE_V4" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
iptables -C FORWARD -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \
|
||||
iptables -I FORWARD 2 -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
|
||||
# ipv6 nat and forwarding
|
||||
if has_ipv6_uplink; then
|
||||
ip6tables -t nat -C POSTROUTING -o "$NETWORK_DEVICE_V6" -j MASQUERADE 2>/dev/null || \
|
||||
ip6tables -t nat -A POSTROUTING -o "$NETWORK_DEVICE_V6" -j MASQUERADE
|
||||
ip6tables -t nat -C POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE 2>/dev/null || \
|
||||
ip6tables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE
|
||||
|
||||
# governed by NYM-EXIT, do not add a broad FORWARD ACCEPT
|
||||
if ! ip6tables -C FORWARD -i "$interface" -o "$NETWORK_DEVICE_V6" -j "$NYM_CHAIN" 2>/dev/null; then
|
||||
ip6tables -C FORWARD -i "$interface" -o "$NETWORK_DEVICE_V6" -j ACCEPT 2>/dev/null || \
|
||||
ip6tables -I FORWARD 1 -i "$interface" -o "$NETWORK_DEVICE_V6" -j ACCEPT
|
||||
fi
|
||||
|
||||
ip6tables -C FORWARD -i "$NETWORK_DEVICE_V6" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \
|
||||
ip6tables -I FORWARD 2 -i "$NETWORK_DEVICE_V6" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
else
|
||||
warn "no ipv6 uplink detected; skipping ipv6 nat/forwarding rules for $interface"
|
||||
# governed by NYM-EXIT, do not add a broad FORWARD ACCEPT
|
||||
if ! ip6tables -C FORWARD -i "$interface" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then
|
||||
ip6tables -C FORWARD -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null || \
|
||||
ip6tables -I FORWARD 1 -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT
|
||||
fi
|
||||
|
||||
ip6tables -C FORWARD -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \
|
||||
ip6tables -I FORWARD 2 -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
|
||||
save_iptables_rules
|
||||
}
|
||||
|
||||
@@ -556,34 +517,6 @@ apply_smtps_465_rate_limit() {
|
||||
###############################################################################
|
||||
|
||||
NETWORK_FIREWALL_COMMENT="NYM-NETWORK-FW"
|
||||
HOST_SSH_PORT="${HOST_SSH_PORT:-22}"
|
||||
NETWORK_FIREWALL_TCP_PORTS=("$HOST_SSH_PORT" 80 443 1789 1790 8080 9000 9001 41264)
|
||||
NETWORK_FIREWALL_UDP_PORTS=(4443 51822 51264)
|
||||
NETWORK_FIREWALL_WG_TCP_PORT=51830
|
||||
|
||||
validate_port_value() {
|
||||
local name="$1"
|
||||
local value="$2"
|
||||
|
||||
if ! [[ "$value" =~ ^[0-9]+$ ]] || (( 10#$value < 1 || 10#$value > 65535 )); then
|
||||
error "invalid ${name}='${value}'. expected integer 1-65535"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
validate_port_value "HOST_SSH_PORT" "$HOST_SSH_PORT"
|
||||
|
||||
build_network_firewall_status_pattern() {
|
||||
local patterns=("$NETWORK_FIREWALL_COMMENT")
|
||||
local port
|
||||
|
||||
for port in "${NETWORK_FIREWALL_TCP_PORTS[@]}" "${NETWORK_FIREWALL_UDP_PORTS[@]}" "$NETWORK_FIREWALL_WG_TCP_PORT"; do
|
||||
patterns+=("dpt:${port}")
|
||||
done
|
||||
|
||||
local IFS='|'
|
||||
printf '%s' "${patterns[*]}"
|
||||
}
|
||||
|
||||
delete_managed_input_rules() {
|
||||
local cmd="$1"
|
||||
@@ -621,19 +554,22 @@ configure_network_firewall() {
|
||||
delete_managed_input_rules iptables
|
||||
delete_managed_input_rules ip6tables
|
||||
|
||||
local tcp_ports=(22 80 443 1789 1790 8080 9000 9001 41264)
|
||||
local udp_ports=(4443 51822 51264)
|
||||
|
||||
local port
|
||||
for port in "${NETWORK_FIREWALL_TCP_PORTS[@]}"; do
|
||||
for port in "${tcp_ports[@]}"; do
|
||||
add_input_port_rule iptables "$port" tcp
|
||||
add_input_port_rule ip6tables "$port" tcp
|
||||
done
|
||||
|
||||
for port in "${NETWORK_FIREWALL_UDP_PORTS[@]}"; do
|
||||
for port in "${udp_ports[@]}"; do
|
||||
add_input_port_rule iptables "$port" udp
|
||||
add_input_port_rule ip6tables "$port" udp
|
||||
done
|
||||
|
||||
add_input_port_rule iptables "$NETWORK_FIREWALL_WG_TCP_PORT" tcp "$WG_INTERFACE"
|
||||
add_input_port_rule ip6tables "$NETWORK_FIREWALL_WG_TCP_PORT" tcp "$WG_INTERFACE"
|
||||
add_input_port_rule iptables 51830 tcp "$WG_INTERFACE"
|
||||
add_input_port_rule ip6tables 51830 tcp "$WG_INTERFACE"
|
||||
|
||||
save_iptables_rules
|
||||
ok "host network firewall configuration completed"
|
||||
@@ -641,25 +577,23 @@ configure_network_firewall() {
|
||||
|
||||
show_network_firewall_status() {
|
||||
info "managed host network firewall rules"
|
||||
local status_pattern
|
||||
|
||||
status_pattern="$(build_network_firewall_status_pattern)"
|
||||
|
||||
echo
|
||||
info "ipv4 input rules:"
|
||||
iptables -L INPUT -n -v --line-numbers | grep -E "(${status_pattern})" || true
|
||||
iptables -L INPUT -n -v --line-numbers | grep -E "($NETWORK_FIREWALL_COMMENT|dpt:22|dpt:80|dpt:443|dpt:1789|dpt:1790|dpt:8080|dpt:9000|dpt:9001|dpt:51822|dpt:51830|dpt:41264|dpt:51264)" || true
|
||||
echo
|
||||
info "ipv6 input rules:"
|
||||
ip6tables -L INPUT -n -v --line-numbers | grep -E "(${status_pattern})" || true
|
||||
ip6tables -L INPUT -n -v --line-numbers | grep -E "($NETWORK_FIREWALL_COMMENT|dpt:22|dpt:80|dpt:443|dpt:1789|dpt:1790|dpt:8080|dpt:9000|dpt:9001|dpt:51822|dpt:51830|dpt:41264|dpt:51264)" || true
|
||||
}
|
||||
|
||||
test_network_firewall_rules() {
|
||||
info "testing host network firewall rules"
|
||||
|
||||
local failures=0
|
||||
local tcp_ports=(22 80 443 1789 1790 8080 9000 9001 41264)
|
||||
local udp_ports=(51822 51264)
|
||||
local port
|
||||
|
||||
for port in "${NETWORK_FIREWALL_TCP_PORTS[@]}"; do
|
||||
for port in "${tcp_ports[@]}"; do
|
||||
if iptables -C INPUT -p tcp --dport "$port" -m conntrack --ctstate NEW -m comment --comment "$NETWORK_FIREWALL_COMMENT" -j ACCEPT 2>/dev/null; then
|
||||
ok "ipv4 tcp port $port allowed"
|
||||
else
|
||||
@@ -675,7 +609,7 @@ test_network_firewall_rules() {
|
||||
fi
|
||||
done
|
||||
|
||||
for port in "${NETWORK_FIREWALL_UDP_PORTS[@]}"; do
|
||||
for port in "${udp_ports[@]}"; do
|
||||
if iptables -C INPUT -p udp --dport "$port" -m conntrack --ctstate NEW -m comment --comment "$NETWORK_FIREWALL_COMMENT" -j ACCEPT 2>/dev/null; then
|
||||
ok "ipv4 udp port $port allowed"
|
||||
else
|
||||
@@ -691,17 +625,17 @@ test_network_firewall_rules() {
|
||||
fi
|
||||
done
|
||||
|
||||
if iptables -C INPUT -i "$WG_INTERFACE" -p tcp --dport "$NETWORK_FIREWALL_WG_TCP_PORT" -m conntrack --ctstate NEW -m comment --comment "$NETWORK_FIREWALL_COMMENT" -j ACCEPT 2>/dev/null; then
|
||||
ok "ipv4 tcp port $NETWORK_FIREWALL_WG_TCP_PORT allowed on $WG_INTERFACE"
|
||||
if iptables -C INPUT -i "$WG_INTERFACE" -p tcp --dport 51830 -m conntrack --ctstate NEW -m comment --comment "$NETWORK_FIREWALL_COMMENT" -j ACCEPT 2>/dev/null; then
|
||||
ok "ipv4 tcp port 51830 allowed on $WG_INTERFACE"
|
||||
else
|
||||
error "ipv4 tcp port $NETWORK_FIREWALL_WG_TCP_PORT missing on $WG_INTERFACE"
|
||||
error "ipv4 tcp port 51830 missing on $WG_INTERFACE"
|
||||
((failures++))
|
||||
fi
|
||||
|
||||
if ip6tables -C INPUT -i "$WG_INTERFACE" -p tcp --dport "$NETWORK_FIREWALL_WG_TCP_PORT" -m conntrack --ctstate NEW -m comment --comment "$NETWORK_FIREWALL_COMMENT" -j ACCEPT 2>/dev/null; then
|
||||
ok "ipv6 tcp port $NETWORK_FIREWALL_WG_TCP_PORT allowed on $WG_INTERFACE"
|
||||
if ip6tables -C INPUT -i "$WG_INTERFACE" -p tcp --dport 51830 -m conntrack --ctstate NEW -m comment --comment "$NETWORK_FIREWALL_COMMENT" -j ACCEPT 2>/dev/null; then
|
||||
ok "ipv6 tcp port 51830 allowed on $WG_INTERFACE"
|
||||
else
|
||||
error "ipv6 tcp port $NETWORK_FIREWALL_WG_TCP_PORT missing on $WG_INTERFACE"
|
||||
error "ipv6 tcp port 51830 missing on $WG_INTERFACE"
|
||||
((failures++))
|
||||
fi
|
||||
|
||||
@@ -774,50 +708,38 @@ create_nym_chain() {
|
||||
done < <(ip6tables -S FORWARD | grep -F " -j $NYM_CHAIN" || true)
|
||||
|
||||
# remove broad ACCEPT rules for wg + tun outbound so NYM-EXIT is authoritative
|
||||
iptables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE_V4" -j ACCEPT 2>/dev/null || true
|
||||
iptables -D FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE_V4" -j ACCEPT 2>/dev/null || true
|
||||
if has_ipv6_uplink; then
|
||||
ip6tables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE_V6" -j ACCEPT 2>/dev/null || true
|
||||
ip6tables -D FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE_V6" -j ACCEPT 2>/dev/null || true
|
||||
fi
|
||||
iptables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null || true
|
||||
iptables -D FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null || true
|
||||
ip6tables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null || true
|
||||
ip6tables -D FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null || true
|
||||
|
||||
# install the correct hook for both wg + tun
|
||||
iptables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE_V4" -j "$NYM_CHAIN"
|
||||
iptables -I FORWARD 1 -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE_V4" -j "$NYM_CHAIN"
|
||||
iptables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN"
|
||||
iptables -I FORWARD 1 -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN"
|
||||
|
||||
if has_ipv6_uplink; then
|
||||
ip6tables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE_V6" -j "$NYM_CHAIN"
|
||||
ip6tables -I FORWARD 1 -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE_V6" -j "$NYM_CHAIN"
|
||||
ok "NYM-EXIT chain ready + IPv4/IPv6 FORWARD hooks installed for $WG_INTERFACE and $TUNNEL_INTERFACE"
|
||||
else
|
||||
warn "no ipv6 uplink detected; installing only IPv4 FORWARD hooks for $WG_INTERFACE and $TUNNEL_INTERFACE"
|
||||
ok "NYM-EXIT chain ready + IPv4 FORWARD hooks installed for $WG_INTERFACE and $TUNNEL_INTERFACE"
|
||||
fi
|
||||
ip6tables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN"
|
||||
ip6tables -I FORWARD 1 -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN"
|
||||
|
||||
ok "NYM-EXIT chain ready + FORWARD hooks installed for $WG_INTERFACE and $TUNNEL_INTERFACE"
|
||||
}
|
||||
|
||||
|
||||
setup_nat_rules() {
|
||||
info "setting up nat and forwarding rules for $WG_INTERFACE via ipv4 uplink $NETWORK_DEVICE_V4${NETWORK_DEVICE_V6:+ and ipv6 uplink $NETWORK_DEVICE_V6}"
|
||||
info "setting up nat and forwarding rules for $WG_INTERFACE via $NETWORK_DEVICE"
|
||||
|
||||
if ! iptables -t nat -C POSTROUTING -o "$NETWORK_DEVICE_V4" -j MASQUERADE 2>/dev/null; then
|
||||
iptables -t nat -A POSTROUTING -o "$NETWORK_DEVICE_V4" -j MASQUERADE
|
||||
if ! iptables -t nat -C POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE 2>/dev/null; then
|
||||
iptables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE
|
||||
fi
|
||||
if has_ipv6_uplink; then
|
||||
if ! ip6tables -t nat -C POSTROUTING -o "$NETWORK_DEVICE_V6" -j MASQUERADE 2>/dev/null; then
|
||||
ip6tables -t nat -A POSTROUTING -o "$NETWORK_DEVICE_V6" -j MASQUERADE
|
||||
fi
|
||||
else
|
||||
warn "no ipv6 uplink detected; skipping ipv6 NAT setup for $WG_INTERFACE"
|
||||
if ! ip6tables -t nat -C POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE 2>/dev/null; then
|
||||
ip6tables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE
|
||||
fi
|
||||
|
||||
# keep reverse RELATED,ESTABLISHED in FORWARD for return traffic.
|
||||
if ! iptables -C FORWARD -i "$NETWORK_DEVICE_V4" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then
|
||||
iptables -I FORWARD 2 -i "$NETWORK_DEVICE_V4" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
if ! iptables -C FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then
|
||||
iptables -I FORWARD 2 -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
fi
|
||||
if has_ipv6_uplink; then
|
||||
if ! ip6tables -C FORWARD -i "$NETWORK_DEVICE_V6" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then
|
||||
ip6tables -I FORWARD 2 -i "$NETWORK_DEVICE_V6" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
fi
|
||||
if ! ip6tables -C FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then
|
||||
ip6tables -I FORWARD 2 -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -1058,12 +980,10 @@ clear_exit_policy_rules() {
|
||||
ip6tables -F "$NYM_CHAIN" 2>/dev/null || true
|
||||
|
||||
# remove hooks for BOTH wg + tun
|
||||
iptables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE_V4" -j "$NYM_CHAIN" 2>/dev/null || true
|
||||
iptables -D FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE_V4" -j "$NYM_CHAIN" 2>/dev/null || true
|
||||
if has_ipv6_uplink; then
|
||||
ip6tables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE_V6" -j "$NYM_CHAIN" 2>/dev/null || true
|
||||
ip6tables -D FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE_V6" -j "$NYM_CHAIN" 2>/dev/null || true
|
||||
fi
|
||||
iptables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null || true
|
||||
iptables -D FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null || true
|
||||
ip6tables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null || true
|
||||
ip6tables -D FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null || true
|
||||
|
||||
iptables -X "$NYM_CHAIN" 2>/dev/null || true
|
||||
ip6tables -X "$NYM_CHAIN" 2>/dev/null || true
|
||||
@@ -1071,12 +991,7 @@ clear_exit_policy_rules() {
|
||||
|
||||
show_exit_policy_status() {
|
||||
info "nym exit policy status"
|
||||
info "ipv4 network device: $NETWORK_DEVICE_V4"
|
||||
if has_ipv6_uplink; then
|
||||
info "ipv6 network device: $NETWORK_DEVICE_V6"
|
||||
else
|
||||
warn "ipv6 network device: not detected"
|
||||
fi
|
||||
info "network device: $NETWORK_DEVICE"
|
||||
info "wireguard interface: $WG_INTERFACE"
|
||||
info "tunnel interface: $TUNNEL_INTERFACE"
|
||||
echo
|
||||
@@ -1177,71 +1092,21 @@ firewall_rule_line() {
|
||||
}
|
||||
|
||||
check_forward_chain() {
|
||||
local errors=0
|
||||
local output
|
||||
output=$(iptables -L FORWARD -n --line-numbers)
|
||||
|
||||
info "checking FORWARD hooks and reverse RELATED,ESTABLISHED rules"
|
||||
|
||||
if iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE_V4" -j "$NYM_CHAIN" 2>/dev/null; then
|
||||
ok "ipv4 FORWARD hook ok (wg): -i $WG_INTERFACE -o $NETWORK_DEVICE_V4 -> $NYM_CHAIN"
|
||||
else
|
||||
error "ipv4 FORWARD hook missing (wg): -i $WG_INTERFACE -o $NETWORK_DEVICE_V4 -> $NYM_CHAIN"
|
||||
errors=1
|
||||
if ! echo "$output" | grep -q "^1[[:space:]]\+$NYM_CHAIN"; then
|
||||
error "FORWARD rule 1 is not ${NYM_CHAIN}; re-run network-tunnel-manager.sh exit_policy_install"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if iptables -C FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE_V4" -j "$NYM_CHAIN" 2>/dev/null; then
|
||||
ok "ipv4 FORWARD hook ok (tun): -i $TUNNEL_INTERFACE -o $NETWORK_DEVICE_V4 -> $NYM_CHAIN"
|
||||
else
|
||||
error "ipv4 FORWARD hook missing (tun): -i $TUNNEL_INTERFACE -o $NETWORK_DEVICE_V4 -> $NYM_CHAIN"
|
||||
errors=1
|
||||
if ! echo "$output" | grep -q "ACCEPT.*state RELATED,ESTABLISHED"; then
|
||||
error "FORWARD chain missing RELATED,ESTABLISHED accepts; re-run network-tunnel-manager.sh apply_iptables_rules_wg"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if iptables -C FORWARD -i "$NETWORK_DEVICE_V4" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then
|
||||
ok "ipv4 reverse RELATED,ESTABLISHED ok (wg): -i $NETWORK_DEVICE_V4 -o $WG_INTERFACE"
|
||||
else
|
||||
error "ipv4 reverse RELATED,ESTABLISHED missing (wg): -i $NETWORK_DEVICE_V4 -o $WG_INTERFACE"
|
||||
errors=1
|
||||
fi
|
||||
|
||||
if iptables -C FORWARD -i "$NETWORK_DEVICE_V4" -o "$TUNNEL_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then
|
||||
ok "ipv4 reverse RELATED,ESTABLISHED ok (tun): -i $NETWORK_DEVICE_V4 -o $TUNNEL_INTERFACE"
|
||||
else
|
||||
error "ipv4 reverse RELATED,ESTABLISHED missing (tun): -i $NETWORK_DEVICE_V4 -o $TUNNEL_INTERFACE"
|
||||
errors=1
|
||||
fi
|
||||
|
||||
if has_ipv6_uplink; then
|
||||
if ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE_V6" -j "$NYM_CHAIN" 2>/dev/null; then
|
||||
ok "ipv6 FORWARD hook ok (wg): -i $WG_INTERFACE -o $NETWORK_DEVICE_V6 -> $NYM_CHAIN"
|
||||
else
|
||||
error "ipv6 FORWARD hook missing (wg): -i $WG_INTERFACE -o $NETWORK_DEVICE_V6 -> $NYM_CHAIN"
|
||||
errors=1
|
||||
fi
|
||||
|
||||
if ip6tables -C FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE_V6" -j "$NYM_CHAIN" 2>/dev/null; then
|
||||
ok "ipv6 FORWARD hook ok (tun): -i $TUNNEL_INTERFACE -o $NETWORK_DEVICE_V6 -> $NYM_CHAIN"
|
||||
else
|
||||
error "ipv6 FORWARD hook missing (tun): -i $TUNNEL_INTERFACE -o $NETWORK_DEVICE_V6 -> $NYM_CHAIN"
|
||||
errors=1
|
||||
fi
|
||||
|
||||
if ip6tables -C FORWARD -i "$NETWORK_DEVICE_V6" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then
|
||||
ok "ipv6 reverse RELATED,ESTABLISHED ok (wg): -i $NETWORK_DEVICE_V6 -o $WG_INTERFACE"
|
||||
else
|
||||
error "ipv6 reverse RELATED,ESTABLISHED missing (wg): -i $NETWORK_DEVICE_V6 -o $WG_INTERFACE"
|
||||
errors=1
|
||||
fi
|
||||
|
||||
if ip6tables -C FORWARD -i "$NETWORK_DEVICE_V6" -o "$TUNNEL_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then
|
||||
ok "ipv6 reverse RELATED,ESTABLISHED ok (tun): -i $NETWORK_DEVICE_V6 -o $TUNNEL_INTERFACE"
|
||||
else
|
||||
error "ipv6 reverse RELATED,ESTABLISHED missing (tun): -i $NETWORK_DEVICE_V6 -o $TUNNEL_INTERFACE"
|
||||
errors=1
|
||||
fi
|
||||
else
|
||||
warn "no ipv6 uplink detected; skipping ipv6 FORWARD validation"
|
||||
fi
|
||||
|
||||
return $errors
|
||||
ok "FORWARD chain ordering looks good"
|
||||
return 0
|
||||
}
|
||||
|
||||
check_nym_exit_chain() {
|
||||
@@ -1423,37 +1288,33 @@ test_forward_chain_hook() {
|
||||
|
||||
local failures=0
|
||||
|
||||
# verify BOTH interfaces are hooked to NYM-EXIT for IPv4
|
||||
if iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE_V4" -j "$NYM_CHAIN" 2>/dev/null; then
|
||||
ok "ipv4 forward hook ok (wg): -i $WG_INTERFACE -o $NETWORK_DEVICE_V4 -> $NYM_CHAIN"
|
||||
# verify BOTH interfaces are hooked to NYM-EXIT (IPv4 + IPv6)
|
||||
if iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then
|
||||
ok "ipv4 forward hook ok (wg): -i $WG_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN"
|
||||
else
|
||||
error "ipv4 forward hook missing or wrong (wg)"
|
||||
((failures++))
|
||||
fi
|
||||
|
||||
if iptables -C FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE_V4" -j "$NYM_CHAIN" 2>/dev/null; then
|
||||
ok "ipv4 forward hook ok (tun): -i $TUNNEL_INTERFACE -o $NETWORK_DEVICE_V4 -> $NYM_CHAIN"
|
||||
if iptables -C FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then
|
||||
ok "ipv4 forward hook ok (tun): -i $TUNNEL_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN"
|
||||
else
|
||||
error "ipv4 forward hook missing or wrong (tun)"
|
||||
((failures++))
|
||||
fi
|
||||
|
||||
if has_ipv6_uplink; then
|
||||
if ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE_V6" -j "$NYM_CHAIN" 2>/dev/null; then
|
||||
ok "ipv6 forward hook ok (wg): -i $WG_INTERFACE -o $NETWORK_DEVICE_V6 -> $NYM_CHAIN"
|
||||
else
|
||||
error "ipv6 forward hook missing or wrong (wg)"
|
||||
((failures++))
|
||||
fi
|
||||
|
||||
if ip6tables -C FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE_V6" -j "$NYM_CHAIN" 2>/dev/null; then
|
||||
ok "ipv6 forward hook ok (tun): -i $TUNNEL_INTERFACE -o $NETWORK_DEVICE_V6 -> $NYM_CHAIN"
|
||||
else
|
||||
error "ipv6 forward hook missing or wrong (tun)"
|
||||
((failures++))
|
||||
fi
|
||||
if ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then
|
||||
ok "ipv6 forward hook ok (wg): -i $WG_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN"
|
||||
else
|
||||
warn "no ipv6 uplink detected; skipping ipv6 forward hook tests"
|
||||
error "ipv6 forward hook missing or wrong (wg)"
|
||||
((failures++))
|
||||
fi
|
||||
|
||||
if ip6tables -C FORWARD -i "$TUNNEL_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then
|
||||
ok "ipv6 forward hook ok (tun): -i $TUNNEL_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN"
|
||||
else
|
||||
error "ipv6 forward hook missing or wrong (tun)"
|
||||
((failures++))
|
||||
fi
|
||||
|
||||
return "$failures"
|
||||
@@ -1548,7 +1409,7 @@ nym_tunnel_setup() {
|
||||
}
|
||||
|
||||
exit_policy_install() {
|
||||
info "installing nym wireguard exit policy for ${WG_INTERFACE} via ipv4 uplink ${NETWORK_DEVICE_V4}${NETWORK_DEVICE_V6:+ and ipv6 uplink ${NETWORK_DEVICE_V6}}"
|
||||
info "installing nym wireguard exit policy for ${WG_INTERFACE} via ${NETWORK_DEVICE}"
|
||||
exit_policy_install_deps
|
||||
adjust_ip_forwarding
|
||||
create_nym_chain
|
||||
@@ -1700,7 +1561,7 @@ tunnel and nat helpers:
|
||||
check_nym_wg_tun Inspect forward chain for ${WG_INTERFACE}
|
||||
check_nymtun_iptables Inspect forward chain for ${TUNNEL_INTERFACE}
|
||||
configure_dns_and_icmp_wg Allow ping and dns ports on this host
|
||||
fetch_and_display_ipv6 Show ipv6 on uplink ${NETWORK_DEVICE_V6:-<none>}
|
||||
fetch_and_display_ipv6 Show ipv6 on uplink ${NETWORK_DEVICE}
|
||||
fetch_ipv6_address_nym_tun Show global ipv6 address on ${TUNNEL_INTERFACE}
|
||||
joke_through_the_mixnet Test via ${TUNNEL_INTERFACE} with joke
|
||||
joke_through_wg_tunnel Test via ${WG_INTERFACE} with joke
|
||||
@@ -1717,12 +1578,9 @@ exit policy manager:
|
||||
Run verification tests on exit policy (options: --skip-default-reject).
|
||||
|
||||
environment overrides:
|
||||
NETWORK_DEVICE Backward-compatible override that sets both uplinks.
|
||||
NETWORK_DEVICE_V4 Auto-detected IPv4 uplink (e.g., eth0). Set manually if detection fails.
|
||||
NETWORK_DEVICE_V6 Auto-detected IPv6 uplink (e.g., eth2). Optional; if unset, IPv6-specific setup is skipped.
|
||||
NETWORK_DEVICE Auto-detected uplink (e.g., eth0). Set manually if detection fails.
|
||||
TUNNEL_INTERFACE Default: nymtun0. Requires root privileges (sudo) to manage.
|
||||
WG_INTERFACE Default: nymwg - Must match your WireGuard interface name.
|
||||
HOST_SSH_PORT Default: 22. Set manually if you connect to your host's SSH daemon through another port.
|
||||
|
||||
EOF
|
||||
status=0
|
||||
|
||||
@@ -68,60 +68,23 @@ class NodeSetupCLI:
|
||||
print("Without confirming the points above, we cannot continue.")
|
||||
exit(1)
|
||||
|
||||
def _coerce_ssh_port(self, value) -> str:
|
||||
sval = str(value).strip() if value is not None else ""
|
||||
if not sval:
|
||||
sval = "22"
|
||||
if not sval.isdigit():
|
||||
print(f"Invalid SSH port: {sval!r}. Expected integer 1..65535.")
|
||||
raise SystemExit(1)
|
||||
port = int(sval, 10)
|
||||
if not 1 <= port <= 65535:
|
||||
print(f"Invalid SSH port: {port}. Expected integer 1..65535.")
|
||||
raise SystemExit(1)
|
||||
return str(port)
|
||||
|
||||
def _resolve_field(args, existing, arg_name, env_key, prompt, *, default=None, validator=None):
|
||||
cli_val = getattr(args, arg_name, None)
|
||||
|
||||
if cli_val is not None:
|
||||
value = str(cli_val).strip()
|
||||
elif existing.get(env_key):
|
||||
value = str(existing[env_key]).strip()
|
||||
else:
|
||||
entered = input(prompt).strip()
|
||||
value = entered if entered else (default if default is not None else "")
|
||||
|
||||
if validator:
|
||||
value = validator(value)
|
||||
|
||||
return value
|
||||
|
||||
def ensure_env_values(self, args):
|
||||
"""Collect env vars from args or prompt interactively, then save to env.sh."""
|
||||
env_file = Path("env.sh")
|
||||
fields = [
|
||||
("hostname", "HOSTNAME", "Enter hostname (if you don't use a DNS, press enter): ", None, None),
|
||||
("location", "LOCATION", "Enter node location (country code or name): ", None, None),
|
||||
("email", "EMAIL", "Enter your email: ", None, None),
|
||||
("moniker", "MONIKER", "Enter node public moniker (visible in explorer & NymVPN app): ", None, None),
|
||||
("description", "DESCRIPTION", "Enter short node public description: ", None, None),
|
||||
("host_ssh_port", "HOST_SSH_PORT", "Enter host SSH port (press enter for default port 22): ", "22", self._coerce_ssh_port),
|
||||
("hostname", "HOSTNAME", "Enter hostname (if you don't use a DNS, press enter): "),
|
||||
("location", "LOCATION", "Enter node location (country code or name): "),
|
||||
("email", "EMAIL", "Enter your email: "),
|
||||
("moniker", "MONIKER", "Enter node public moniker (visible in explorer & NymVPN app): "),
|
||||
("description", "DESCRIPTION", "Enter short node public description: "),
|
||||
]
|
||||
|
||||
existing = self._read_env_file(env_file)
|
||||
updated = {}
|
||||
|
||||
for arg_name, key, prompt, default, validator in fields:
|
||||
value = self._resolve_field(
|
||||
args,
|
||||
existing,
|
||||
arg_name,
|
||||
key,
|
||||
prompt,
|
||||
default=default,
|
||||
validator=validator,
|
||||
)
|
||||
for arg_name, key, prompt in fields:
|
||||
cli_val = getattr(args, arg_name, None)
|
||||
value = cli_val.strip() if cli_val else existing.get(key) or input(prompt).strip()
|
||||
updated[key] = value
|
||||
os.environ[key] = value
|
||||
|
||||
@@ -676,13 +639,6 @@ class ArgParser:
|
||||
install_parser.add_argument("--moniker", help="Public moniker displayed in explorer & NymVPN app")
|
||||
install_parser.add_argument("--description", help="Short public description of the node")
|
||||
install_parser.add_argument("--public-ip", help="External IPv4 address (autodetected if omitted)")
|
||||
|
||||
install_parser.add_argument(
|
||||
"--host-ssh-port",
|
||||
type=int,
|
||||
help="Host SSH port to allow in the firewall (default: 22)",
|
||||
)
|
||||
|
||||
install_parser.add_argument("--nym-node-binary", help="URL for nym-node binary (autodetected if omitted)")
|
||||
install_parser.add_argument("--uplink-dev", help="Override uplink interface used for NAT/FORWARD (e.g., 'eth0'; autodetected if omitted)")
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ while true; do
|
||||
read -rp "Enter your email: " EMAIL
|
||||
read -rp "Enter node public moniker (visible in the explorer and NymVPN app): " MONIKER
|
||||
read -rp "Enter node public description: " DESCRIPTION
|
||||
read -rp "Enter host SSH port (press enter for default port 22): " HOST_SSH_PORT
|
||||
HOST_SSH_PORT="${HOST_SSH_PORT:-22}"
|
||||
|
||||
# show summary table
|
||||
echo -e "\nPlease confirm the values you entered:"
|
||||
@@ -30,7 +28,6 @@ while true; do
|
||||
printf "%-20s %s\n" "EMAIL:" "$EMAIL"
|
||||
printf "%-20s %s\n" "MONIKER:" "$MONIKER"
|
||||
printf "%-20s %s\n" "DESCRIPTION:" "$DESCRIPTION"
|
||||
printf "%-20s %s\n" "HOST_SSH_PORT:" "$HOST_SSH_PORT"
|
||||
echo "---------------------------------------"
|
||||
|
||||
read -rp "Are these correct? (y/n): " CONFIRM
|
||||
@@ -55,7 +52,6 @@ PUBLIC_IP=${PUBLIC_IP:-""}
|
||||
echo "export MONIKER=\"${MONIKER}\""
|
||||
echo "export DESCRIPTION=\"${DESCRIPTION}\""
|
||||
echo "export PUBLIC_IP=\"${PUBLIC_IP}\""
|
||||
echo "export HOST_SSH_PORT=\"${HOST_SSH_PORT}\""
|
||||
} > env.sh
|
||||
|
||||
echo -e "\nVariables saved to ./env.sh"
|
||||
|
||||
@@ -133,7 +133,8 @@ impl VersionedResponse {
|
||||
ClientVersion::V9 => {
|
||||
let mut resp = IpPacketResponseV8::try_from(self)?;
|
||||
resp.version = nym_ip_packet_requests::v9::VERSION;
|
||||
resp.to_bytes()
|
||||
let bytes = resp.to_bytes();
|
||||
bytes
|
||||
}
|
||||
}
|
||||
.map_err(|err| IpPacketRouterError::FailedToSerializeResponsePacket { source: err })
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-network-requester"
|
||||
version = "1.1.76"
|
||||
version = "1.1.77"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-cli"
|
||||
version = "1.1.75"
|
||||
version = "1.1.76"
|
||||
authors.workspace = true
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nymvisor"
|
||||
version = "0.1.40"
|
||||
version = "0.1.41"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
Reference in New Issue
Block a user