diff --git a/.github/workflows/ci-check-ns-api-version.yml b/.github/workflows/ci-check-ns-api-version.yml index f449750764..e767c26112 100644 --- a/.github/workflows/ci-check-ns-api-version.yml +++ b/.github/workflows/ci-check-ns-api-version.yml @@ -3,7 +3,7 @@ name: ci-check-ns-api-version on: pull_request: paths: - - "nym-node-status-api/**" + - "nym-node-status-api/nym-node-status-api/**" env: WORKING_DIRECTORY: "nym-node-status-api/nym-node-status-api" diff --git a/Cargo.lock b/Cargo.lock index 25ac7b4776..91a632d24f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6489,6 +6489,7 @@ dependencies = [ "nym-validator-client", "pnet_packet", "rand 0.8.5", + "reqwest 0.12.28", "serde", "serde_json", "thiserror 2.0.17", @@ -7234,7 +7235,7 @@ dependencies = [ [[package]] name = "nym-node-status-agent" -version = "1.0.7" +version = "1.1.0" dependencies = [ "anyhow", "clap", diff --git a/nym-api/.sqlx/query-00d857b624e7edab1198114b17cbad1e16988a3f9989d135840500e1143ce5e5.json b/nym-api/.sqlx/query-00d857b624e7edab1198114b17cbad1e16988a3f9989d135840500e1143ce5e5.json deleted file mode 100644 index 03474ab35e..0000000000 --- a/nym-api/.sqlx/query-00d857b624e7edab1198114b17cbad1e16988a3f9989d135840500e1143ce5e5.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_signatures, serialization_revision as \"serialization_revision: u8\"\n FROM expiration_date_signatures\n WHERE expiration_date = ?\n ", - "describe": { - "columns": [ - { - "name": "epoch_id: u32", - "ordinal": 0, - "type_info": "Integer" - }, - { - "name": "serialised_signatures", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "serialization_revision: u8", - "ordinal": 2, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "00d857b624e7edab1198114b17cbad1e16988a3f9989d135840500e1143ce5e5" -} diff --git a/nym-api/.sqlx/query-1d4535b58abdefaaca96bc7312fe14f63ccb56fa62976f7ce3d3b4f6eca8b711.json b/nym-api/.sqlx/query-1d4535b58abdefaaca96bc7312fe14f63ccb56fa62976f7ce3d3b4f6eca8b711.json deleted file mode 100644 index 131935337d..0000000000 --- a/nym-api/.sqlx/query-1d4535b58abdefaaca96bc7312fe14f63ccb56fa62976f7ce3d3b4f6eca8b711.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT timestamp, reliability as \"reliability: u8\"\n FROM gateway_status\n JOIN gateway_details\n ON gateway_status.gateway_details_id = gateway_details.id\n WHERE gateway_details.node_id=? AND gateway_status.timestamp > ?;\n ", - "describe": { - "columns": [ - { - "name": "timestamp", - "ordinal": 0, - "type_info": "Integer" - }, - { - "name": "reliability: u8", - "ordinal": 1, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - true, - true - ] - }, - "hash": "1d4535b58abdefaaca96bc7312fe14f63ccb56fa62976f7ce3d3b4f6eca8b711" -} diff --git a/nym-api/.sqlx/query-2f3c12cb0c48084b569e12ecb0315212a6f526f78258e173c96ec177988696ef.json b/nym-api/.sqlx/query-2f3c12cb0c48084b569e12ecb0315212a6f526f78258e173c96ec177988696ef.json new file mode 100644 index 0000000000..405049043d --- /dev/null +++ b/nym-api/.sqlx/query-2f3c12cb0c48084b569e12ecb0315212a6f526f78258e173c96ec177988696ef.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n DELETE FROM emergency_credential\n WHERE id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "2f3c12cb0c48084b569e12ecb0315212a6f526f78258e173c96ec177988696ef" +} diff --git a/nym-api/.sqlx/query-4a4f3b32b313f7fbc6eb579659e7cec1442967e53764b83ba0a66cd9a72494f9.json b/nym-api/.sqlx/query-4a4f3b32b313f7fbc6eb579659e7cec1442967e53764b83ba0a66cd9a72494f9.json new file mode 100644 index 0000000000..e71f300174 --- /dev/null +++ b/nym-api/.sqlx/query-4a4f3b32b313f7fbc6eb579659e7cec1442967e53764b83ba0a66cd9a72494f9.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n DELETE FROM emergency_credential\n WHERE type = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "4a4f3b32b313f7fbc6eb579659e7cec1442967e53764b83ba0a66cd9a72494f9" +} diff --git a/nym-api/.sqlx/query-5d3b8ad051ab6f46c702308c2fc751a5ca340ac9c6dd86da1a5e9a3e65ea589f.json b/nym-api/.sqlx/query-5d3b8ad051ab6f46c702308c2fc751a5ca340ac9c6dd86da1a5e9a3e65ea589f.json new file mode 100644 index 0000000000..eb6a62ba22 --- /dev/null +++ b/nym-api/.sqlx/query-5d3b8ad051ab6f46c702308c2fc751a5ca340ac9c6dd86da1a5e9a3e65ea589f.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT serialised_signatures, serialization_revision as \"serialization_revision: u8\"\n FROM expiration_date_signatures\n WHERE expiration_date = ? AND epoch_id = ?\n ", + "describe": { + "columns": [ + { + "name": "serialised_signatures", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "serialization_revision: u8", + "ordinal": 1, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false + ] + }, + "hash": "5d3b8ad051ab6f46c702308c2fc751a5ca340ac9c6dd86da1a5e9a3e65ea589f" +} diff --git a/nym-api/.sqlx/query-676299beb2004ab89f7b38cf21ffb84ab5e7d7435297573523e2532560c2e302.json b/nym-api/.sqlx/query-676299beb2004ab89f7b38cf21ffb84ab5e7d7435297573523e2532560c2e302.json deleted file mode 100644 index 903ee12ebf..0000000000 --- a/nym-api/.sqlx/query-676299beb2004ab89f7b38cf21ffb84ab5e7d7435297573523e2532560c2e302.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT\n d.node_id as \"node_id: NodeId\",\n CASE WHEN count(*) > 3 THEN AVG(reliability) ELSE 100 END as \"value: f32\"\n FROM\n gateway_details d\n JOIN\n gateway_status s on d.id = s.gateway_details_id\n WHERE\n timestamp >= ? AND\n timestamp <= ?\n GROUP BY 1\n ", - "describe": { - "columns": [ - { - "name": "node_id: NodeId", - "ordinal": 0, - "type_info": "Integer" - }, - { - "name": "value: f32", - "ordinal": 1, - "type_info": "Null" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false, - null - ] - }, - "hash": "676299beb2004ab89f7b38cf21ffb84ab5e7d7435297573523e2532560c2e302" -} diff --git a/nym-api/.sqlx/query-5eb13bfbee53b50641f69d4d6b62383c7f43864bffe98642bb8d1cf7c259d7be.json b/nym-api/.sqlx/query-697e6d738aecf115a3608139579b2d1d937dd4cc4778dfb42709adf08c1497f2.json similarity index 72% rename from nym-api/.sqlx/query-5eb13bfbee53b50641f69d4d6b62383c7f43864bffe98642bb8d1cf7c259d7be.json rename to nym-api/.sqlx/query-697e6d738aecf115a3608139579b2d1d937dd4cc4778dfb42709adf08c1497f2.json index 8d03e6b94b..1fe08369b7 100644 --- a/nym-api/.sqlx/query-5eb13bfbee53b50641f69d4d6b62383c7f43864bffe98642bb8d1cf7c259d7be.json +++ b/nym-api/.sqlx/query-697e6d738aecf115a3608139579b2d1d937dd4cc4778dfb42709adf08c1497f2.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_signatures\n FROM global_expiration_date_signatures\n WHERE expiration_date = ?\n ", + "query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_signatures\n FROM global_expiration_date_signatures\n WHERE expiration_date = ? AND epoch_id = ?\n ", "describe": { "columns": [ { @@ -15,12 +15,12 @@ } ], "parameters": { - "Right": 1 + "Right": 2 }, "nullable": [ false, false ] }, - "hash": "5eb13bfbee53b50641f69d4d6b62383c7f43864bffe98642bb8d1cf7c259d7be" + "hash": "697e6d738aecf115a3608139579b2d1d937dd4cc4778dfb42709adf08c1497f2" } diff --git a/nym-api/.sqlx/query-73ca856950a0157acfd3e2ed07b11aca3d875f67c77e2e7c75653c3f337d594e.json b/nym-api/.sqlx/query-73ca856950a0157acfd3e2ed07b11aca3d875f67c77e2e7c75653c3f337d594e.json deleted file mode 100644 index 6c62e5e972..0000000000 --- a/nym-api/.sqlx/query-73ca856950a0157acfd3e2ed07b11aca3d875f67c77e2e7c75653c3f337d594e.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT timestamp, reliability as \"reliability: u8\"\n FROM mixnode_status\n JOIN mixnode_details\n ON mixnode_status.mixnode_details_id = mixnode_details.id\n WHERE mixnode_details.mix_id=? AND mixnode_status.timestamp > ?;\n ", - "describe": { - "columns": [ - { - "name": "timestamp", - "ordinal": 0, - "type_info": "Integer" - }, - { - "name": "reliability: u8", - "ordinal": 1, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - true, - true - ] - }, - "hash": "73ca856950a0157acfd3e2ed07b11aca3d875f67c77e2e7c75653c3f337d594e" -} diff --git a/nym-api/.sqlx/query-1f72d6f538a3655a031a3a8706794559c4c0df6defdfd179c84d02d3b8a6c055.json b/nym-api/.sqlx/query-7e1829a17b81d7ee3dd8134b7a2be38ffdcaa672afbae44993365ca6910f68f6.json similarity index 72% rename from nym-api/.sqlx/query-1f72d6f538a3655a031a3a8706794559c4c0df6defdfd179c84d02d3b8a6c055.json rename to nym-api/.sqlx/query-7e1829a17b81d7ee3dd8134b7a2be38ffdcaa672afbae44993365ca6910f68f6.json index 21a1f70f90..57a7eee0f8 100644 --- a/nym-api/.sqlx/query-1f72d6f538a3655a031a3a8706794559c4c0df6defdfd179c84d02d3b8a6c055.json +++ b/nym-api/.sqlx/query-7e1829a17b81d7ee3dd8134b7a2be38ffdcaa672afbae44993365ca6910f68f6.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_signatures\n FROM partial_expiration_date_signatures\n WHERE expiration_date = ?\n ", + "query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_signatures\n FROM partial_expiration_date_signatures\n WHERE expiration_date = ? AND epoch_id = ?\n ", "describe": { "columns": [ { @@ -15,12 +15,12 @@ } ], "parameters": { - "Right": 1 + "Right": 2 }, "nullable": [ false, false ] }, - "hash": "1f72d6f538a3655a031a3a8706794559c4c0df6defdfd179c84d02d3b8a6c055" + "hash": "7e1829a17b81d7ee3dd8134b7a2be38ffdcaa672afbae44993365ca6910f68f6" } diff --git a/nym-api/.sqlx/query-805ad4f26e0234d7f482a263e186156311713d2e9f69d39c868cd16296b56326.json b/nym-api/.sqlx/query-805ad4f26e0234d7f482a263e186156311713d2e9f69d39c868cd16296b56326.json new file mode 100644 index 0000000000..e1eb54bb9c --- /dev/null +++ b/nym-api/.sqlx/query-805ad4f26e0234d7f482a263e186156311713d2e9f69d39c868cd16296b56326.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO emergency_credential\n (type, content, expiration)\n VALUES (?, ?, ?)\n ON CONFLICT(type, content) DO NOTHING;\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "805ad4f26e0234d7f482a263e186156311713d2e9f69d39c868cd16296b56326" +} diff --git a/nym-api/.sqlx/query-924f8eb10c6cbb7f35da6c1bb77e1025442a594dcb5c6401b3dfac7df9c25073.json b/nym-api/.sqlx/query-924f8eb10c6cbb7f35da6c1bb77e1025442a594dcb5c6401b3dfac7df9c25073.json deleted file mode 100644 index c9cf3e5e09..0000000000 --- a/nym-api/.sqlx/query-924f8eb10c6cbb7f35da6c1bb77e1025442a594dcb5c6401b3dfac7df9c25073.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT identity FROM gateway_details WHERE node_id = ?", - "describe": { - "columns": [ - { - "name": "identity", - "ordinal": 0, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "924f8eb10c6cbb7f35da6c1bb77e1025442a594dcb5c6401b3dfac7df9c25073" -} diff --git a/nym-api/.sqlx/query-c19e1b3768bf2929407599e6e8783ead09f4d7319b7997fa2a9bb628f9404166.json b/nym-api/.sqlx/query-c19e1b3768bf2929407599e6e8783ead09f4d7319b7997fa2a9bb628f9404166.json deleted file mode 100644 index c4b25591f3..0000000000 --- a/nym-api/.sqlx/query-c19e1b3768bf2929407599e6e8783ead09f4d7319b7997fa2a9bb628f9404166.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT\n d.mix_id as \"mix_id: NodeId\",\n AVG(s.reliability) as \"value: f32\"\n FROM\n mixnode_details d\n JOIN\n mixnode_status s on d.id = s.mixnode_details_id\n WHERE\n timestamp >= ? AND\n timestamp <= ?\n GROUP BY 1\n ", - "describe": { - "columns": [ - { - "name": "mix_id: NodeId", - "ordinal": 0, - "type_info": "Integer" - }, - { - "name": "value: f32", - "ordinal": 1, - "type_info": "Null" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false, - null - ] - }, - "hash": "c19e1b3768bf2929407599e6e8783ead09f4d7319b7997fa2a9bb628f9404166" -} diff --git a/nym-data-observatory/.sqlx/query-08f4e54ac24fccd54f4208797b3749e457f8cd4ba3d7d906a7ab3bf5b4e7dc9c.json b/nym-data-observatory/.sqlx/query-08f4e54ac24fccd54f4208797b3749e457f8cd4ba3d7d906a7ab3bf5b4e7dc9c.json deleted file mode 100644 index cc5863fd0e..0000000000 --- a/nym-data-observatory/.sqlx/query-08f4e54ac24fccd54f4208797b3749e457f8cd4ba3d7d906a7ab3bf5b4e7dc9c.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO transaction\n (hash, height, index, success, messages, memo, signatures, signer_infos, fee, gas_wanted, gas_used, raw_log, logs, events)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)\n ON CONFLICT (hash) DO UPDATE\n SET height = excluded.height,\n index = excluded.index,\n success = excluded.success,\n messages = excluded.messages,\n memo = excluded.memo,\n signatures = excluded.signatures,\n signer_infos = excluded.signer_infos,\n fee = excluded.fee,\n gas_wanted = excluded.gas_wanted,\n gas_used = excluded.gas_used,\n raw_log = excluded.raw_log,\n logs = excluded.logs,\n events = excluded.events\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Int8", - "Int4", - "Bool", - "Jsonb", - "Text", - "TextArray", - "Jsonb", - "Jsonb", - "Int8", - "Int8", - "Text", - "Jsonb", - "Jsonb" - ] - }, - "nullable": [] - }, - "hash": "08f4e54ac24fccd54f4208797b3749e457f8cd4ba3d7d906a7ab3bf5b4e7dc9c" -} diff --git a/nym-data-observatory/.sqlx/query-0d3709efacf763b06bf14803bb803b5ee5b27879b0026bb0480b3f2722318a75.json b/nym-data-observatory/.sqlx/query-0d3709efacf763b06bf14803bb803b5ee5b27879b0026bb0480b3f2722318a75.json deleted file mode 100644 index 36ba8bb96b..0000000000 --- a/nym-data-observatory/.sqlx/query-0d3709efacf763b06bf14803bb803b5ee5b27879b0026bb0480b3f2722318a75.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO validator (consensus_address, consensus_pubkey)\n VALUES ($1, $2)\n ON CONFLICT DO NOTHING\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "0d3709efacf763b06bf14803bb803b5ee5b27879b0026bb0480b3f2722318a75" -} diff --git a/nym-data-observatory/.sqlx/query-1c2fb0e9ffceca21ef8dbea19b116422b1f723d0a316314b50c43c8b29f8891d.json b/nym-data-observatory/.sqlx/query-1c2fb0e9ffceca21ef8dbea19b116422b1f723d0a316314b50c43c8b29f8891d.json deleted file mode 100644 index 2e10a89220..0000000000 --- a/nym-data-observatory/.sqlx/query-1c2fb0e9ffceca21ef8dbea19b116422b1f723d0a316314b50c43c8b29f8891d.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM pre_commit WHERE height < $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - }, - "hash": "1c2fb0e9ffceca21ef8dbea19b116422b1f723d0a316314b50c43c8b29f8891d" -} diff --git a/nym-data-observatory/.sqlx/query-2561fb016951ea4cd29e43fb9a4a93e944b0d44ed1f7c1036f306e34372da11c.json b/nym-data-observatory/.sqlx/query-2561fb016951ea4cd29e43fb9a4a93e944b0d44ed1f7c1036f306e34372da11c.json deleted file mode 100644 index 0d1b70f8cc..0000000000 --- a/nym-data-observatory/.sqlx/query-2561fb016951ea4cd29e43fb9a4a93e944b0d44ed1f7c1036f306e34372da11c.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT height\n FROM block\n ORDER BY height ASC\n LIMIT 1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "height", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false - ] - }, - "hash": "2561fb016951ea4cd29e43fb9a4a93e944b0d44ed1f7c1036f306e34372da11c" -} diff --git a/nym-data-observatory/.sqlx/query-2679cdf11fa66c7920678cde860c57402119ec7c3aae731b0da831327301466f.json b/nym-data-observatory/.sqlx/query-2679cdf11fa66c7920678cde860c57402119ec7c3aae731b0da831327301466f.json deleted file mode 100644 index b97ea34d16..0000000000 --- a/nym-data-observatory/.sqlx/query-2679cdf11fa66c7920678cde860c57402119ec7c3aae731b0da831327301466f.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE metadata SET last_processed_height = GREATEST(last_processed_height, $1)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - }, - "hash": "2679cdf11fa66c7920678cde860c57402119ec7c3aae731b0da831327301466f" -} diff --git a/nym-data-observatory/.sqlx/query-36ba5941aca6e7b604a10b8b0aba70635028f392fe794d6131827b083e1755e1.json b/nym-data-observatory/.sqlx/query-36ba5941aca6e7b604a10b8b0aba70635028f392fe794d6131827b083e1755e1.json deleted file mode 100644 index dede45475e..0000000000 --- a/nym-data-observatory/.sqlx/query-36ba5941aca6e7b604a10b8b0aba70635028f392fe794d6131827b083e1755e1.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE pruning SET last_pruned_height = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - }, - "hash": "36ba5941aca6e7b604a10b8b0aba70635028f392fe794d6131827b083e1755e1" -} diff --git a/nym-data-observatory/.sqlx/query-3bdf81a9db6075f6f77224c30553f419a849d4ec45af40b052a4cbf09b44f3ec.json b/nym-data-observatory/.sqlx/query-3bdf81a9db6075f6f77224c30553f419a849d4ec45af40b052a4cbf09b44f3ec.json deleted file mode 100644 index e638bce922..0000000000 --- a/nym-data-observatory/.sqlx/query-3bdf81a9db6075f6f77224c30553f419a849d4ec45af40b052a4cbf09b44f3ec.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT last_pruned_height FROM pruning\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "last_pruned_height", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false - ] - }, - "hash": "3bdf81a9db6075f6f77224c30553f419a849d4ec45af40b052a4cbf09b44f3ec" -} diff --git a/nym-data-observatory/.sqlx/query-52c27143720ddfdfd0f5644b60f5b67fd9281ce1de0653efa53b9d9b93cf335d.json b/nym-data-observatory/.sqlx/query-52c27143720ddfdfd0f5644b60f5b67fd9281ce1de0653efa53b9d9b93cf335d.json deleted file mode 100644 index 58af4f89c4..0000000000 --- a/nym-data-observatory/.sqlx/query-52c27143720ddfdfd0f5644b60f5b67fd9281ce1de0653efa53b9d9b93cf335d.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM message WHERE height < $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - }, - "hash": "52c27143720ddfdfd0f5644b60f5b67fd9281ce1de0653efa53b9d9b93cf335d" -} diff --git a/nym-data-observatory/.sqlx/query-62e14613f5ffe692346a79086857a22f0444fbc679db1c06b651fb8b5538b278.json b/nym-data-observatory/.sqlx/query-62e14613f5ffe692346a79086857a22f0444fbc679db1c06b651fb8b5538b278.json deleted file mode 100644 index a7c102469d..0000000000 --- a/nym-data-observatory/.sqlx/query-62e14613f5ffe692346a79086857a22f0444fbc679db1c06b651fb8b5538b278.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO pre_commit (validator_address, height, timestamp, voting_power, proposer_priority)\n VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (validator_address, timestamp) DO NOTHING\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Int8", - "Timestamp", - "Int8", - "Int8" - ] - }, - "nullable": [] - }, - "hash": "62e14613f5ffe692346a79086857a22f0444fbc679db1c06b651fb8b5538b278" -} diff --git a/nym-data-observatory/.sqlx/query-64a484fd46d8ec46797f944a4cced56b6e270ce186f0e49528865d1924343b78.json b/nym-data-observatory/.sqlx/query-64a484fd46d8ec46797f944a4cced56b6e270ce186f0e49528865d1924343b78.json deleted file mode 100644 index 08983f2af9..0000000000 --- a/nym-data-observatory/.sqlx/query-64a484fd46d8ec46797f944a4cced56b6e270ce186f0e49528865d1924343b78.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO block (height, hash, num_txs, total_gas, proposer_address, timestamp)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT DO NOTHING\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Text", - "Int4", - "Int8", - "Text", - "Timestamp" - ] - }, - "nullable": [] - }, - "hash": "64a484fd46d8ec46797f944a4cced56b6e270ce186f0e49528865d1924343b78" -} diff --git a/nym-data-observatory/.sqlx/query-7e82426f5dbcadf1631ba1a806e19cc462d04222fb20ad76de2a40f3f4f8fe15.json b/nym-data-observatory/.sqlx/query-7e82426f5dbcadf1631ba1a806e19cc462d04222fb20ad76de2a40f3f4f8fe15.json deleted file mode 100644 index 3a60c573ed..0000000000 --- a/nym-data-observatory/.sqlx/query-7e82426f5dbcadf1631ba1a806e19cc462d04222fb20ad76de2a40f3f4f8fe15.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT height\n FROM block\n WHERE timestamp < $1\n ORDER BY timestamp DESC\n LIMIT 1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "height", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Timestamp" - ] - }, - "nullable": [ - false - ] - }, - "hash": "7e82426f5dbcadf1631ba1a806e19cc462d04222fb20ad76de2a40f3f4f8fe15" -} diff --git a/nym-data-observatory/.sqlx/query-9455331f9be5a3be28e2bd399a36b2e2d6a9ad4b225c4c883aafc4e9f0428008.json b/nym-data-observatory/.sqlx/query-9455331f9be5a3be28e2bd399a36b2e2d6a9ad4b225c4c883aafc4e9f0428008.json deleted file mode 100644 index 309aa81d9c..0000000000 --- a/nym-data-observatory/.sqlx/query-9455331f9be5a3be28e2bd399a36b2e2d6a9ad4b225c4c883aafc4e9f0428008.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT height\n FROM block\n WHERE timestamp > $1\n ORDER BY timestamp\n LIMIT 1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "height", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Timestamp" - ] - }, - "nullable": [ - false - ] - }, - "hash": "9455331f9be5a3be28e2bd399a36b2e2d6a9ad4b225c4c883aafc4e9f0428008" -} diff --git a/nym-data-observatory/.sqlx/query-bc7795e58ce71893c3f32a19db8e77b7bc0a1af315ffd42c3e68156d6e4ace70.json b/nym-data-observatory/.sqlx/query-bc7795e58ce71893c3f32a19db8e77b7bc0a1af315ffd42c3e68156d6e4ace70.json deleted file mode 100644 index caca484b94..0000000000 --- a/nym-data-observatory/.sqlx/query-bc7795e58ce71893c3f32a19db8e77b7bc0a1af315ffd42c3e68156d6e4ace70.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT COUNT(*) as count FROM pre_commit\n WHERE\n validator_address = $1\n AND height >= $2\n AND height <= $3\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - "Int8", - "Int8" - ] - }, - "nullable": [ - null - ] - }, - "hash": "bc7795e58ce71893c3f32a19db8e77b7bc0a1af315ffd42c3e68156d6e4ace70" -} diff --git a/nym-data-observatory/.sqlx/query-be43d4873911deca784b7be0531ab7bd82ecd68041aa932a56c8ce09623251e4.json b/nym-data-observatory/.sqlx/query-be43d4873911deca784b7be0531ab7bd82ecd68041aa932a56c8ce09623251e4.json deleted file mode 100644 index f1df706371..0000000000 --- a/nym-data-observatory/.sqlx/query-be43d4873911deca784b7be0531ab7bd82ecd68041aa932a56c8ce09623251e4.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT * FROM validator\n WHERE EXISTS (\n SELECT 1 FROM pre_commit\n WHERE height = $1\n AND pre_commit.validator_address = validator.consensus_address\n )\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "consensus_address", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "consensus_pubkey", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false - ] - }, - "hash": "be43d4873911deca784b7be0531ab7bd82ecd68041aa932a56c8ce09623251e4" -} diff --git a/nym-data-observatory/.sqlx/query-c88d07fecc3f33deaa6e93db1469ce71582635df47f52dcf3fd1df4e7be6b96d.json b/nym-data-observatory/.sqlx/query-c88d07fecc3f33deaa6e93db1469ce71582635df47f52dcf3fd1df4e7be6b96d.json deleted file mode 100644 index 9bf3eaf97b..0000000000 --- a/nym-data-observatory/.sqlx/query-c88d07fecc3f33deaa6e93db1469ce71582635df47f52dcf3fd1df4e7be6b96d.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT last_processed_height FROM metadata\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "last_processed_height", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false - ] - }, - "hash": "c88d07fecc3f33deaa6e93db1469ce71582635df47f52dcf3fd1df4e7be6b96d" -} diff --git a/nym-data-observatory/.sqlx/query-cc0ae74082d7d8a89f2d3364676890bbf6150ab394c72783114340d4def5f9ef.json b/nym-data-observatory/.sqlx/query-cc0ae74082d7d8a89f2d3364676890bbf6150ab394c72783114340d4def5f9ef.json deleted file mode 100644 index 5c0da1448a..0000000000 --- a/nym-data-observatory/.sqlx/query-cc0ae74082d7d8a89f2d3364676890bbf6150ab394c72783114340d4def5f9ef.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO message(transaction_hash, index, type, value, involved_accounts_addresses, height)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT (transaction_hash, index) DO UPDATE\n SET height = excluded.height,\n type = excluded.type,\n value = excluded.value,\n involved_accounts_addresses = excluded.involved_accounts_addresses\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Int8", - "Text", - "Jsonb", - "TextArray", - "Int8" - ] - }, - "nullable": [] - }, - "hash": "cc0ae74082d7d8a89f2d3364676890bbf6150ab394c72783114340d4def5f9ef" -} diff --git a/nym-data-observatory/.sqlx/query-cdba9b267f143c8a8c6c3d6ed713cf00236490b86779559d84740ec18bcfa3a9.json b/nym-data-observatory/.sqlx/query-cdba9b267f143c8a8c6c3d6ed713cf00236490b86779559d84740ec18bcfa3a9.json deleted file mode 100644 index 2ae11a8fbb..0000000000 --- a/nym-data-observatory/.sqlx/query-cdba9b267f143c8a8c6c3d6ed713cf00236490b86779559d84740ec18bcfa3a9.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM block WHERE height < $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - }, - "hash": "cdba9b267f143c8a8c6c3d6ed713cf00236490b86779559d84740ec18bcfa3a9" -} diff --git a/nym-data-observatory/.sqlx/query-d89558c37c51e8e6b1e6a9d5a2b13d0598fd856aa019a0cbbae12d7cafb4672f.json b/nym-data-observatory/.sqlx/query-d89558c37c51e8e6b1e6a9d5a2b13d0598fd856aa019a0cbbae12d7cafb4672f.json deleted file mode 100644 index 1970629169..0000000000 --- a/nym-data-observatory/.sqlx/query-d89558c37c51e8e6b1e6a9d5a2b13d0598fd856aa019a0cbbae12d7cafb4672f.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM transaction WHERE height < $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - }, - "hash": "d89558c37c51e8e6b1e6a9d5a2b13d0598fd856aa019a0cbbae12d7cafb4672f" -} diff --git a/nym-gateway-probe/Cargo.toml b/nym-gateway-probe/Cargo.toml index 16fe2c08e9..536412f580 100644 --- a/nym-gateway-probe/Cargo.toml +++ b/nym-gateway-probe/Cargo.toml @@ -24,6 +24,7 @@ hex.workspace = true tracing.workspace = true pnet_packet.workspace = true rand.workspace = true +reqwest = { workspace = true, features = ["socks"] } serde.workspace = true serde_json.workspace = true thiserror.workspace = true diff --git a/nym-gateway-probe/src/common/mod.rs b/nym-gateway-probe/src/common/mod.rs index 3f2cda9f35..6276ec032c 100644 --- a/nym-gateway-probe/src/common/mod.rs +++ b/nym-gateway-probe/src/common/mod.rs @@ -12,5 +12,6 @@ pub(crate) mod icmp; pub(crate) mod netstack; pub(crate) mod nodes; pub(crate) mod probe_tests; +pub(crate) mod socks5_test; pub(crate) mod types; pub(crate) mod wireguard; diff --git a/nym-gateway-probe/src/common/nodes.rs b/nym-gateway-probe/src/common/nodes.rs index 6485920574..9716a923fd 100644 --- a/nym-gateway-probe/src/common/nodes.rs +++ b/nym-gateway-probe/src/common/nodes.rs @@ -4,8 +4,9 @@ use anyhow::{Context, anyhow, bail}; use nym_api_requests::models::{ AuthenticatorDetailsV2, DeclaredRolesV2, DescribedNodeTypeV2, HostInformationV2, - IpPacketRouterDetailsV2, LewesProtocolDetailsV1, NetworkRequesterDetailsV2, NymNodeDataV2, - OffsetDateTimeJsonSchemaWrapper, WebSocketsV2, WireguardDetailsV2, + IpPacketRouterDetailsV2, LewesProtocolDetailsV1, NetworkRequesterDetailsV1, + NetworkRequesterDetailsV2, NymNodeDataV2, OffsetDateTimeJsonSchemaWrapper, WebSocketsV2, + WireguardDetailsV2, }; use nym_authenticator_requests::AuthenticatorVersion; use nym_bin_common::build_information::BinaryBuildInformationOwned; @@ -161,10 +162,12 @@ impl DirectoryNode { }), _ => None, }; + let network_requester_details = self.described.description.network_requester.clone(); Ok(TestedNodeDetails { identity: self.identity(), exit_router_address, + network_requester_details, authenticator_address, authenticator_version, ip_address: Some(ip_address), @@ -409,7 +412,17 @@ impl NymApiDirectory { .iter() .filter(|(_, n)| n.described.description.ip_packet_router.is_some()) .choose(&mut rand::thread_rng()) - .ok_or(anyhow!("no gateways running IPR available")) + .context("no gateways running IPR available") + .map(|(id, _)| *id) + } + + pub fn random_exit_with_nr(&self) -> anyhow::Result { + info!("Selecting random gateway with NR enabled"); + self.nodes + .iter() + .filter(|(_, n)| n.described.description.ip_packet_router.is_some()) + .choose(&mut rand::thread_rng()) + .context("no gateways running NR available") .map(|(id, _)| *id) } @@ -419,7 +432,7 @@ impl NymApiDirectory { .iter() .filter(|(_, n)| n.described.description.declared_role.entry) .choose(&mut rand::thread_rng()) - .ok_or(anyhow!("no entry gateways available")) + .context("no entry gateways available") .map(|(id, _)| *id) } @@ -439,6 +452,16 @@ impl NymApiDirectory { }; Ok(maybe_entry) } + + pub fn exit_gateway_nr(&self, identity: &NodeIdentity) -> anyhow::Result { + let Some(maybe_entry) = self.nodes.get(identity).cloned() else { + bail!("{identity} not found in directory") + }; + if !maybe_entry.described.description.declared_role.exit_nr { + bail!("{identity} doesn't support exit NR mode") + }; + Ok(maybe_entry) + } } #[derive(Default, Debug)] @@ -468,6 +491,7 @@ impl TestedNode { pub struct TestedNodeDetails { pub identity: NodeIdentity, pub exit_router_address: Option, + pub network_requester_details: Option, pub authenticator_address: Option, pub authenticator_version: AuthenticatorVersion, pub ip_address: Option, @@ -489,6 +513,7 @@ impl TestedNodeDetails { identity, ip_address: Some(lp_data.address.ip()), lp_data: Some(lp_data), + network_requester_details: None, // These are None in localnet mode - only needed for mixnet/authenticator exit_router_address: None, authenticator_address: None, diff --git a/nym-gateway-probe/src/common/probe_tests.rs b/nym-gateway-probe/src/common/probe_tests.rs index a892901aa7..018c414a11 100644 --- a/nym-gateway-probe/src/common/probe_tests.rs +++ b/nym-gateway-probe/src/common/probe_tests.rs @@ -1,9 +1,12 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::NymApiDirectory; +use crate::common::helpers::mixnet_debug_config; use crate::common::nodes::{TestedNodeDetails, TestedNodeLpDetails}; +use crate::common::socks5_test::HttpsConnectivityTest; use crate::common::types::{ - Entry, Exit, IpPingReplies, LpProbeResults, ProbeOutcome, WgProbeResults, + Entry, Exit, IpPingReplies, LpProbeResults, ProbeOutcome, Socks5ProbeResults, WgProbeResults, }; use crate::common::wireguard::{ TwoHopWgTunnelConfig, WgTunnelConfig, run_tunnel_tests, run_two_hop_tunnel_tests, @@ -27,7 +30,12 @@ use nym_crypto::asymmetric::{ed25519, x25519}; use nym_ip_packet_client::IprClientConnect; use nym_ip_packet_requests::{IpPair, codec::MultiIpPacketCodec}; use nym_registration_client::{LpRegistrationClient, NestedLpSession}; -use nym_sdk::mixnet::{MixnetClient, NodeIdentity, Recipient}; +use nym_sdk::mixnet::{MixnetClient, MixnetClientBuilder, NodeIdentity, Recipient, Socks5}; +use nym_sdk::{ + DebugConfig, NymApiTopologyProvider, NymApiTopologyProviderConfig, NymNetworkDetails, + TopologyProvider, +}; +use nym_topology::{HardcodedTopologyProvider, NymTopology}; use std::{ net::{IpAddr, Ipv4Addr, Ipv6Addr}, sync::Arc, @@ -36,6 +44,7 @@ use std::{ use tokio::net::TcpStream; use tokio_util::{codec::Decoder, sync::CancellationToken}; use tracing::*; +use url::Url; pub async fn wg_probe( mut auth_client: AuthenticatorClient, @@ -446,6 +455,7 @@ pub async fn do_ping( exit_result.map(|exit| ProbeOutcome { as_entry: entry, as_exit: exit, + socks5: None, wg: None, lp: None, }), @@ -609,3 +619,178 @@ pub async fn listen_for_icmp_ping_replies( can_route_ip_external_v6: registered_replies.external_ip_v6, })) } + +/// Creates a SOCKS5 proxy connection through the mixnet to the exit GW +/// and performs necessary tests. +#[allow(clippy::too_many_arguments)] +#[instrument(level = "info", name = "socks5_test", skip_all)] +pub(crate) async fn do_socks5_connectivity_test( + network_requester_address: &str, + network_details: NymNetworkDetails, + directory: &NymApiDirectory, + json_rpc_endpoints: Vec, + mixnet_client_timeout: u64, + test_run_count: u64, + failure_count_cutoff: usize, + topology: Option, +) -> anyhow::Result { + info!( + "Starting SOCKS5 test through Network Requester: {}", + network_requester_address + ); + if json_rpc_endpoints.is_empty() { + bail!("You need to define JSON RPC URLs in order to test SOCKS5") + } + + // parse the network requester address + let nr_recipient = match network_requester_address.parse::() { + Ok(addr) => addr, + Err(e) => { + error!("Invalid Network Requester address: {}", e); + + return Ok(Socks5ProbeResults::error_before_connecting(format!( + "Invalid NR address: {}", + e + ))); + } + }; + + info!( + "Network Requester gateway: {}", + nr_recipient.gateway().to_base58_string() + ); + info!( + "Network Requester identity: {}", + nr_recipient.identity().to_base58_string() + ); + + // create ephemeral SOCKS5 client + let socks5_config = Socks5::new(network_requester_address.to_string()); + + // since we define both entry & exit gateways to be the same tested GW, + // this shouldn't negatively affect mixnet layers but it will force route + // construction in case GW would get filtered out of topology + let min_gw_performance = Some(0); + + // debug config similar to main probe + let debug_config = mixnet_debug_config(min_gw_performance, true); + + // Verify the NR gateway exists in the directory with exit_nr role + let nr_gateway_id = nr_recipient.gateway(); + if let Err(e) = directory.exit_gateway_nr(&nr_gateway_id) { + return Ok(Socks5ProbeResults::error_before_connecting(e.to_string())); + } else { + info!("✔️ Network Requester gateway found in directory with exit_nr role"); + } + + // use intended exit as entry as well + let entry_gateway = nr_gateway_id; + + // use existing topology if available, otherwise fetch it + let topology_provider: Box = match topology { + Some(t) => { + info!("✔️ Reusing topology from main mixnet client"); + Box::new(HardcodedTopologyProvider::new(t)) + } + None => { + info!("Fetching topology for SOCKS5 client..."); + match hardcoded_topology(&network_details, &debug_config).await { + Ok(provider) => provider, + Err(e) => return Ok(Socks5ProbeResults::error_before_connecting(e)), + } + } + }; + + let socks5_client_builder = MixnetClientBuilder::new_ephemeral() + // Specify entry gateway explicitly + .request_gateway(entry_gateway.to_base58_string()) + .socks5_config(socks5_config) + .network_details(network_details) + .debug_config(debug_config) + .custom_topology_provider(topology_provider) + .build()?; + + // connect to mixnet via SOCKS5 + let socks5_client = match socks5_client_builder.connect_to_mixnet_via_socks5().await { + Ok(client) => { + info!("🌐 Successfully connected to mixnet via SOCKS5 proxy"); + info!( + "Connected via entry gateway: {}", + client.nym_address().gateway().to_base58_string() + ); + client + } + Err(e) => { + error!("Failed to establish SOCKS5 connection: {}", e); + return Ok(Socks5ProbeResults::error_before_connecting(format!( + "SOCKS5 connection failed: {}", + e + ))); + } + }; + + let test = match HttpsConnectivityTest::new( + test_run_count, + mixnet_client_timeout, + failure_count_cutoff, + json_rpc_endpoints, + socks5_client.socks5_url(), + ) { + Ok(test) => test, + Err(err) => { + socks5_client.disconnect().await; + + error!("{err}"); + return Ok(Socks5ProbeResults::error_after_connecting( + "Failed to create client", + )); + } + }; + + let result = test.run_tests().await; + socks5_client.disconnect().await; + + Ok(Socks5ProbeResults::with_http_result(result)) +} + +async fn hardcoded_topology( + network_details: &NymNetworkDetails, + debug_config: &DebugConfig, +) -> Result, String> { + // get Nym API URLs from network_details + let nym_api_urls: Vec = network_details + .nym_api_urls + .as_ref() + .map(|urls| urls.iter().filter_map(|u| u.url.parse().ok()).collect()) + .or_else(|| { + network_details + .endpoints + .first() + .and_then(|e| e.api_url()) + .map(|url| vec![url]) + }) + .unwrap_or_default(); + + if nym_api_urls.is_empty() { + return Err(String::from("No nym-api URLs available to fetch topology")); + } + + let topology_config = NymApiTopologyProviderConfig { + min_mixnode_performance: debug_config.topology.minimum_mixnode_performance, + min_gateway_performance: debug_config.topology.minimum_gateway_performance, + use_extended_topology: debug_config.topology.use_extended_topology, + ignore_egress_epoch_role: debug_config.topology.ignore_egress_epoch_role, + }; + + let api_client = nym_http_api_client::Client::new_url(nym_api_urls[0].clone(), None) + .map_err(|e| e.to_string())?; + let mut provider = NymApiTopologyProvider::new(topology_config, nym_api_urls, api_client); + + match provider.get_new_topology().await { + Some(topology) => { + info!("Fetched network topology"); + Ok(Box::new(HardcodedTopologyProvider::new(topology))) + } + None => Err(String::from("Failed to fetch network topology")), + } +} diff --git a/nym-gateway-probe/src/common/socks5_test/json_rpc_client.rs b/nym-gateway-probe/src/common/socks5_test/json_rpc_client.rs new file mode 100644 index 0000000000..e38269ed04 --- /dev/null +++ b/nym-gateway-probe/src/common/socks5_test/json_rpc_client.rs @@ -0,0 +1,255 @@ +use anyhow::bail; +use rand::Rng; +use reqwest::Proxy; +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use tokio::time::Instant; +use tracing::{debug, error, info, warn}; + +use crate::common::socks5_test::SingleHttpsTestResult; + +pub struct JsonRpcClient { + client: reqwest::Client, + client_timeout: Duration, + test_endpoints: Vec, +} + +impl JsonRpcClient { + pub fn new( + client_timeout: u64, + proxy: Option, + test_endpoints: Vec, + ) -> anyhow::Result { + let mut builder = reqwest::Client::builder().timeout(Duration::from_secs(client_timeout)); + + if let Some(proxy) = proxy { + builder = builder.proxy(proxy); + } + let client = builder.build()?; + + Ok(Self { + client_timeout: Duration::from_secs(client_timeout), + test_endpoints, + client, + }) + } + + pub(super) async fn https_request_with_fallbacks(&self) -> SingleHttpsTestResult { + let mut error_msg = String::new(); + + // endpoints are used as fallbacks: in case of success, return early + for endpoint in self.test_endpoints.iter() { + info!( + "Testing against {} with timeout {}s", + endpoint, + self.client_timeout.as_secs() + ); + let start = Instant::now(); + + let res = self.eth_chainid(endpoint).await; + let elapsed = start.elapsed(); + match res { + Ok((status, JsonRpcResponse::Ok { .. })) => { + debug!( + "HTTPS test completed: status={}, latency={}ms", + status.as_u16(), + elapsed.as_millis() + ); + return SingleHttpsTestResult { + success: true, + status_code: Some(status.as_u16()), + latency_ms: Some(elapsed.as_millis() as u64), + endpoint_used: Some(endpoint.to_string()), + error: None, + }; + } + Ok((_, JsonRpcResponse::Err { error, .. })) => { + warn!("JSON-RPC error: {} (code: {})", error.message, error.code); + error_msg = format!("JSON-RPC error: {}", error.message); + } + Err(e) => { + error_msg = e.to_string(); + error!("{}", &error_msg); + } + } + } + + SingleHttpsTestResult { + success: false, + status_code: None, + latency_ms: None, + endpoint_used: self.test_endpoints.last().cloned(), + error: Some(error_msg), + } + } + + async fn eth_chainid( + &self, + endpoint: &str, + ) -> anyhow::Result<(reqwest::StatusCode, JsonRpcResponse)> { + match self + .client + .post(endpoint) + .header(reqwest::header::CONTENT_TYPE, "application/json") + .json(&JsonRpcRequestBody::eth_chainid()) + .send() + .await + .and_then(reqwest::Response::error_for_status) + { + Ok(response) => { + let status = response.status(); + if status.is_success() { + // Deserialize body into JsonRpcResponse + response + .json::() + .await + .map(|res| (status, res)) + .map_err(From::from) + } else { + bail!("HTTP error status: {}", status.as_u16()); + } + } + Err(e) => { + error!("HTTPS request failed: {}", e); + bail!("HTTPS request failed: {}", e); + } + } + } + + pub async fn ensure_endpoint_works(&self) -> anyhow::Result<()> { + let mut any_works = false; + for endpoint in self.test_endpoints.iter() { + if let Err(err) = self.eth_chainid(endpoint).await { + warn!("Endpoint {endpoint} error: {err}"); + } else { + any_works = true; + } + } + + if any_works { + Ok(()) + } else { + bail!("None of the endpoints are valid, see logs"); + } + } +} + +/// https://www.jsonrpc.org/specification +#[derive(Serialize)] +struct JsonRpcRequestBody { + // A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0". + jsonrpc: String, + method: String, + // A Structured value that holds the parameter values to be used during the invocation of the method. This member MAY be omitted. + params: serde_json::Value, + // The Server MUST reply with the same value in the Response object if included. + // This member is used to correlate the context between the two objects. + id: i64, +} + +impl JsonRpcRequestBody { + /// Very simple endpoint that requires no dynamic input + /// + /// https://ethereum.org/developers/docs/apis/json-rpc/#eth_chainId + pub fn eth_chainid() -> Self { + Self { + jsonrpc: String::from("2.0"), + method: String::from("eth_chainId"), + params: serde_json::json!([]), + id: rand::thread_rng().r#gen(), + } + } + + /// Create an eth_getBlockByNumber request with invalid params for testing error responses + #[cfg(test)] + pub fn eth_get_block_by_number_invalid() -> Self { + Self { + jsonrpc: String::from("2.0"), + method: String::from("eth_getBlockByNumber"), + // Invalid params: should be [blockNumber, boolean] but we pass garbage + params: serde_json::json!(["invalid_block_number"]), + id: rand::thread_rng().r#gen(), + } + } +} + +// dead code: we need these fields for deserialization, even if we don't read them explicitly +#[allow(dead_code)] +#[derive(Deserialize)] +#[serde(untagged)] +enum JsonRpcResponse { + Ok { + jsonrpc: String, + // have to use opaque Value because spec say this might be string, number or null (we don't care either way) + id: serde_json::Value, + // we don't really care for the exact result, just whether the response is OK or error + result: serde_json::Value, + }, + Err { + jsonrpc: String, + // have to use opaque Value because spec say this might be string, number or null (we don't care either way) + id: serde_json::Value, + error: JsonRpcError, + }, +} + +// dead code: we need these fields for deserialization, even if we don't read them explicitly +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +struct JsonRpcError { + pub code: i64, + pub message: String, + #[serde(default)] + pub data: Option, +} + +#[cfg(test)] +mod test { + use super::*; + + const JSON_RPC_ENDPOINT: &str = "https://cloudflare-eth.com"; + + #[tokio::test] + async fn test_eth_chainid_returns_ok_response() { + let client = reqwest::Client::new(); + let response = client + .post(JSON_RPC_ENDPOINT) + .header(reqwest::header::CONTENT_TYPE, "application/json") + .json(&JsonRpcRequestBody::eth_chainid()) + .send() + .await + .expect("Failed to send request"); + + assert!(response.status().is_success()); + + let json_response: JsonRpcResponse = + response.json().await.expect("Failed to parse response"); + + assert!( + matches!(json_response, JsonRpcResponse::Ok { .. }), + "Expected Ok variant for eth_chainId" + ); + } + + #[tokio::test] + async fn test_eth_get_block_by_number_invalid_returns_error_response() { + let client = reqwest::Client::new(); + let response = client + .post(JSON_RPC_ENDPOINT) + .header(reqwest::header::CONTENT_TYPE, "application/json") + .json(&JsonRpcRequestBody::eth_get_block_by_number_invalid()) + .send() + .await + .expect("Failed to send request"); + + assert!(response.status().is_success()); // HTTP 200 but JSON-RPC error + + let json_response: JsonRpcResponse = + response.json().await.expect("Failed to parse response"); + + assert!( + matches!(json_response, JsonRpcResponse::Err { .. }), + "Expected Err variant for invalid params" + ); + } +} diff --git a/nym-gateway-probe/src/common/socks5_test/mod.rs b/nym-gateway-probe/src/common/socks5_test/mod.rs new file mode 100644 index 0000000000..5ea4e361d1 --- /dev/null +++ b/nym-gateway-probe/src/common/socks5_test/mod.rs @@ -0,0 +1,161 @@ +use serde::{Deserialize, Serialize}; +use tracing::{info, warn}; + +pub(crate) use json_rpc_client::JsonRpcClient; + +mod json_rpc_client; + +pub struct HttpsConnectivityTest { + test_count: u64, + failure_count_cutoff: usize, + client: JsonRpcClient, +} + +impl HttpsConnectivityTest { + pub fn new( + test_count: u64, + mixnet_client_timeout: u64, + failure_count_cutoff: usize, + json_rpc_test_endpoints: Vec, + socks5_proxy_url: String, + ) -> anyhow::Result { + let proxy = reqwest::Proxy::all(socks5_proxy_url) + .map_err(|e| anyhow::anyhow!("Failed to create proxy: {}", e))?; + let client = + JsonRpcClient::new(mixnet_client_timeout, Some(proxy), json_rpc_test_endpoints)?; + let res = Self { + test_count: std::cmp::max(test_count, 1), + failure_count_cutoff, + client, + }; + + Ok(res) + } + + pub async fn run_tests(self) -> HttpsConnectivityResult { + let mut results = Vec::new(); + + for i in 1..=self.test_count { + info!("Running test {}/{}", i, self.test_count); + let interim_res = self.client.https_request_with_fallbacks().await; + + if interim_res.success { + info!( + "{}/{} latency: {}ms", + i, + self.test_count, + interim_res.latency_ms.unwrap_or(0) + ); + } + + results.push(interim_res); + + // early exit + let unsuccessful = results.iter().filter(|r| !r.success).count(); + if unsuccessful > self.failure_count_cutoff { + warn!("Too many failed runs: returning early..."); + break; + } + } + + let final_result = HttpsConnectivityResult::from_results(results); + info!("AVG latency (in ms): {:?}", final_result.https_latency_ms); + final_result + } +} + +/// single HTTPS test attempt +struct SingleHttpsTestResult { + success: bool, + status_code: Option, + latency_ms: Option, + endpoint_used: Option, + error: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct HttpsConnectivityResult { + /// successfully completed HTTPS request + https_success: bool, + + /// HTTPS status code received + https_status_code: Option, + + /// average HTTPS request latency in milliseconds + https_latency_ms: Option, + + /// among multiple endpoints available, list the one actually used + endpoint_used: Option, + + /// error message(s) (if any) + error: Option>, +} + +impl HttpsConnectivityResult { + pub fn with_errors(error: Vec) -> Self { + Self { + https_success: false, + https_status_code: None, + https_latency_ms: None, + endpoint_used: None, + error: Some(error), + } + } + + fn from_results(results: Vec) -> Self { + let (successes, errors): (Vec, Vec) = + results.into_iter().partition(|r| r.success); + let errors = errors + .into_iter() + .map(|r| r.error) + .collect::>>() + // partition above guarantees this vec is non-empty + .unwrap_or_default(); + + // use the last successful result for status_code and endpoint + // this works as an empty check as well: if there is no last success, array must be empty hence only errors are present + let Some(last_success) = successes.last() else { + return Self::with_errors(errors); + }; + + // average latency from successful runs + let mut successes_count = 0; + let total_latency: u64 = successes + .iter() + .filter_map(|r| { + successes_count += 1; + r.latency_ms + }) + .sum(); + let avg_latency = total_latency / successes_count as u64; + + Self { + https_success: true, + https_status_code: last_success.status_code, + https_latency_ms: Some(avg_latency), + endpoint_used: last_success.endpoint_used.clone(), + // even in case of success, some errors were possible + error: if errors.is_empty() { + None + } else { + Some(errors) + }, + } + } + + pub fn https_success(&self) -> bool { + self.https_success + } + + pub fn https_status_code(&self) -> Option<&u16> { + self.https_status_code.as_ref() + } + + pub fn https_latency_ms(&self) -> Option<&u64> { + self.https_latency_ms.as_ref() + } + + pub fn endpoint_used(&self) -> Option<&String> { + self.endpoint_used.as_ref() + } +} diff --git a/nym-gateway-probe/src/common/types.rs b/nym-gateway-probe/src/common/types.rs index ec887d61fb..f2027ec78f 100644 --- a/nym-gateway-probe/src/common/types.rs +++ b/nym-gateway-probe/src/common/types.rs @@ -1,6 +1,8 @@ use nym_connection_monitor::ConnectionStatusEvent; use serde::{Deserialize, Serialize}; +pub use super::socks5_test::HttpsConnectivityResult; + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProbeResult { pub node: String, @@ -12,6 +14,7 @@ pub struct ProbeResult { pub struct ProbeOutcome { pub as_entry: Entry, pub as_exit: Option, + pub socks5: Option, pub wg: Option, pub lp: Option, } @@ -132,6 +135,38 @@ impl Exit { } } +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Socks5ProbeResults { + /// whether we could establish a SOCKS5 proxy connection + can_connect_socks5: bool, + + /// HTTPS connectivity test + https_connectivity: HttpsConnectivityResult, +} + +impl Socks5ProbeResults { + pub fn with_http_result(https_connectivity: HttpsConnectivityResult) -> Self { + Self { + can_connect_socks5: true, + https_connectivity, + } + } + + pub fn error_before_connecting(error: impl Into) -> Self { + Self { + can_connect_socks5: false, + https_connectivity: HttpsConnectivityResult::with_errors(vec![error.into()]), + } + } + + pub fn error_after_connecting(error: impl Into) -> Self { + Self { + can_connect_socks5: true, + https_connectivity: HttpsConnectivityResult::with_errors(vec![error.into()]), + } + } +} + #[derive(Debug, Clone, Default)] pub struct IpPingReplies { pub ipr_tun_ip_v4: bool, diff --git a/nym-gateway-probe/src/config/mod.rs b/nym-gateway-probe/src/config/mod.rs index 3601045253..a87be29839 100644 --- a/nym-gateway-probe/src/config/mod.rs +++ b/nym-gateway-probe/src/config/mod.rs @@ -3,8 +3,10 @@ mod credentials; mod netstack; +mod socks5; mod test_mode; pub use credentials::CredentialArgs; pub use netstack::NetstackArgs; +pub use socks5::Socks5Args; pub use test_mode::TestMode; diff --git a/nym-gateway-probe/src/config/socks5.rs b/nym-gateway-probe/src/config/socks5.rs new file mode 100644 index 0000000000..5c2a29c80a --- /dev/null +++ b/nym-gateway-probe/src/config/socks5.rs @@ -0,0 +1,32 @@ +use clap::Args; + +use crate::common::socks5_test::JsonRpcClient; + +#[derive(Args)] +pub struct Socks5Args { + #[arg(long, value_delimiter = ';')] + pub socks5_json_rpc_url_list: Vec, + + #[arg(long, default_value_t = 30)] + pub mixnet_client_timeout_sec: u64, + + #[arg(long, default_value_t = 10)] + pub test_count: u64, + + /// stops socks5 test early after this many failed attempts + #[arg(long, default_value_t = 3)] + pub failure_count_cutoff: usize, +} + +impl Socks5Args { + pub async fn validate_socks5_endpoints(&self) -> anyhow::Result<()> { + let client = JsonRpcClient::new( + self.mixnet_client_timeout_sec, + None, + self.socks5_json_rpc_url_list.clone(), + )?; + client.ensure_endpoint_works().await?; + + Ok(()) + } +} diff --git a/nym-gateway-probe/src/lib.rs b/nym-gateway-probe/src/lib.rs index ccd42a4121..cbd388efeb 100644 --- a/nym-gateway-probe/src/lib.rs +++ b/nym-gateway-probe/src/lib.rs @@ -2,9 +2,13 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::common::helpers; -use crate::common::probe_tests::{do_ping, lp_registration_probe, wg_probe, wg_probe_lp}; -use crate::common::types::{Entry, Exit, WgProbeResults}; +use crate::common::probe_tests::{ + do_ping, do_socks5_connectivity_test, lp_registration_probe, wg_probe, wg_probe_lp, +}; +use crate::common::types::{Entry, Exit, Socks5ProbeResults, WgProbeResults}; +use crate::config::Socks5Args; use anyhow::bail; +use nym_api_requests::models::NetworkRequesterDetailsV1; use nym_authenticator_client::{AuthClientMixnetListener, AuthenticatorClient}; use nym_client_core::config::ForgetMe; use nym_config::defaults::NymNetworkDetails; @@ -14,6 +18,7 @@ use nym_sdk::mixnet::{ CredentialStorage, Ephemeral, KeyStore, MixnetClient, MixnetClientBuilder, MixnetClientStorage, NodeIdentity, StoragePaths, }; +use nym_topology::NymTopology; use rand::rngs::OsRng; use std::path::PathBuf; use std::sync::Arc; @@ -48,6 +53,7 @@ pub struct Probe { localnet_entry: Option, /// Localnet exit gateway info (used when --exit-gateway-identity is specified) localnet_exit: Option, + socks5_args: Socks5Args, } impl Probe { @@ -56,6 +62,7 @@ impl Probe { tested_node: TestedNode, netstack_args: NetstackArgs, credentials_args: CredentialArgs, + socks5_args: Socks5Args, ) -> Self { Self { entrypoint, @@ -67,6 +74,7 @@ impl Probe { exit_gateway_node: None, localnet_entry: None, localnet_exit: None, + socks5_args, } } @@ -77,6 +85,7 @@ impl Probe { netstack_args: NetstackArgs, credentials_args: CredentialArgs, gateway_node: DirectoryNode, + socks5_args: Socks5Args, ) -> Self { Self { entrypoint, @@ -88,6 +97,7 @@ impl Probe { exit_gateway_node: None, localnet_entry: None, localnet_exit: None, + socks5_args, } } @@ -99,6 +109,7 @@ impl Probe { credentials_args: CredentialArgs, entry_gateway_node: DirectoryNode, exit_gateway_node: DirectoryNode, + socks5_args: Socks5Args, ) -> Self { Self { entrypoint, @@ -110,6 +121,7 @@ impl Probe { exit_gateway_node: Some(exit_gateway_node), localnet_entry: None, localnet_exit: None, + socks5_args, } } @@ -120,6 +132,7 @@ impl Probe { exit: Option, netstack_args: NetstackArgs, credentials_args: CredentialArgs, + socks5_args: Socks5Args, ) -> Self { let entrypoint = entry.identity; Self { @@ -132,6 +145,7 @@ impl Probe { exit_gateway_node: None, localnet_entry: Some(entry), localnet_exit: exit, + socks5_args, } } @@ -150,6 +164,7 @@ impl Probe { only_lp_registration: bool, test_lp_wg: bool, min_mixnet_performance: Option, + network_details: NymNetworkDetails, ) -> anyhow::Result { let tickets_materials = self.credentials_args.decode_attached_ticket_materials()?; @@ -161,7 +176,7 @@ impl Probe { // Connect to the mixnet via the entry gateway let disconnected_mixnet_client = MixnetClientBuilder::new_with_storage(storage.clone()) .request_gateway(mixnet_entry_gateway_id.to_string()) - .network_details(NymNetworkDetails::new_from_env()) + .network_details(network_details.clone()) .debug_config(helpers::mixnet_debug_config( min_mixnet_performance, ignore_egress_epoch_role, @@ -176,6 +191,15 @@ impl Probe { let mixnet_client = Box::pin(disconnected_mixnet_client.connect_to_mixnet()).await; + // Extract topology from the connected client (if successful) to reuse for SOCKS5 test + let topology = match &mixnet_client { + Ok(client) => client + .read_current_route_provider() + .await + .map(|rp| rp.topology.clone()), + Err(_) => None, + }; + // Convert legacy flags to TestMode let has_exit = self.exit_gateway_node.is_some() || self.localnet_exit.is_some(); let test_mode = @@ -192,6 +216,8 @@ impl Probe { test_mode, only_wireguard, false, // Not using mock ecash in regular probe mode + network_details, + topology, ) .await } @@ -209,6 +235,7 @@ impl Probe { test_lp_wg: bool, min_mixnet_performance: Option, use_mock_ecash: bool, + network_details: NymNetworkDetails, ) -> anyhow::Result { // Localnet mode - identity + LP address from CLI, no HTTP query // This path is used when --entry-gateway-identity is specified @@ -249,6 +276,8 @@ impl Probe { test_mode, only_wireguard, use_mock_ecash, + network_details, + None, // No topology (no mixnet client in localnet mode) ) .await; } @@ -296,6 +325,8 @@ impl Probe { test_mode, only_wireguard, use_mock_ecash, + network_details, + None, // No topology (no mixnet client in direct gateway mode) ) .await; } @@ -328,7 +359,7 @@ impl Probe { // and keeps its bandwidth between probe runs let disconnected_mixnet_client = MixnetClientBuilder::new_with_storage(storage.clone()) .request_gateway(mixnet_entry_gateway_id.to_string()) - .network_details(NymNetworkDetails::new_from_env()) + .network_details(network_details.clone()) .debug_config(helpers::mixnet_debug_config( min_mixnet_performance, ignore_egress_epoch_role, @@ -371,6 +402,15 @@ impl Probe { let mixnet_client = Box::pin(disconnected_mixnet_client.connect_to_mixnet()).await; + // extract topology from the connected client (if any) to reuse for SOCKS5 test + let topology = match &mixnet_client { + Ok(client) => client + .read_current_route_provider() + .await + .map(|rp| rp.topology.clone()), + Err(_) => None, + }; + // Convert legacy flags to TestMode let has_exit = self.exit_gateway_node.is_some() || self.localnet_exit.is_some(); let test_mode = @@ -387,6 +427,8 @@ impl Probe { test_mode, only_wireguard, use_mock_ecash, + network_details, + topology, ) .await } @@ -458,11 +500,44 @@ impl Probe { Some(Exit::fail_to_connect()) }, wg: None, + socks5: None, lp: Some(lp_outcome), }, }) } + async fn test_socks5_if_possible( + &self, + network_details: NymNetworkDetails, + network_requester_details: &Option, + directory: &NymApiDirectory, + topology: Option, + ) -> Option { + if let Some(nr_details) = network_requester_details { + match do_socks5_connectivity_test( + &nr_details.address, + network_details, + directory, + self.socks5_args.socks5_json_rpc_url_list.clone(), + self.socks5_args.mixnet_client_timeout_sec, + self.socks5_args.test_count, + self.socks5_args.failure_count_cutoff, + topology, + ) + .await + { + Ok(results) => Some(results), + Err(e) => { + error!("SOCKS5 test failed: {}", e); + None + } + } + } else { + info!("No NR available, skipping SOCKS5 tests"); + None + } + } + pub async fn lookup_gateway( &self, directory: &Option, @@ -535,11 +610,16 @@ impl Probe { test_mode: TestMode, only_wireguard: bool, use_mock_ecash: bool, + network_details: NymNetworkDetails, + topology: Option, ) -> anyhow::Result where T: MixnetClientStorage + Clone + 'static, ::StorageError: Send + Sync, { + let Some(directory) = directory else { + bail!("You need to provide NYM API through environment") + }; // test_mode replaces the old only_lp_registration and test_lp_wg flags. // only_wireguard is kept separate as it controls ping behavior within Mixnet mode. let mut rng = rand::thread_rng(); @@ -557,6 +637,7 @@ impl Probe { Entry::EntryFailure }, as_exit: None, + socks5: None, wg: None, lp: None, }, @@ -595,6 +676,7 @@ impl Probe { Entry::NotTested }, as_exit: None, + socks5: None, wg: None, lp: None, }), @@ -609,6 +691,7 @@ impl Probe { Ok(ProbeOutcome { as_entry: Entry::NotTested, as_exit: None, + socks5: None, wg: None, lp: None, }), @@ -624,6 +707,7 @@ impl Probe { Entry::EntryFailure }, as_exit: None, + socks5: None, wg: None, lp: None, }), @@ -681,8 +765,6 @@ impl Probe { // The tested node is the exit let exit_gateway = node_info.clone(); - let directory = directory - .ok_or_else(|| anyhow::anyhow!("Directory is required for LP-WG test mode"))?; let entry_gateway_node = directory.entry_gateway(&mixnet_entry_gateway_id)?; let entry_gateway = entry_gateway_node.to_testable_node()?; @@ -723,9 +805,8 @@ impl Probe { Arc::new(KeyPair::new(&mut rng)), ip_address, ); - let config = nym_validator_client::nyxd::Config::try_from_nym_network_details( - &NymNetworkDetails::new_from_env(), - )?; + let config = + nym_validator_client::nyxd::Config::try_from_nym_network_details(&network_details)?; let client = nym_validator_client::nyxd::NyxdClient::connect(config, nyxd_url.as_str())?; let bw_controller = nym_bandwidth_controller::BandwidthController::new( @@ -790,10 +871,21 @@ impl Probe { None }; + // test failure doesn't stop further tests + let socks5_outcome = self + .test_socks5_if_possible( + network_details, + &node_info.network_requester_details, + directory, + topology, + ) + .await; + // Disconnect the mixnet client gracefully outcome.map(|mut outcome| { outcome.wg = Some(wg_outcome); outcome.lp = lp_outcome; + outcome.socks5 = socks5_outcome; ProbeResult { node: node_info.identity.to_string(), used_entry: mixnet_entry_gateway_id.to_string(), diff --git a/nym-gateway-probe/src/run.rs b/nym-gateway-probe/src/run.rs index d03a9aa267..889617406c 100644 --- a/nym-gateway-probe/src/run.rs +++ b/nym-gateway-probe/src/run.rs @@ -6,6 +6,7 @@ use clap::{Parser, Subcommand}; use nym_bin_common::bin_info; use nym_config::defaults::setup_env; use nym_crypto::asymmetric::{ed25519, x25519}; +use nym_gateway_probe::config::Socks5Args; use nym_gateway_probe::{ CredentialArgs, NetstackArgs, NymApiDirectory, ProbeResult, TestMode, TestedNode, TestedNodeDetails, TestedNodeLpDetails, query_gateway_by_ip, @@ -172,6 +173,10 @@ struct CliArgs { /// Arguments to manage credentials #[command(flatten)] credential_args: CredentialArgs, + + /// Arguments to configure socks5 probe + #[command(flatten)] + socks5_args: Socks5Args, } const DEFAULT_CONFIG_DIR: &str = "/tmp/nym-gateway-probe/config/"; @@ -263,6 +268,8 @@ pub(crate) async fn run() -> anyhow::Result { .map(|ep| ep.nyxd_url()) .ok_or(anyhow::anyhow!("missing nyxd url"))?; + args.socks5_args.validate_socks5_endpoints().await?; + // Three resolution modes in priority order: // 1. Localnet mode: --entry-gateway-identity provided (no HTTP query) // 2. Direct IP mode: --gateway-ip provided (queries HTTP API) @@ -381,6 +388,7 @@ pub(crate) async fn run() -> anyhow::Result { exit_details, args.netstack_args, args.credential_args, + args.socks5_args, ); if let Some(awg_args) = args.amnezia_args { @@ -414,6 +422,7 @@ pub(crate) async fn run() -> anyhow::Result { test_lp_wg, args.min_gateway_mixnet_performance, *use_mock_ecash, + network, )) .await } @@ -426,6 +435,7 @@ pub(crate) async fn run() -> anyhow::Result { only_lp_registration, test_lp_wg, args.min_gateway_mixnet_performance, + network, )) .await } @@ -515,6 +525,7 @@ pub(crate) async fn run() -> anyhow::Result { args.credential_args, entry_node.clone(), exit_node.clone(), + args.socks5_args, ) } else if let Some(gw_node) = gateway_node { // Only entry gateway provided @@ -524,17 +535,24 @@ pub(crate) async fn run() -> anyhow::Result { args.netstack_args, args.credential_args, gw_node, + args.socks5_args, ) } else { // No direct gateways, use directory lookup - nym_gateway_probe::Probe::new(entry, test_point, args.netstack_args, args.credential_args) + nym_gateway_probe::Probe::new( + entry, + test_point, + args.netstack_args, + args.credential_args, + args.socks5_args, + ) }; if let Some(awg_args) = args.amnezia_args { trial.with_amnezia(&awg_args); } - match &args.command { + match args.command { Some(Commands::RunLocal { mnemonic, config_dir, @@ -559,7 +577,8 @@ pub(crate) async fn run() -> anyhow::Result { only_lp_registration, test_lp_wg, args.min_gateway_mixnet_performance, - *use_mock_ecash, + use_mock_ecash, + network, )) .await } @@ -572,6 +591,7 @@ pub(crate) async fn run() -> anyhow::Result { only_lp_registration, test_lp_wg, args.min_gateway_mixnet_performance, + network, )) .await } diff --git a/nym-node-status-api/nym-node-status-agent/Cargo.toml b/nym-node-status-api/nym-node-status-agent/Cargo.toml index f866a4aacb..23817c5eb2 100644 --- a/nym-node-status-api/nym-node-status-agent/Cargo.toml +++ b/nym-node-status-api/nym-node-status-agent/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-agent" -version = "1.0.7" +version = "1.1.0" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/nym-node-status-api/nym-node-status-agent/run.sh b/nym-node-status-api/nym-node-status-agent/run.sh index 1061ad5f95..2a5a0b5e41 100755 --- a/nym-node-status-api/nym-node-status-agent/run.sh +++ b/nym-node-status-api/nym-node-status-agent/run.sh @@ -1,15 +1,16 @@ #!/bin/bash +# used primarily for local testing + set -eu export ENVIRONMENT=${ENVIRONMENT:-"mainnet"} -probe_git_ref="nym-vpn-core-v1.4.0" - crate_root=$(dirname $(realpath "$0")) +echo crate_root=${crate_root} monorepo_root=$(realpath "${crate_root}/../..") +echo monorepo_root=${monorepo_root} -echo "Expecting nym-vpn-client repo at a sibling level of nym monorepo dir" -gateway_probe_src=$(dirname "${monorepo_root}")/nym-vpn-client/nym-vpn-core +gateway_probe_src="${monorepo_root}/nym-gateway-probe" echo "gateway_probe_src=$gateway_probe_src" set -a @@ -25,7 +26,8 @@ export RUST_LOG="info" NODE_STATUS_AGENT_SERVER_ADDRESS="http://127.0.0.1" NODE_STATUS_AGENT_SERVER_PORT="8000" SERVER="${NODE_STATUS_AGENT_SERVER_ADDRESS}|${NODE_STATUS_AGENT_SERVER_PORT}" -export NODE_STATUS_AGENT_AUTH_KEY="BjyC9SsHAZUzPRkQR4sPTvVrp4GgaquTh5YfSJksvvWT" +# hardcoded key used only for LOCAL TESTING +export NODE_STATUS_AGENT_AUTH_KEY=${NODE_STATUS_AGENT_AUTH_KEY_STAGING:-"BjyC9SsHAZUzPRkQR4sPTvVrp4GgaquTh5YfSJksvvWT"} export NODE_STATUS_AGENT_PROBE_PATH="$crate_root/nym-gateway-probe" export NODE_STATUS_AGENT_PROBE_EXTRA_ARGS="netstack-download-timeout-sec=30,netstack-num-ping=2,netstack-send-timeout-sec=1,netstack-recv-timeout-sec=1" @@ -35,11 +37,9 @@ echo "Running $workers workers in parallel" # build & copy over GW probe function copy_gw_probe() { pushd $gateway_probe_src - git fetch -a - git checkout $probe_git_ref cargo build --release --package nym-gateway-probe - cp target/release/nym-gateway-probe "$crate_root" + cp "${monorepo_root}/target/release/nym-gateway-probe" "$crate_root" $crate_root/nym-gateway-probe --version popd diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index 5020c1bd5c..20e562e662 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -601,6 +601,13 @@ where ); let available_gateways = self.available_gateways().await?; + for node in available_gateways.iter() { + debug!( + "node_id={}, identity_key={}", + node.node_id, + node.identity_key.to_base58_string() + ); + } Ok(GatewaySetup::New { specification: selection_spec,